diff --git a/crates/traverse-cli/src/http_api.rs b/crates/traverse-cli/src/http_api.rs index c4e14141..e9208926 100644 --- a/crates/traverse-cli/src/http_api.rs +++ b/crates/traverse-cli/src/http_api.rs @@ -7434,6 +7434,7 @@ mod tests { event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/crates/traverse-cli/src/main.rs b/crates/traverse-cli/src/main.rs index b9186ac0..5dee9c39 100644 --- a/crates/traverse-cli/src/main.rs +++ b/crates/traverse-cli/src/main.rs @@ -3008,11 +3008,17 @@ fn capability_publish_plan( ) })?; reject_private_contract_scope(&contract_text)?; - // Spec 102: check raw JSON so use_cases (not on CapabilityContract) are visible. + // Spec 102: fail closed on schema ⊆ use_cases before normalization. enforce_contract_surface_coverage(&contract_text)?; // Fail fast on persona_ref gaps before opening a registry PR. enforce_persona_refs_resolve(&contract_text, &request.registry_repo_path)?; + let raw_contract_value: Value = serde_json::from_str(&contract_text).map_err(|error| { + ( + "capability_publish_contract_parse_failed", + format!("failed to parse capability contract JSON: {error}"), + ) + })?; let contract = parse_contract(&contract_text).map_err(|failure| { ( "capability_publish_contract_parse_failed", @@ -3063,6 +3069,7 @@ fn capability_publish_plan( format!("failed to serialize normalized capability contract: {error}"), ) })?; + merge_author_fields_into_publish_contract(&mut contract_value, &raw_contract_value); contract_value["artifact"] = serde_json::json!({ "digest": artifact_digest, "url": artifact_url, @@ -3090,6 +3097,20 @@ fn capability_publish_plan( }) } +/// Spec 102 FR-005: preserve author `use_cases` and `evidence` into registry-bound JSON. +/// `validate_contract` clears evidence on normalize; merge both from the raw author JSON. +fn merge_author_fields_into_publish_contract( + contract_value: &mut Value, + raw_contract_value: &Value, +) { + if let Some(use_cases) = raw_contract_value.get("use_cases") { + contract_value["use_cases"] = use_cases.clone(); + } + if let Some(evidence) = raw_contract_value.get("evidence") { + contract_value["evidence"] = evidence.clone(); + } +} + fn reject_private_contract_scope(contract_text: &str) -> Result<(), (&'static str, String)> { let value: Value = serde_json::from_str(contract_text).map_err(|error| { ( @@ -3107,8 +3128,8 @@ fn reject_private_contract_scope(contract_text: &str) -> Result<(), (&'static st Ok(()) } -/// Spec `102-contract-surface-coverage` FR-001/FR-002: every `action` enum value -/// must appear in at least one `use_cases[].input_example.action`. +/// Spec `102-contract-surface-coverage` (Decision 58): fail closed when use_cases +/// do not cover declared input enums, required props, or output reason_code/status enums. fn enforce_contract_surface_coverage(contract_text: &str) -> Result<(), (&'static str, String)> { let value: Value = serde_json::from_str(contract_text).map_err(|error| { ( @@ -3116,59 +3137,294 @@ fn enforce_contract_surface_coverage(contract_text: &str) -> Result<(), (&'stati format!("failed to parse capability contract JSON: {error}"), ) })?; - match uncovered_action_enum_values(&value) { - Ok(uncovered) if uncovered.is_empty() => Ok(()), - Ok(uncovered) => Err(( + match surface_coverage_gap_messages(&value) { + Ok(gaps) if gaps.is_empty() => Ok(()), + Ok(gaps) => Err(( "capability_publish_surface_coverage_failed", format!( - "inputs.schema.properties.action.enum values lack covering use_cases: {}. Every enum value must appear as use_cases[].input_example.action (spec 102-contract-surface-coverage FR-001)", - uncovered.join(", ") + "{}. (spec 102-contract-surface-coverage Decision 58 / FR-001–FR-004)", + gaps.join("; ") ), )), Err(message) => Err(("capability_publish_surface_coverage_failed", message)), } } +fn surface_coverage_gap_messages(contract: &Value) -> Result, String> { + let use_cases = contract + .get("use_cases") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); + if use_cases.is_empty() { + return Ok(vec![ + "use_cases is missing or empty; every capability contract must declare at least one use case (FR-004)" + .to_string(), + ]); + } + + let mut gaps = Vec::new(); + + let uncovered_enums = uncovered_input_schema_enum_values(contract, &use_cases)?; + if !uncovered_enums.is_empty() { + gaps.push(format!( + "inputs.schema string enum values lack covering use_cases[].input_example at the same path: {}", + uncovered_enums.join(", ") + )); + } + + let uncovered_required = uncovered_required_input_properties(contract, &use_cases); + if !uncovered_required.is_empty() { + gaps.push(format!( + "inputs.schema.required properties missing from every use_cases[].input_example: {}", + uncovered_required.join(", ") + )); + } + + let uncovered_outputs = uncovered_output_enum_values(contract, &use_cases)?; + if !uncovered_outputs.is_empty() { + gaps.push(format!( + "outputs.schema enum values lack covering use_cases[].output_example: {}", + uncovered_outputs.join(", ") + )); + } + + Ok(gaps) +} + +/// Legacy helper retained for action-focused unit tests: uncovered `action` enum values only. +#[cfg(test)] fn uncovered_action_enum_values(contract: &Value) -> Result, String> { - let Some(action_schema) = contract.pointer("/inputs/schema/properties/action") else { + let use_cases = contract + .get("use_cases") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); + let all = uncovered_input_schema_enum_values(contract, &use_cases)?; + Ok(all + .into_iter() + .filter_map(|entry| match entry.split_once('=') { + Some(("action", value)) => Some(value.to_string()), + _ => None, + }) + .collect()) +} + +fn uncovered_input_schema_enum_values( + contract: &Value, + use_cases: &[Value], +) -> Result, String> { + let Some(schema) = contract.pointer("/inputs/schema") else { return Ok(Vec::new()); }; - let Some(enum_values) = action_schema.get("enum").and_then(Value::as_array) else { - return Ok(Vec::new()); + let mut declared: Vec<(String, String)> = Vec::new(); + collect_string_enums_under_properties(schema, "", &mut declared)?; + let mut uncovered = Vec::new(); + for (path, enum_value) in declared { + let covered = use_cases.iter().any(|use_case| { + use_case + .get("input_example") + .is_some_and(|example| example_covers_path_string(example, &path, &enum_value)) + }); + if !covered { + uncovered.push(format!("{path}={enum_value}")); + } + } + Ok(uncovered) +} + +fn uncovered_required_input_properties(contract: &Value, use_cases: &[Value]) -> Vec { + let Some(required) = contract + .pointer("/inputs/schema/required") + .and_then(Value::as_array) + else { + return Vec::new(); }; - let mut declared = Vec::new(); - for value in enum_values { - let Some(as_str) = value.as_str() else { - return Err( - "inputs.schema.properties.action.enum must contain only strings (spec 102 FR-001)" - .to_string(), - ); + let mut uncovered = Vec::new(); + for item in required { + let Some(name) = item.as_str() else { + continue; }; - declared.push(as_str.to_string()); + let covered = use_cases.iter().any(|use_case| { + use_case + .get("input_example") + .and_then(Value::as_object) + .is_some_and(|obj| obj.contains_key(name)) + }); + if !covered { + uncovered.push(name.to_string()); + } } - if declared.is_empty() { - return Ok(Vec::new()); + uncovered +} + +fn uncovered_output_enum_values( + contract: &Value, + use_cases: &[Value], +) -> Result, String> { + let mut uncovered = Vec::new(); + for field in ["reason_code", "status"] { + let Some(field_schema) = contract.pointer(&format!("/outputs/schema/properties/{field}")) + else { + continue; + }; + let Some(enum_values) = field_schema.get("enum").and_then(Value::as_array) else { + continue; + }; + for value in enum_values { + let Some(as_str) = value.as_str() else { + return Err(format!( + "outputs.schema.properties.{field}.enum must contain only strings (spec 102 FR-003)" + )); + }; + let covered = use_cases.iter().any(|use_case| { + use_case + .pointer(&format!("/output_example/{field}")) + .and_then(Value::as_str) + == Some(as_str) + }); + if !covered { + uncovered.push(format!("{field}={as_str}")); + } + } } + Ok(uncovered) +} - let use_cases = contract +/// Walk `properties` recursively (and array `items`), collecting string enums. +/// Skips schemas under `additionalProperties`. +fn collect_string_enums_under_properties( + schema: &Value, + path_prefix: &str, + out: &mut Vec<(String, String)>, +) -> Result<(), String> { + let Some(properties) = schema.get("properties").and_then(Value::as_object) else { + return Ok(()); + }; + for (name, child) in properties { + let path = if path_prefix.is_empty() { + name.clone() + } else { + format!("{path_prefix}.{name}") + }; + if let Some(enum_values) = child.get("enum").and_then(Value::as_array) { + let mut strings = Vec::new(); + for value in enum_values { + let Some(as_str) = value.as_str() else { + return Err(format!( + "inputs.schema property '{path}' enum must contain only strings (spec 102 FR-001)" + )); + }; + strings.push(as_str.to_string()); + } + if !strings.is_empty() { + for value in strings { + out.push((path.clone(), value)); + } + } + } + collect_string_enums_under_properties(child, &path, out)?; + if let Some(items) = child.get("items") { + collect_string_enums_under_properties(items, &path, out)?; + } + } + Ok(()) +} + +fn example_covers_path_string(example: &Value, path: &str, expected: &str) -> bool { + let parts: Vec<&str> = path.split('.').filter(|part| !part.is_empty()).collect(); + example_covers_path_parts(example, &parts, expected) +} + +fn example_covers_path_parts(node: &Value, parts: &[&str], expected: &str) -> bool { + if parts.is_empty() { + return node.as_str() == Some(expected); + } + match node { + Value::Array(items) => items + .iter() + .any(|item| example_covers_path_parts(item, parts, expected)), + Value::Object(map) => { + let Some(child) = map.get(parts[0]) else { + return false; + }; + example_covers_path_parts(child, &parts[1..], expected) + } + _ => false, + } +} + +/// Spec 102 FR-007: each `use_cases[i]` must have a matching `runtime-requests/ucNN-*.json` +/// fixture (1-based index, zero-padded to two digits). +#[cfg(test)] +fn use_case_smoke_coverage_gaps( + use_case_count: usize, + runtime_request_filenames: &[String], +) -> Vec { + let mut gaps = Vec::new(); + for index in 1..=use_case_count { + let prefix = format!("uc{index:02}-"); + let found = runtime_request_filenames.iter().any(|name| { + name.starts_with(&prefix) + && Path::new(name) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("json")) + && !name.contains('/') + }); + if !found { + gaps.push(format!( + "use_cases[{}] lacks runtime-requests/{prefix}*.json (spec 102 FR-007)", + index.saturating_sub(1) + )); + } + } + gaps +} + +#[cfg(test)] +fn use_case_smoke_coverage_gaps_for_package(package_dir: &Path) -> Result, String> { + let contract_path = package_dir.join("contract.json"); + let contract_text = fs::read_to_string(&contract_path) + .map_err(|error| format!("failed to read {}: {error}", contract_path.display()))?; + let contract: Value = serde_json::from_str(&contract_text) + .map_err(|error| format!("failed to parse {}: {error}", contract_path.display()))?; + let use_case_count = contract .get("use_cases") .and_then(Value::as_array) - .cloned() - .unwrap_or_default(); - let mut covered = std::collections::BTreeSet::new(); - for use_case in &use_cases { - if let Some(action) = use_case - .pointer("/input_example/action") - .and_then(Value::as_str) + .map_or(0, Vec::len); + if use_case_count == 0 { + return Ok(vec![ + "use_cases is missing or empty; cannot verify smoke coverage (spec 102 FR-004/FR-007)" + .to_string(), + ]); + } + let requests_dir = package_dir.join("runtime-requests"); + if !requests_dir.is_dir() { + return Ok(vec![format!( + "runtime-requests/ directory missing under {} (spec 102 FR-007)", + package_dir.display() + )]); + } + let mut filenames = Vec::new(); + for entry in fs::read_dir(&requests_dir) + .map_err(|error| format!("failed to read {}: {error}", requests_dir.display()))? + { + let entry = entry.map_err(|error| { + format!( + "failed to read entry under {}: {error}", + requests_dir.display() + ) + })?; + let name = entry.file_name().to_string_lossy().into_owned(); + if Path::new(&name) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("json")) { - covered.insert(action.to_string()); + filenames.push(name); } } - - Ok(declared - .into_iter() - .filter(|action| !covered.contains(action)) - .collect()) + filenames.sort(); + Ok(use_case_smoke_coverage_gaps(use_case_count, &filenames)) } /// Resolve each `use_cases[].persona_ref` against `personas///persona.json` @@ -3466,7 +3722,7 @@ fn capability_publish_pr_body(plan: &CapabilityPublishPlan) -> String { // Governing specs must be from the *registry* approved-spec set, not Traverse's. // Declaring Traverse-only IDs (056/054/102) fails registry `spec-alignment`. format!( - "## Summary\n\n- publish `{}` version `{}` to the public capability registry\n- add `{}`\n\n## Governing Spec\n\n- `001-registry-foundation`\n- `002-capability-validation`\n- `005-yank-deprecation`\n- `006-public-scope-and-identity`\n- `007-artifact-hosting`\n\n## Project Item\n\n- Capability publish via traverse-cli\n\n## Validation\n\n- local capability contract validation passed\n- contract surface coverage (action enum ⊆ use_cases) passed\n- use_cases persona_ref resolution against registry personas passed\n- artifact digest computed: `{}`\n- release artifact: `{}`\n", + "## Summary\n\n- publish `{}` version `{}` to the public capability registry\n- add `{}`\n\n## Governing Spec\n\n- `001-registry-foundation`\n- `002-capability-validation`\n- `005-yank-deprecation`\n- `006-public-scope-and-identity`\n- `007-artifact-hosting`\n\n## Project Item\n\n- Capability publish via traverse-cli\n\n## Validation\n\n- local capability contract validation passed\n- contract surface coverage (schema ⊆ use_cases) passed\n- use_cases persona_ref resolution against registry personas passed\n- artifact digest computed: `{}`\n- release artifact: `{}`\n", plan.capability_id, plan.version, plan.registry_relative_path.display(), @@ -6151,8 +6407,9 @@ mod tests { publish_file_sha256_digest, register_bundle, register_generated_app_bundle, registry_record_order, registry_sync_at, registry_sync_default_or_override, registry_sync_failure_json, reject_private_contract_scope, run_command, sha256_hex, - telemetry, uncovered_action_enum_values, unresolved_persona_refs, - validate_registry_path_segment, + surface_coverage_gap_messages, telemetry, uncovered_action_enum_values, + unresolved_persona_refs, use_case_smoke_coverage_gaps, + use_case_smoke_coverage_gaps_for_package, validate_registry_path_segment, }; use crate::capability_packages::fnv1a64; use serde_json::Value; @@ -6685,6 +6942,23 @@ mod tests { .expect("published registry contract should parse"); assert_eq!(contract["artifact"]["digest"], json["artifact_digest"]); assert_eq!(contract["artifact"]["url"], json["artifact_url"]); + assert_eq!( + contract["use_cases"].as_array().map_or(0, Vec::len), + 1, + "publish must preserve author use_cases (spec 102 FR-005)" + ); + assert!( + contract["use_cases"][0]["input_example"]["note"] + .as_str() + .is_some(), + "preserved use_cases must keep input_example" + ); + assert!( + contract["evidence"] + .as_array() + .is_some_and(|items| !items.is_empty()), + "publish must preserve author evidence (spec 102 FR-005)" + ); assert!(commands.contains("gh release create artifacts/traverse-starter.process-1.0.0")); assert!(commands.contains("git checkout -B publish/traverse-starter.process-1.0.0")); assert!(commands.contains("gh pr create")); @@ -6780,10 +7054,10 @@ mod tests { }); contract["use_cases"] = serde_json::json!([ { - "name": "create only", - "description": "covers create", - "input_example": { "action": "create" }, - "expected_output_example": { "ok": true } + "scenario": "create only", + "input_example": { "note": "hello", "action": "create" }, + "output_example": { "status": "ok" }, + "happy": true } ]); fs::write( @@ -6821,11 +7095,11 @@ mod tests { .expect("contract fixture should parse"); contract["use_cases"] = serde_json::json!([ { - "name": "missing persona", - "description": "references a persona that is not in the registry checkout", + "scenario": "missing persona", "persona_ref": "missing-persona-for-publish", - "input_example": { "action": "create" }, - "expected_output_example": { "ok": true } + "input_example": { "note": "hello" }, + "output_example": { "status": "ok" }, + "happy": true } ]); fs::write( @@ -6879,11 +7153,11 @@ mod tests { .expect("contract fixture should parse"); contract["use_cases"] = serde_json::json!([ { - "name": "present persona", - "description": "references a persona that exists in the registry checkout", + "scenario": "present persona", "persona_ref": "platform-security-engineer", - "input_example": { "action": "create" }, - "expected_output_example": { "ok": true } + "input_example": { "note": "hello" }, + "output_example": { "status": "ok" }, + "happy": true } ]); fs::write( @@ -6985,15 +7259,22 @@ mod tests { vec!["pin".to_string()] ); - let no_action = serde_json::json!({ "inputs": { "schema": { "properties": {} } } }); + let no_action = serde_json::json!({ + "inputs": { "schema": { "properties": {} } }, + "use_cases": [{ "input_example": {} }] + }); assert!( uncovered_action_enum_values(&no_action) .expect("no action enum") .is_empty() ); - enforce_contract_surface_coverage(r#"{"inputs":{"schema":{"properties":{}}}}"#) - .expect("contracts without action enum must pass"); + let empty_err = + enforce_contract_surface_coverage(r#"{"inputs":{"schema":{"properties":{}}}}"#) + .expect_err("contracts without use_cases must fail"); + assert_eq!(empty_err.0, "capability_publish_surface_coverage_failed"); + assert!(empty_err.1.contains("use_cases")); + let err = enforce_contract_surface_coverage( r#"{"inputs":{"schema":{"properties":{"action":{"enum":["a","b"]}}}},"use_cases":[{"input_example":{"action":"a"}}]}"#, ) @@ -7002,6 +7283,181 @@ mod tests { assert!(err.1.contains('b')); } + #[test] + fn surface_coverage_pass_covers_required_nested_enums_and_outputs() { + let pass = serde_json::json!({ + "inputs": { + "schema": { + "required": ["note", "mode"], + "properties": { + "note": { "type": "string" }, + "mode": { "type": "string", "enum": ["fast", "careful"] }, + "config": { + "type": "object", + "properties": { + "tone": { "type": "string", "enum": ["soft", "direct"] } + } + }, + "extra": { + "additionalProperties": { + "type": "string", + "enum": ["ignored-by-walker"] + } + } + } + } + }, + "outputs": { + "schema": { + "properties": { + "reason_code": { + "type": "string", + "enum": ["ok", "bad_input"] + }, + "status": { + "type": "string", + "enum": ["allow", "deny"] + } + } + } + }, + "use_cases": [ + { + "input_example": { + "note": "n1", + "mode": "fast", + "config": { "tone": "soft" } + }, + "output_example": { "reason_code": "ok", "status": "allow" } + }, + { + "input_example": { + "note": "n2", + "mode": "careful", + "config": { "tone": "direct" } + }, + "output_example": { "reason_code": "bad_input", "status": "deny" } + } + ] + }); + assert!( + surface_coverage_gap_messages(&pass) + .expect("pass fixture") + .is_empty() + ); + enforce_contract_surface_coverage(&pass.to_string()).expect("full coverage must pass"); + } + + #[test] + fn surface_coverage_reports_missing_required_input_properties() { + let required_gap = serde_json::json!({ + "inputs": { + "schema": { + "required": ["note", "mode"], + "properties": { + "note": { "type": "string" }, + "mode": { "type": "string", "enum": ["fast"] } + } + } + }, + "use_cases": [ + { "input_example": { "mode": "fast" }, "output_example": {} } + ] + }); + let required_msgs = + surface_coverage_gap_messages(&required_gap).expect("required gap fixture"); + assert!( + required_msgs.iter().any(|msg| msg.contains("note")), + "expected missing required note, got {required_msgs:?}" + ); + } + + #[test] + fn surface_coverage_reports_nested_input_enum_gaps() { + let nested_gap = serde_json::json!({ + "inputs": { + "schema": { + "properties": { + "config": { + "properties": { + "tone": { "enum": ["soft", "direct"] } + } + } + } + } + }, + "use_cases": [ + { "input_example": { "config": { "tone": "soft" } }, "output_example": {} } + ] + }); + let nested_msgs = surface_coverage_gap_messages(&nested_gap).expect("nested gap fixture"); + assert!( + nested_msgs + .iter() + .any(|msg| msg.contains("config.tone=direct")), + "expected nested enum gap, got {nested_msgs:?}" + ); + } + + #[test] + fn surface_coverage_reports_output_reason_code_enum_gaps() { + let output_gap = serde_json::json!({ + "inputs": { "schema": { "properties": {} } }, + "outputs": { + "schema": { + "properties": { + "reason_code": { "enum": ["ok", "bad_input"] } + } + } + }, + "use_cases": [ + { "input_example": {}, "output_example": { "reason_code": "ok" } } + ] + }); + let output_msgs = surface_coverage_gap_messages(&output_gap).expect("output gap fixture"); + assert!( + output_msgs + .iter() + .any(|msg| msg.contains("reason_code=bad_input")), + "expected reason_code gap, got {output_msgs:?}" + ); + } + + #[test] + fn use_case_smoke_coverage_gaps_require_ucnn_fixtures() { + assert!( + use_case_smoke_coverage_gaps( + 2, + &[ + "uc01-happy.json".to_string(), + "uc02-sad.json".to_string(), + "extra.json".to_string() + ] + ) + .is_empty() + ); + assert_eq!( + use_case_smoke_coverage_gaps(2, &["uc01-happy.json".to_string()]), + vec!["use_cases[1] lacks runtime-requests/uc02-*.json (spec 102 FR-007)".to_string()] + ); + + let temp_dir = unique_temp_dir(); + let package_dir = temp_dir.join("pkg"); + fs::create_dir_all(package_dir.join("runtime-requests")) + .expect("package dirs should create"); + fs::write( + package_dir.join("contract.json"), + r#"{"use_cases":[{"scenario":"a"},{"scenario":"b"}]}"#, + ) + .expect("contract should write"); + fs::write(package_dir.join("runtime-requests/uc01-a.json"), "{}") + .expect("uc01 should write"); + let gaps = use_case_smoke_coverage_gaps_for_package(&package_dir) + .expect("package gaps should compute"); + assert_eq!(gaps.len(), 1); + assert!(gaps[0].contains("uc02-")); + } + #[test] fn app_validate_returns_validated_json_for_checked_in_app_manifest() { let manifest_path = @@ -9120,6 +9576,38 @@ mod tests { &contract_path, ) .expect("capability contract fixture should copy"); + // Spec 102 FR-004: publish fixtures need a non-empty use_cases surface. + let mut contract: Value = serde_json::from_str( + &fs::read_to_string(&contract_path).expect("copied contract should read"), + ) + .expect("copied contract should parse"); + contract["use_cases"] = serde_json::json!([ + { + "scenario": "Process a starter note into structured metadata.", + "input_example": { "note": "Ship the starter app path" }, + "output_example": { + "title": "Ship the starter app path", + "tags": ["starter"], + "noteType": "task", + "suggestedNextAction": "review", + "status": "ok" + }, + "happy": true + } + ]); + // Author evidence must survive normalize (validate_contract clears it). + contract["evidence"] = serde_json::json!([ + { + "evidence_id": "fixture-evd-1", + "type": "contract_validation", + "status": "passed" + } + ]); + fs::write( + &contract_path, + serde_json::to_string_pretty(&contract).expect("fixture contract should serialize"), + ) + .expect("fixture contract should write"); fs::write(&artifact_path, b"fixture wasm bytes").expect("artifact fixture should write"); fs::create_dir_all(®istry_repo_path).expect("registry fixture should create"); diff --git a/crates/traverse-contracts/src/lib.rs b/crates/traverse-contracts/src/lib.rs index 65d6434f..bb79757a 100644 --- a/crates/traverse-contracts/src/lib.rs +++ b/crates/traverse-contracts/src/lib.rs @@ -56,6 +56,20 @@ pub struct CapabilityContract { /// Typed JSON schema for capability state values written through the runtime `DataStore`. #[serde(default)] pub state_schema: Option, + /// Executable surface examples (spec 102). Preserved through publish; not cleared by validate. + #[serde(default)] + pub use_cases: Vec, +} + +/// One authored use case that demonstrates a concrete input/output path for a capability. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UseCase { + pub scenario: String, + pub input_example: Value, + pub output_example: Value, + pub happy: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub persona_ref: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/traverse-contracts/tests/validation.rs b/crates/traverse-contracts/tests/validation.rs index dcc759fb..c2140160 100644 --- a/crates/traverse-contracts/tests/validation.rs +++ b/crates/traverse-contracts/tests/validation.rs @@ -715,6 +715,7 @@ fn valid_contract() -> CapabilityContract { event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/crates/traverse-mcp/src/lib.rs b/crates/traverse-mcp/src/lib.rs index bcd6d697..30cf7351 100644 --- a/crates/traverse-mcp/src/lib.rs +++ b/crates/traverse-mcp/src/lib.rs @@ -1106,6 +1106,7 @@ mod tests { event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/crates/traverse-mcp/tests/mcp_tests.rs b/crates/traverse-mcp/tests/mcp_tests.rs index 40bedf94..4ce1093a 100644 --- a/crates/traverse-mcp/tests/mcp_tests.rs +++ b/crates/traverse-mcp/tests/mcp_tests.rs @@ -128,6 +128,7 @@ fn capability_contract() -> traverse_contracts::CapabilityContract { event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/crates/traverse-runtime/src/data_store.rs b/crates/traverse-runtime/src/data_store.rs index 8e5013e0..5ae22949 100644 --- a/crates/traverse-runtime/src/data_store.rs +++ b/crates/traverse-runtime/src/data_store.rs @@ -2384,6 +2384,7 @@ mod tests { event_trigger: None, connector_requirements: Vec::new(), state_schema, + use_cases: Vec::new(), } } } diff --git a/crates/traverse-runtime/src/lib.rs b/crates/traverse-runtime/src/lib.rs index 4931e7d1..1e6a76b6 100644 --- a/crates/traverse-runtime/src/lib.rs +++ b/crates/traverse-runtime/src/lib.rs @@ -5503,6 +5503,7 @@ mod tests { event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/crates/traverse-runtime/src/security.rs b/crates/traverse-runtime/src/security.rs index 8e0ade0d..8eda25cd 100644 --- a/crates/traverse-runtime/src/security.rs +++ b/crates/traverse-runtime/src/security.rs @@ -534,6 +534,7 @@ mod tests { event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), }; let record = CapabilityRegistryRecord { scope: RegistryScope::Private, diff --git a/crates/traverse-runtime/src/workflows.rs b/crates/traverse-runtime/src/workflows.rs index 67d0d327..243076b3 100644 --- a/crates/traverse-runtime/src/workflows.rs +++ b/crates/traverse-runtime/src/workflows.rs @@ -3550,6 +3550,7 @@ mod tests { }, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/crates/traverse-runtime/tests/expedition_wasm_tests.rs b/crates/traverse-runtime/tests/expedition_wasm_tests.rs index 27ef6dcb..c68a917a 100644 --- a/crates/traverse-runtime/tests/expedition_wasm_tests.rs +++ b/crates/traverse-runtime/tests/expedition_wasm_tests.rs @@ -149,6 +149,7 @@ fn expedition_contract() -> CapabilityContract { event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/crates/traverse-runtime/tests/placement_router_live_wiring.rs b/crates/traverse-runtime/tests/placement_router_live_wiring.rs index 11ad44b2..375603bc 100644 --- a/crates/traverse-runtime/tests/placement_router_live_wiring.rs +++ b/crates/traverse-runtime/tests/placement_router_live_wiring.rs @@ -276,6 +276,7 @@ fn base_contract( event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/crates/traverse-runtime/tests/placement_tests.rs b/crates/traverse-runtime/tests/placement_tests.rs index d15d4eed..a48e940f 100644 --- a/crates/traverse-runtime/tests/placement_tests.rs +++ b/crates/traverse-runtime/tests/placement_tests.rs @@ -88,6 +88,7 @@ fn base_contract() -> CapabilityContract { event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/crates/traverse-runtime/tests/router_tests.rs b/crates/traverse-runtime/tests/router_tests.rs index ee8ebc97..abe9bd5d 100644 --- a/crates/traverse-runtime/tests/router_tests.rs +++ b/crates/traverse-runtime/tests/router_tests.rs @@ -104,6 +104,7 @@ fn base_contract(service_type: ServiceType) -> CapabilityContract { event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/crates/traverse-runtime/tests/runtime.rs b/crates/traverse-runtime/tests/runtime.rs index 06c8cf07..86ca2a96 100644 --- a/crates/traverse-runtime/tests/runtime.rs +++ b/crates/traverse-runtime/tests/runtime.rs @@ -1106,6 +1106,7 @@ fn capability_contract( event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } @@ -1437,6 +1438,7 @@ fn simple_registration(scope: RegistryScope, id: &str, version: &str) -> Capabil event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), }; CapabilityRegistration { scope, diff --git a/crates/traverse-runtime/tests/thread_pool_integration.rs b/crates/traverse-runtime/tests/thread_pool_integration.rs index 0bcadd1e..7f31a835 100644 --- a/crates/traverse-runtime/tests/thread_pool_integration.rs +++ b/crates/traverse-runtime/tests/thread_pool_integration.rs @@ -112,6 +112,7 @@ fn test_contract() -> CapabilityContract { event_trigger: None, connector_requirements: Vec::new(), state_schema: None, + use_cases: Vec::new(), } } diff --git a/docs/adr/0038-contract-surface-coverage.md b/docs/adr/0038-contract-surface-coverage.md index 906078e5..d4625479 100644 --- a/docs/adr/0038-contract-surface-coverage.md +++ b/docs/adr/0038-contract-surface-coverage.md @@ -1,39 +1,47 @@ # ADR-0038: Contract Surface Must Be Covered by Use Cases -- Status: Accepted -- Governing spec: `102-contract-surface-coverage` -- Related issues: traverse#1014, #1015, #1016; registry#192, #193 +- Status: Accepted (amended 2026-08-10 for Decision 58) +- Governing spec: `102-contract-surface-coverage` (v1.1.0 Approved) +- Related issues: traverse#1014, #1015, #1016, #1040; registry#192, #193, #215 ## Context Capability contracts combine: 1. Free-text `summary` / `description` -2. JSON Schema input surface (including discriminator enums such as `action`) +2. JSON Schema input/output surface (enums and required properties) 3. `use_cases[]` with concrete input/output examples 4. An executable WASM artifact verified by package smoke -Only (3) and (4) are mechanically exercised today. `core.process-comment@1.0.0` demonstrated the failure mode: the schema enum and description advertised `resolve` / `pin` / markup sanitisation / allow-list “strict” mentions, while the artifact and eight use cases implemented a narrower matrix. Registry and traverse publish gates accepted the overclaim. +Only (3) and (4) are mechanically exercised. Two failure modes appeared: + +- `core.process-comment@1.0.0` overclaimed `action` enum values beyond its use-case/smoke matrix. +- Loop batch publish validated use cases from raw JSON, then wrote a normalized `CapabilityContract` **without** a `use_cases` field, so registry copies lost them while CI still allowed missing use cases. ## Decision -Adopt **schema ⊆ use_cases ⊆ smoke** as a governed rule for discriminator enums (starting with `action`): +Adopt **schema ⊆ use_cases ⊆ smoke** for the full checkable schema surface (Decision 58): -- Every enum value retained in the contract MUST have at least one use case. -- Publish dry-run MUST fail on gaps once Spec 102 is Approved. +- Every input schema string enum value MUST appear in at least one use case input example. +- Every `inputs.schema.required` property MUST appear in at least one use case input example (no cartesian product). +- Every `reason_code` / `status` output enum value MUST appear in at least one use case output example; those fields MUST be enums when coverage is required. +- `use_cases` MUST be non-empty and MUST survive `capability publish` into the registry record. +- Each use case MUST have a matching smoke fixture asserting its `reason_code` / key outputs. - Description claims beyond use cases MUST be called out under **Known limitations** or removed. - An enum value MUST NOT be “implemented” solely as an undocumented generic unsupported stub. -For already-published overclaims: do not edit immutable versions; publish an honesty bump (e.g. `core.process-comment@1.0.1`) and deprecate the overclaiming version with an explicit reason. +For already-published gaps: do not edit immutable versions; publish an honesty bump and deprecate the dishonest version with an explicit reason. ## Alternatives Considered +- **Minimum use-case counts** — rejected: owner requires coverage of the declared capability surface, not N examples. +- **Cartesian required-field matrices** — rejected as an impractical publish gate. - **Description-only linting (NLP)** — rejected for v1: high false-positive risk; use cases are the executable contract. -- **Require implementing every marketing claim immediately** — rejected as the default honesty path: narrowing the declared surface is a valid fix; full feature completion is a separate product ticket. -- **Gate only in registry** — rejected as sole control: authors need fail-fast in `capability publish --dry-run` before opening a registry PR. Registry SHOULD mirror the check for newly ADDED contracts. +- **CLI-only or registry-only enforcement** — rejected: authors need fail-fast publish dry-run; registry must still reject bypasses. +- **Parallel new coverage spec** — rejected: amend Spec 102 / registry FR-011 instead. ## Consequences -- Traverse gains Spec 102 + publish coverage checker (#1016) after approval. -- Registry gains a diff-based mirror check (registry#192) after the traverse spec is Approved (or a thin registry FR that references it). -- `core.process-comment` honesty bump (#1015 / registry#193) can land under existing `516` without waiting for Spec 102 approval, because it reduces claimed surface to already-tested behavior. +- Traverse Spec 102 v1.1.0 + expanded publish coverage checker (#1040). +- Registry FR-011 becomes MUST for new/changed contracts; CI mirror (#215). +- Honesty patch-bumps for stripped Loop capabilities follow under FR-010. diff --git a/docs/capability-contract-authoring-guide.md b/docs/capability-contract-authoring-guide.md index ed94414b..a4bf6dcc 100644 --- a/docs/capability-contract-authoring-guide.md +++ b/docs/capability-contract-authoring-guide.md @@ -176,17 +176,30 @@ cargo run -p traverse-cli-rs -- bundle register ## Contract surface coverage (honesty) -Treat `use_cases` as the executable promise. If `inputs.schema` declares a -discriminator enum (especially `action`), every enum value MUST appear in at -least one `use_cases[].input_example`, and package smoke SHOULD exercise that -set. Do not list actions the artifact only rejects with a generic -`unsupported_action` unless that failure is itself a documented use case. - -Description prose that mentions behavior beyond the use-case matrix MUST either -be removed or called out under an explicit **Known limitations** section. +Treat `use_cases` as the executable promise for the **entire declared schema +surface** (Spec `102` v1.1.0 / Decision 58) — not a minimum example count: + +- Every string `enum` under `inputs.schema` MUST appear in ≥1 + `use_cases[].input_example` at the same path. +- Every top-level `inputs.schema.required` property MUST appear in ≥1 + `use_cases[].input_example`. +- `outputs.schema.properties.reason_code` and `status`, when used for + checkable outcomes, MUST be enums; every enum value MUST appear in ≥1 + `use_cases[].output_example`. +- `use_cases` MUST be non-empty; `capability publish` preserves them into the + registry record. +- Each use case MUST have a matching package smoke fixture + (`runtime-requests/ucNN-*.json`) that asserts its `reason_code` / key outputs. + +Do not list enum values the artifact only rejects with a generic +`unsupported_*` stub unless that failure is itself a documented use case. +Description prose beyond the use-case matrix MUST be removed or called out +under **Known limitations**. Narrowing an overclaimed surface via an honesty +patch-bump is a valid fix. Governed by Spec `102-contract-surface-coverage` / ADR-0038. -`capability publish` / `--dry-run` enforce enum ⊆ use_cases (issue #1016). +`capability publish` / `--dry-run` and registry CI enforce the gate +(issues #1040 / registry#215). ## Persona references diff --git a/docs/decision-log.md b/docs/decision-log.md index 8014103d..18d2f91d 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -2130,3 +2130,39 @@ Publish and registry validation treated `description` and broad `action` enums a Tickets filed on Project 1 (`#1014`–`#1016`) and Project 3 (`#192`–`#193`). Spec/ADR drafted. Honesty bump proceeds under existing `516` while Spec 102 awaits approval. + +## Decision 58: Full Capability Surface Coverage via Use Cases (Not Minimum Counts) + +- **Date**: 2026-08-10 +- **Status**: Accepted; Spec 102 v1.1.0 Approved 2026-08-10 +- **Governing spec**: `102-contract-surface-coverage` (v1.1.0), ADR-0038 (amended), registry `001` FR-011 +- **Related issues**: traverse `#1040`; registry `#215` +- **Origin**: Post-Loop-batch audit — most registry `core.*` contracts had `use_cases` stripped by publish; owner directed that use cases must cover the entire capability, as a non-negotiable gate. + +### Context + +Decision 57 / Spec 102 v1.0.0 gated only `inputs.schema.properties.action.enum`. Loop capability publish validated use cases from raw JSON, then serialized `CapabilityContract` (which has no `use_cases` field), so registry copies lost them. Registry CI explicitly allowed missing use cases (`test_contract_without_use_cases_is_not_flagged`). Local examples often had use cases and smokes, but the catalog of record did not. + +### Decision + +1. **Coverage target**: the declared schema surface — every input schema string enum value; every `inputs.schema.required` property at least once; every `outputs.schema.properties.reason_code` / `status` enum value. Not a minimum use-case count. No cartesian product of required fields. +2. **Enums for checkable outcomes**: `reason_code` / `status` MUST be schema enums when authors need those outcomes covered; free-string fields are not coverage-checkable. +3. **Smoke linkage**: every `use_cases[]` entry MUST have a matching executable smoke fixture that asserts its `reason_code` / key outputs. +4. **Enforcement**: fail closed in both `capability publish` / `--dry-run` and registry CI for newly ADDED or CHANGED contracts. Publish MUST preserve `use_cases` (and author evidence) into the registry-bound JSON. +5. **History**: do not edit immutable stripped versions in place; honesty patch-bump them under FR-010. +6. **Governance vehicle**: amend Spec 102 (and registry `001` FR-011 from MAY→MUST for new/changed contracts); do not create a parallel coverage law. + +### Alternatives Considered + +- Minimum happy+unhappy counts only — rejected; owner clarified coverage of the whole capability matters, not N. +- Cartesian required-field matrices — rejected as an impractical publish gate. +- NLP description coverage as a blocking gate — deferred; Known limitations remain the honesty path for prose. +- Forward-only gate without republishing stripped caps — rejected; catalog would stay dishonest. +- CLI-only or registry-only enforcement — rejected; both are required. + +### Outcome + +- Spec 102 drafted at v1.1.0 (Draft) and ADR-0038 amended. +- Implementation tracked by traverse `#1040` and registry `#215`. +- Honesty patch-bumps for stripped Loop caps follow once the gate lands. + diff --git a/examples/core-aggregate-team-action-health/contract.json b/examples/core-aggregate-team-action-health/contract.json index ea4d7c79..e4ff3c4f 100644 --- a/examples/core-aggregate-team-action-health/contract.json +++ b/examples/core-aggregate-team-action-health/contract.json @@ -4,14 +4,14 @@ "id": "core.aggregate-team-action-health", "namespace": "core", "name": "aggregate-team-action-health", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Aggregates open action items of a team into a health summary for managers (on-track %, overdue, overloaded owners, top pressure items).", - "description": "Pure aggregation capability used by the Manager Visibility workflow. Returns a compact health snapshot that can be rendered in a dashboard or weekly digest.", + "description": "Pure aggregation capability used by the Manager Visibility workflow. Returns a compact health snapshot that can be rendered in a dashboard or weekly digest.\n\nImplemented and smoke-tested matrix (use_cases):\n- team pulse aggregation with overloaded owners and top pressure items\n- invalid_input when reference_date is empty\n\nKnown limitations (intentional in 1.0.1):\n- Aggregation is memory-only; no persistence or notification side effects", "use_cases": [ { "scenario": "As a manager, I want a quick pulse of my team's action items.", @@ -66,6 +66,28 @@ }, "happy": true, "persona_ref": "collaboration-product-owner" + }, + { + "scenario": "As a workflow author, I want invalid_input when reference_date is empty so bad schedules fail closed.", + "input_example": { + "items": [], + "reference_date": "", + "aggregation_config": { + "version": "1.0", + "overdue_threshold_days": 0 + } + }, + "output_example": { + "total_open": 0, + "on_track_pct": 0, + "overdue_count": 0, + "overloaded_owners": [], + "top_pressure_items": [], + "reason_code": "invalid_input", + "evaluation_trace": [] + }, + "happy": false, + "persona_ref": "runtime-engineer" } ], "inputs": { @@ -122,7 +144,11 @@ } }, "reason_code": { - "type": "string" + "type": "string", + "enum": [ + "ok", + "invalid_input" + ] }, "evaluation_trace": { "type": "array", @@ -176,7 +202,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.aggregate-team-action-health@1.0.0", + "spec_ref": "core.aggregate-team-action-health@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -184,7 +210,7 @@ }, "evidence": [ { - "evidence_id": "core-aggregate-team-action-health-1.0.0-contract-validation", + "evidence_id": "core-aggregate-team-action-health-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-aggregate-team-action-health/manifest.json b/examples/core-aggregate-team-action-health/manifest.json index d1448809..f4ec7373 100644 --- a/examples/core-aggregate-team-action-health/manifest.json +++ b/examples/core-aggregate-team-action-health/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.aggregate-team-action-health-agent", - "version": "1.0.0", - "summary": "Capability package for core.aggregate-team-action-health@1.0.0.", + "version": "1.0.1", + "summary": "Capability package for core.aggregate-team-action-health@1.0.1.", "capability_ref": { "id": "core.aggregate-team-action-health", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.aggregate-team-action-health", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-aggregate-team-action-health/runtime-requests/uc01-team-pulse.json b/examples/core-aggregate-team-action-health/runtime-requests/uc01-team-pulse.json index c43d7c36..c0751b46 100644 --- a/examples/core-aggregate-team-action-health/runtime-requests/uc01-team-pulse.json +++ b/examples/core-aggregate-team-action-health/runtime-requests/uc01-team-pulse.json @@ -4,7 +4,7 @@ "request_id": "core-aggregate-team-action-health-01", "intent": { "capability_id": "core.aggregate-team-action-health", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "items": [ diff --git a/examples/core-aggregate-team-action-health/runtime-requests/uc02-invalid-input.json b/examples/core-aggregate-team-action-health/runtime-requests/uc02-invalid-input.json new file mode 100644 index 00000000..723b971f --- /dev/null +++ b/examples/core-aggregate-team-action-health/runtime-requests/uc02-invalid-input.json @@ -0,0 +1,26 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-aggregate-team-action-health-02", + "intent": { + "capability_id": "core.aggregate-team-action-health", + "capability_version": "1.0.1" + }, + "input": { + "items": [], + "reference_date": "", + "aggregation_config": { + "version": "1.0", + "overdue_threshold_days": 0 + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-aggregate-team-action-health-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-aggregate-team-action-health/workflows/aggregate-team-action-health/workflow.json b/examples/core-aggregate-team-action-health/workflows/aggregate-team-action-health/workflow.json index e40c3298..4761cfe5 100644 --- a/examples/core-aggregate-team-action-health/workflows/aggregate-team-action-health/workflow.json +++ b/examples/core-aggregate-team-action-health/workflows/aggregate-team-action-health/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.aggregate-team-action-health", "name": "aggregate-team-action-health", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -35,7 +35,7 @@ { "node_id": "run", "capability_id": "core.aggregate-team-action-health", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [ "items", diff --git a/examples/core-assign-ownership/contract.json b/examples/core-assign-ownership/contract.json index 16233e24..737eec8a 100644 --- a/examples/core-assign-ownership/contract.json +++ b/examples/core-assign-ownership/contract.json @@ -4,14 +4,14 @@ "id": "core.assign-ownership", "namespace": "core", "name": "assign-ownership", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Resolves and records clear ownership for an action item (single accountable person).", - "description": "Takes a suggested owner (name, email, or id) plus a workspace member list and returns a canonical owner_id or a structured failure. Supports fallback strategies (creator, unassigned, fail). Pure and deterministic — never mutates storage.\n\nResolution order for a non-null suggestion:\n1. exact member id match\n2. case-insensitive email match\n3. case-insensitive name match\nThen apply `ownership_config.fallback` when unresolved.", + "description": "Resolves a suggested owner against workspace members (id, email, name) and applies ownership_config.fallback when unresolved. Pure and deterministic.\n\nImplemented and smoke-tested matrix (use_cases):\n- name_match → ok\n- email_match → ok\n- null suggestion with fallback=creator → ok\n- unknown suggestion with fallback=fail → unresolved\n- inactive member with require_active_member → inactive_member\n- invalid fallback value → config_error\n- unknown suggestion with fallback=unassigned → ok (owner_id null)\n\nKnown limitations (intentional in 1.0.1):\n- Does not mutate storage or notify members; callers persist the decision", "use_cases": [ { "scenario": "As the system, I want a name match to resolve to a canonical user id.", @@ -105,7 +105,7 @@ "reason_code": "ok", "evaluation_trace": [ "suggested_owner null", - "fallback=creator → user-carol" + "fallback=creator" ] }, "happy": true, @@ -134,12 +134,106 @@ "resolution_method": "unresolved", "reason_code": "unresolved", "evaluation_trace": [ - "no member match for Unknown Person", + "no member match for suggestion", "fallback=fail" ] }, "happy": false, "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the system, I want an inactive member match rejected with inactive_member when require_active_member is true.", + "input_example": { + "suggested_owner": "user-ada", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev", + "active": false + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "fail", + "require_active_member": true + } + }, + "output_example": { + "owner_id": null, + "resolution_method": "unresolved", + "reason_code": "inactive_member", + "evaluation_trace": [ + "matched inactive member" + ] + }, + "happy": false, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the system, I want an invalid fallback value rejected with config_error.", + "input_example": { + "suggested_owner": "Ada Lovelace", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev" + }, + { + "id": "user-bob", + "name": "Bob Smith", + "email": "bob@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "bogus", + "require_active_member": true + } + }, + "output_example": { + "owner_id": null, + "resolution_method": "config_error", + "reason_code": "config_error", + "evaluation_trace": [ + "invalid fallback" + ] + }, + "happy": false, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the system, I want unknown suggestions to resolve as unassigned when fallback is unassigned.", + "input_example": { + "suggested_owner": "Unknown Person", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "unassigned", + "require_active_member": true + } + }, + "output_example": { + "owner_id": null, + "resolution_method": "fallback_unassigned", + "reason_code": "ok", + "evaluation_trace": [ + "no member match for suggestion", + "fallback=unassigned" + ] + }, + "happy": true, + "persona_ref": "collaboration-product-owner" } ], "inputs": { @@ -284,7 +378,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.assign-ownership@1.0.0", + "spec_ref": "core.assign-ownership@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -292,7 +386,7 @@ }, "evidence": [ { - "evidence_id": "core-assign-ownership-1.0.0-contract-validation", + "evidence_id": "core-assign-ownership-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-assign-ownership/manifest.json b/examples/core-assign-ownership/manifest.json index 4dcc20e1..37f82a8f 100644 --- a/examples/core-assign-ownership/manifest.json +++ b/examples/core-assign-ownership/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.assign-ownership-agent", - "version": "1.0.0", - "summary": "Capability package for core.assign-ownership@1.0.0 (Loop ownership resolver).", + "version": "1.0.1", + "summary": "Capability package for core.assign-ownership@1.0.1.", "capability_ref": { "id": "core.assign-ownership", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.assign-ownership", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-assign-ownership/runtime-requests/uc01-name-match.json b/examples/core-assign-ownership/runtime-requests/uc01-name-match.json index 160533a9..01324360 100644 --- a/examples/core-assign-ownership/runtime-requests/uc01-name-match.json +++ b/examples/core-assign-ownership/runtime-requests/uc01-name-match.json @@ -4,7 +4,7 @@ "request_id": "core-assign-ownership-01", "intent": { "capability_id": "core.assign-ownership", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "suggested_owner": "Ada Lovelace", diff --git a/examples/core-assign-ownership/runtime-requests/uc02-email-match.json b/examples/core-assign-ownership/runtime-requests/uc02-email-match.json index be55b9d4..2a8121af 100644 --- a/examples/core-assign-ownership/runtime-requests/uc02-email-match.json +++ b/examples/core-assign-ownership/runtime-requests/uc02-email-match.json @@ -4,7 +4,7 @@ "request_id": "core-assign-ownership-02", "intent": { "capability_id": "core.assign-ownership", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "suggested_owner": "bob@loop.dev", diff --git a/examples/core-assign-ownership/runtime-requests/uc03-null-fallback-creator.json b/examples/core-assign-ownership/runtime-requests/uc03-null-fallback-creator.json index c126d759..8976fad5 100644 --- a/examples/core-assign-ownership/runtime-requests/uc03-null-fallback-creator.json +++ b/examples/core-assign-ownership/runtime-requests/uc03-null-fallback-creator.json @@ -4,7 +4,7 @@ "request_id": "core-assign-ownership-03", "intent": { "capability_id": "core.assign-ownership", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "suggested_owner": null, diff --git a/examples/core-assign-ownership/runtime-requests/uc04-unresolved-fail.json b/examples/core-assign-ownership/runtime-requests/uc04-unresolved-fail.json index baa4cb5b..ff346a50 100644 --- a/examples/core-assign-ownership/runtime-requests/uc04-unresolved-fail.json +++ b/examples/core-assign-ownership/runtime-requests/uc04-unresolved-fail.json @@ -4,7 +4,7 @@ "request_id": "core-assign-ownership-04", "intent": { "capability_id": "core.assign-ownership", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "suggested_owner": "Unknown Person", diff --git a/examples/core-assign-ownership/runtime-requests/uc05-inactive-member.json b/examples/core-assign-ownership/runtime-requests/uc05-inactive-member.json new file mode 100644 index 00000000..22d7321e --- /dev/null +++ b/examples/core-assign-ownership/runtime-requests/uc05-inactive-member.json @@ -0,0 +1,35 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-assign-ownership-05", + "intent": { + "capability_id": "core.assign-ownership", + "capability_version": "1.0.1" + }, + "input": { + "suggested_owner": "user-ada", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev", + "active": false + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "fail", + "require_active_member": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-assign-ownership-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-assign-ownership/runtime-requests/uc06-config-error.json b/examples/core-assign-ownership/runtime-requests/uc06-config-error.json new file mode 100644 index 00000000..736ab493 --- /dev/null +++ b/examples/core-assign-ownership/runtime-requests/uc06-config-error.json @@ -0,0 +1,39 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-assign-ownership-06", + "intent": { + "capability_id": "core.assign-ownership", + "capability_version": "1.0.1" + }, + "input": { + "suggested_owner": "Ada Lovelace", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev" + }, + { + "id": "user-bob", + "name": "Bob Smith", + "email": "bob@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "bogus", + "require_active_member": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-assign-ownership-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-assign-ownership/runtime-requests/uc07-fallback-unassigned.json b/examples/core-assign-ownership/runtime-requests/uc07-fallback-unassigned.json new file mode 100644 index 00000000..444491ff --- /dev/null +++ b/examples/core-assign-ownership/runtime-requests/uc07-fallback-unassigned.json @@ -0,0 +1,34 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-assign-ownership-07", + "intent": { + "capability_id": "core.assign-ownership", + "capability_version": "1.0.1" + }, + "input": { + "suggested_owner": "Unknown Person", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "unassigned", + "require_active_member": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-assign-ownership-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-assign-ownership/workflows/assign-ownership/workflow.json b/examples/core-assign-ownership/workflows/assign-ownership/workflow.json index 34639d39..42620f06 100644 --- a/examples/core-assign-ownership/workflows/assign-ownership/workflow.json +++ b/examples/core-assign-ownership/workflows/assign-ownership/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.assign-ownership", "name": "assign-ownership", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -34,7 +34,7 @@ { "node_id": "assign", "capability_id": "core.assign-ownership", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [ "suggested_owner", diff --git a/examples/core-authorize/contract.json b/examples/core-authorize/contract.json index 8ea041db..811f368c 100644 --- a/examples/core-authorize/contract.json +++ b/examples/core-authorize/contract.json @@ -4,14 +4,14 @@ "id": "core.authorize", "namespace": "core", "name": "authorize", - "version": "1.1.0", + "version": "1.1.1", "lifecycle": "active", "owner": { "team": "traverse-core", "contact": "capability-authors@traverse-framework.com" }, "summary": "Pure authorization evaluator: allow/deny for principal+action+resource under a caller-supplied versioned policy.", - "description": "core.authorize is a pure, side-effect-free authorization capability designed for maximum reusability across web, mobile, desktop, edge, and AI-agent runtimes.\n\nIt deliberately separates policy definition from policy evaluation. The caller supplies a complete, versioned policy configuration at invocation time (or a compact reference in future extensions). The same WASM binary can therefore enforce RBAC, ABAC, ReBAC-lite, hybrid, or explicit allow/deny rules without recompilation.\n\nKey design principles (Persona-Council reviewed):\n- Fail-closed by default (default_effect = deny).\n- Fully deterministic: identical inputs always produce identical outputs.\n- No host, network, or filesystem access.\n- Structured, machine-readable output suitable for audit logs, UI enforcement, and downstream agents.\n- Explicit support for multi-tenant isolation, hierarchical roles, time/context conditions, ownership, relationships, high-priority denies, and break-glass overrides.\n- Obligations are first-class structured objects (not free-form strings) so callers know exactly what additional steps are required.\n\nThe capability never mutates state and never contacts external systems. All decision logic lives inside the supplied policy + the deterministic evaluator.\n\nKnown intentional limitations (disclosed):\n- The evaluator implements a practical subset of common authorization patterns; it is not a full XACML or Cedar engine.\n- Complex boolean expressions inside conditions are limited to a small, safe DSL to keep the WASM footprint and attack surface small.\n- Policy size should be kept modest when inlined; very large policy sets should be versioned and referenced externally in future iterations.", + "description": "Pure authorization evaluator: allow/deny for principal+action+resource under a caller-supplied versioned policy (RBAC/hybrid rule matching, tenant/status conditions, break-glass, obligations).\n\nImplemented and smoke-tested matrix (use_cases):\n- admin role allow (matched_allow_rule)\n- tenant isolation deny (matched_deny_rule)\n- owner update allow (matched_allow_rule)\n- suspended principal deny (matched_deny_rule)\n- allow with obligations (matched_allow_rule)\n- no matching rule → deny (no_matching_rule)\n- empty policy rules → empty_or_invalid_policy\n- break-glass override (break_glass_override)\n- missing principal.id → invalid_principal\n- missing action → invalid_action\n- missing resource.type → invalid_resource\n\nKnown limitations (intentional in 1.1.1):\n- policy.mode values abac/explicit removed from schema (not distinct evaluator modes in this binary)\n- default_effect narrowed to deny (fail-closed); allow-default not published\n- obligation severity narrowed to required\n- condition_failed / policy_evaluation_error are not emitted as distinct reason_codes\n- Condition DSL is a practical subset (eq/neq on selected attribute paths), not full XACML/Cedar", "use_cases": [ { "scenario": "As a platform security engineer, I want an admin user with the correct role to be allowed to delete a document so that privileged operations succeed when policy permits them.", @@ -77,7 +77,7 @@ "evaluation_trace": [ "policy_version=1.0 mode=hybrid default=deny", "evaluated 1 rule(s)", - "rule admin-delete matched on role+action+resource.type \u2192 allow" + "rule admin-delete matched on role+action+resource.type → allow" ], "policy_hash": "fnv1a64:3a7f...", "confidence": "high", @@ -155,7 +155,7 @@ "obligations": [], "evaluation_trace": [ "policy_version=1.0 mode=hybrid default=deny", - "rule tenant-isolation condition matched \u2192 deny (priority 200 wins)" + "rule tenant-isolation condition matched → deny (priority 200 wins)" ], "policy_hash": "fnv1a64:9c2e...", "confidence": "high", @@ -214,7 +214,7 @@ ], "obligations": [], "evaluation_trace": [ - "rule owner-update condition principal.id == resource.owner_id \u2192 allow" + "rule owner-update condition principal.id == resource.owner_id → allow" ], "policy_hash": "fnv1a64:1b4d...", "confidence": "high", @@ -285,7 +285,7 @@ ], "obligations": [], "evaluation_trace": [ - "rule suspended-deny matched on principal.attributes.status \u2192 deny (priority 300)" + "rule suspended-deny matched on principal.attributes.status → deny (priority 300)" ], "policy_hash": "fnv1a64:7e8a...", "confidence": "high", @@ -376,7 +376,7 @@ } ], "evaluation_trace": [ - "rule high-value-transfer matched \u2192 allow + 2 obligations" + "rule high-value-transfer matched → allow + 2 obligations" ], "policy_hash": "fnv1a64:4f2c...", "confidence": "high", @@ -555,7 +555,7 @@ ], "evaluation_trace": [ "break_glass attribute present and enabled in policy", - "rule break-glass-allow matched \u2192 allow + obligations" + "rule break-glass-allow matched → allow + obligations" ], "policy_hash": "fnv1a64:bg01...", "confidence": "high", @@ -599,6 +599,110 @@ }, "happy": false, "persona_ref": "client-developer" + }, + { + "scenario": "As a client developer, I want missing action rejected with invalid_action.", + "input_example": { + "principal": { + "id": "user-1", + "roles": [ + "admin" + ] + }, + "action": "", + "resource": { + "type": "document", + "id": "doc-1" + }, + "context": {}, + "policy": { + "version": "1.0", + "mode": "rbac", + "default_effect": "deny", + "rules": [ + { + "id": "r1", + "effect": "allow", + "priority": 1, + "principal": { + "roles": [ + "admin" + ] + }, + "action": [ + "read" + ] + } + ] + } + }, + "output_example": { + "decision": "deny", + "reason": "action is required but missing", + "reason_code": "invalid_action", + "matched_rules": [], + "obligations": [], + "evaluation_trace": [ + "precondition failed: action missing" + ], + "policy_hash": null, + "confidence": "high", + "break_glass_used": false + }, + "happy": false, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As a client developer, I want missing resource.type rejected with invalid_resource.", + "input_example": { + "principal": { + "id": "user-1", + "roles": [ + "admin" + ] + }, + "action": "read", + "resource": { + "type": "", + "id": "doc-1" + }, + "context": {}, + "policy": { + "version": "1.0", + "mode": "rbac", + "default_effect": "deny", + "rules": [ + { + "id": "r1", + "effect": "allow", + "priority": 1, + "principal": { + "roles": [ + "admin" + ] + }, + "action": [ + "read" + ] + } + ] + } + }, + "output_example": { + "decision": "deny", + "reason": "resource.type is required but missing", + "reason_code": "invalid_resource", + "matched_rules": [], + "obligations": [], + "evaluation_trace": [ + "precondition failed: resource.type missing" + ], + "policy_hash": null, + "confidence": "high", + "break_glass_used": false + }, + "happy": false, + "persona_ref": "runtime-engineer" } ], "inputs": { @@ -711,15 +815,12 @@ "type": "string", "enum": [ "rbac", - "abac", - "hybrid", - "explicit" + "hybrid" ] }, "default_effect": { "type": "string", "enum": [ - "allow", "deny" ], "default": "deny" @@ -792,9 +893,7 @@ "severity": { "type": "string", "enum": [ - "required", - "recommended", - "optional" + "required" ] }, "metadata": { @@ -849,9 +948,7 @@ "invalid_principal", "invalid_resource", "invalid_action", - "break_glass_override", - "condition_failed", - "policy_evaluation_error" + "break_glass_override" ] }, "matched_rules": { @@ -891,9 +988,7 @@ "severity": { "type": "string", "enum": [ - "required", - "recommended", - "optional" + "required" ] }, "metadata": { @@ -1007,9 +1102,9 @@ "dependencies": [], "provenance": { "source": "ai-assisted", - "author": "traverse-capability-author + persona-council", + "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T05:36:00Z", - "spec_ref": "core.authorize@1.1.0", + "spec_ref": "core.authorize@1.1.1", "adr_refs": [ "0001-rust-wasm-foundation", "persona-council-review-2026-08-08" @@ -1018,7 +1113,7 @@ }, "evidence": [ { - "evidence_id": "core-authorize-1.1.0-contract-validation", + "evidence_id": "core-authorize-1.1.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-authorize/manifest.json b/examples/core-authorize/manifest.json index db71c3b3..3f49271b 100644 --- a/examples/core-authorize/manifest.json +++ b/examples/core-authorize/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.authorize-agent", - "version": "1.1.0", - "summary": "Capability package for core.authorize@1.1.0 (hybrid policy evaluator, 9 use cases).", + "version": "1.1.1", + "summary": "Capability package for core.authorize@1.1.1.", "capability_ref": { "id": "core.authorize", - "version": "1.1.0", + "version": "1.1.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.authorize", - "workflow_version": "1.1.0" + "workflow_version": "1.1.1" } ], "source": { diff --git a/examples/core-authorize/runtime-requests/uc01-admin-delete-allow.json b/examples/core-authorize/runtime-requests/uc01-admin-delete-allow.json index 28fe9c9a..0296312e 100644 --- a/examples/core-authorize/runtime-requests/uc01-admin-delete-allow.json +++ b/examples/core-authorize/runtime-requests/uc01-admin-delete-allow.json @@ -4,7 +4,7 @@ "request_id": "core-authorize-01", "intent": { "capability_id": "core.authorize", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "principal": { diff --git a/examples/core-authorize/runtime-requests/uc02-tenant-isolation-deny.json b/examples/core-authorize/runtime-requests/uc02-tenant-isolation-deny.json index a76694bb..6a2f1775 100644 --- a/examples/core-authorize/runtime-requests/uc02-tenant-isolation-deny.json +++ b/examples/core-authorize/runtime-requests/uc02-tenant-isolation-deny.json @@ -4,7 +4,7 @@ "request_id": "core-authorize-02", "intent": { "capability_id": "core.authorize", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "principal": { diff --git a/examples/core-authorize/runtime-requests/uc03-owner-update-allow.json b/examples/core-authorize/runtime-requests/uc03-owner-update-allow.json index 1ada4d58..92eab3fb 100644 --- a/examples/core-authorize/runtime-requests/uc03-owner-update-allow.json +++ b/examples/core-authorize/runtime-requests/uc03-owner-update-allow.json @@ -4,7 +4,7 @@ "request_id": "core-authorize-03", "intent": { "capability_id": "core.authorize", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "principal": { diff --git a/examples/core-authorize/runtime-requests/uc04-suspended-deny.json b/examples/core-authorize/runtime-requests/uc04-suspended-deny.json index ef4cdc41..c9453604 100644 --- a/examples/core-authorize/runtime-requests/uc04-suspended-deny.json +++ b/examples/core-authorize/runtime-requests/uc04-suspended-deny.json @@ -4,7 +4,7 @@ "request_id": "core-authorize-04", "intent": { "capability_id": "core.authorize", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "principal": { diff --git a/examples/core-authorize/runtime-requests/uc05-obligations-allow.json b/examples/core-authorize/runtime-requests/uc05-obligations-allow.json index 53195e49..e408b794 100644 --- a/examples/core-authorize/runtime-requests/uc05-obligations-allow.json +++ b/examples/core-authorize/runtime-requests/uc05-obligations-allow.json @@ -4,7 +4,7 @@ "request_id": "core-authorize-05", "intent": { "capability_id": "core.authorize", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "principal": { diff --git a/examples/core-authorize/runtime-requests/uc06-no-match-deny.json b/examples/core-authorize/runtime-requests/uc06-no-match-deny.json index 19cc5d21..617b78ad 100644 --- a/examples/core-authorize/runtime-requests/uc06-no-match-deny.json +++ b/examples/core-authorize/runtime-requests/uc06-no-match-deny.json @@ -4,7 +4,7 @@ "request_id": "core-authorize-06", "intent": { "capability_id": "core.authorize", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "principal": { diff --git a/examples/core-authorize/runtime-requests/uc07-empty-policy-deny.json b/examples/core-authorize/runtime-requests/uc07-empty-policy-deny.json index 61e03b40..75257845 100644 --- a/examples/core-authorize/runtime-requests/uc07-empty-policy-deny.json +++ b/examples/core-authorize/runtime-requests/uc07-empty-policy-deny.json @@ -4,7 +4,7 @@ "request_id": "core-authorize-07", "intent": { "capability_id": "core.authorize", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "principal": { diff --git a/examples/core-authorize/runtime-requests/uc08-break-glass-allow.json b/examples/core-authorize/runtime-requests/uc08-break-glass-allow.json index 386b6e08..d2045de2 100644 --- a/examples/core-authorize/runtime-requests/uc08-break-glass-allow.json +++ b/examples/core-authorize/runtime-requests/uc08-break-glass-allow.json @@ -4,7 +4,7 @@ "request_id": "core-authorize-08", "intent": { "capability_id": "core.authorize", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "principal": { diff --git a/examples/core-authorize/runtime-requests/uc09-invalid-principal-deny.json b/examples/core-authorize/runtime-requests/uc09-invalid-principal-deny.json index 7e130322..f9c47910 100644 --- a/examples/core-authorize/runtime-requests/uc09-invalid-principal-deny.json +++ b/examples/core-authorize/runtime-requests/uc09-invalid-principal-deny.json @@ -4,7 +4,7 @@ "request_id": "core-authorize-09", "intent": { "capability_id": "core.authorize", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "principal": { diff --git a/examples/core-authorize/runtime-requests/uc10-invalid-action-deny.json b/examples/core-authorize/runtime-requests/uc10-invalid-action-deny.json new file mode 100644 index 00000000..8c52892e --- /dev/null +++ b/examples/core-authorize/runtime-requests/uc10-invalid-action-deny.json @@ -0,0 +1,52 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-authorize-10", + "intent": { + "capability_id": "core.authorize", + "capability_version": "1.1.1" + }, + "input": { + "principal": { + "id": "user-1", + "roles": [ + "admin" + ] + }, + "action": "", + "resource": { + "type": "document", + "id": "doc-1" + }, + "context": {}, + "policy": { + "version": "1.0", + "mode": "rbac", + "default_effect": "deny", + "rules": [ + { + "id": "r1", + "effect": "allow", + "priority": 1, + "principal": { + "roles": [ + "admin" + ] + }, + "action": [ + "read" + ] + } + ] + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-authorize-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-authorize/runtime-requests/uc11-invalid-resource-deny.json b/examples/core-authorize/runtime-requests/uc11-invalid-resource-deny.json new file mode 100644 index 00000000..1083976a --- /dev/null +++ b/examples/core-authorize/runtime-requests/uc11-invalid-resource-deny.json @@ -0,0 +1,52 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-authorize-11", + "intent": { + "capability_id": "core.authorize", + "capability_version": "1.1.1" + }, + "input": { + "principal": { + "id": "user-1", + "roles": [ + "admin" + ] + }, + "action": "read", + "resource": { + "type": "", + "id": "doc-1" + }, + "context": {}, + "policy": { + "version": "1.0", + "mode": "rbac", + "default_effect": "deny", + "rules": [ + { + "id": "r1", + "effect": "allow", + "priority": 1, + "principal": { + "roles": [ + "admin" + ] + }, + "action": [ + "read" + ] + } + ] + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-authorize-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-authorize/workflows/authorize/workflow.json b/examples/core-authorize/workflows/authorize/workflow.json index b177b86c..37eaa131 100644 --- a/examples/core-authorize/workflows/authorize/workflow.json +++ b/examples/core-authorize/workflows/authorize/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.authorize", "name": "authorize", - "version": "1.1.0", + "version": "1.1.1", "lifecycle": "active", "owner": { "team": "traverse-core", @@ -40,7 +40,7 @@ { "node_id": "authorize", "capability_id": "core.authorize", - "capability_version": "1.1.0", + "capability_version": "1.1.1", "input": { "from_workflow_input": [ "principal", diff --git a/examples/core-calculate-deadline-pressure/contract.json b/examples/core-calculate-deadline-pressure/contract.json index cd8121b1..d3e52252 100644 --- a/examples/core-calculate-deadline-pressure/contract.json +++ b/examples/core-calculate-deadline-pressure/contract.json @@ -4,14 +4,14 @@ "id": "core.calculate-deadline-pressure", "namespace": "core", "name": "calculate-deadline-pressure", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Computes a deterministic deadline pressure score from due date versus a reference date.", - "description": "Pure WF2 capability used after ownership assignment. Maps days-until-due into a 0.0-1.0 pressure_score and a band (low|medium|high|overdue) using a configurable horizon.", + "description": "Pure WF2 capability used after ownership assignment. Maps days-until-due into a 0.0-1.0 pressure_score and a band (low|medium|high|overdue).\n\nImplemented and smoke-tested matrix (use_cases):\n- due-soon medium pressure\n- overdue max pressure\n- missing_due_date when due_date is empty\n- invalid_date for unparseable dates\n- config_error when reference_date is empty\n\nKnown limitations (intentional in 1.0.1):\n- Band thresholds are fixed heuristic cutovers; no calendar/holiday awareness", "use_cases": [ { "scenario": "As the commit pipeline, I want pressure for an item due in three days.", @@ -70,6 +70,81 @@ }, "happy": true, "persona_ref": "collaboration-product-owner" + }, + { + "scenario": "As WF2, I want missing_due_date when due_date is empty so undated items fail closed.", + "input_example": { + "item": { + "id": "ai-m", + "due_date": "", + "status": "open" + }, + "reference_date": "2026-08-07", + "pressure_config": { + "version": "1.0", + "horizon_days": 14 + } + }, + "output_example": { + "item_id": "ai-m", + "pressure_score": 0, + "days_until_due": 0, + "pressure_band": "unknown", + "reason_code": "missing_due_date", + "evaluation_trace": [] + }, + "happy": false, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As WF2, I want invalid_date when due_date cannot be parsed.", + "input_example": { + "item": { + "id": "ai-i", + "due_date": "not-a-date", + "status": "open" + }, + "reference_date": "2026-08-07", + "pressure_config": { + "version": "1.0", + "horizon_days": 14 + } + }, + "output_example": { + "item_id": "ai-i", + "pressure_score": 0, + "days_until_due": 0, + "pressure_band": "unknown", + "reason_code": "invalid_date", + "evaluation_trace": [] + }, + "happy": false, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As WF2, I want config_error when reference_date is empty.", + "input_example": { + "item": { + "id": "ai-c", + "due_date": "2026-08-10", + "status": "open" + }, + "reference_date": "", + "pressure_config": { + "version": "1.0", + "horizon_days": 14 + } + }, + "output_example": { + "item_id": "", + "pressure_score": 0, + "days_until_due": 0, + "pressure_band": "unknown", + "reason_code": "config_error", + "evaluation_trace": [] + }, + "happy": false, + "persona_ref": "runtime-engineer" } ], "inputs": { @@ -209,7 +284,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.calculate-deadline-pressure@1.0.0", + "spec_ref": "core.calculate-deadline-pressure@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -217,7 +292,7 @@ }, "evidence": [ { - "evidence_id": "core-calculate-deadline-pressure-1.0.0-contract-validation", + "evidence_id": "core-calculate-deadline-pressure-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-calculate-deadline-pressure/manifest.json b/examples/core-calculate-deadline-pressure/manifest.json index 218a7025..79a99ca8 100644 --- a/examples/core-calculate-deadline-pressure/manifest.json +++ b/examples/core-calculate-deadline-pressure/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.calculate-deadline-pressure-agent", - "version": "1.0.0", - "summary": "Capability package for core.calculate-deadline-pressure@1.0.0.", + "version": "1.0.1", + "summary": "Capability package for core.calculate-deadline-pressure@1.0.1.", "capability_ref": { "id": "core.calculate-deadline-pressure", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.calculate-deadline-pressure", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-calculate-deadline-pressure/runtime-requests/uc01-due-soon.json b/examples/core-calculate-deadline-pressure/runtime-requests/uc01-due-soon.json index 932c5229..f96dae3f 100644 --- a/examples/core-calculate-deadline-pressure/runtime-requests/uc01-due-soon.json +++ b/examples/core-calculate-deadline-pressure/runtime-requests/uc01-due-soon.json @@ -4,7 +4,7 @@ "request_id": "core-calculate-deadline-pressure-01", "intent": { "capability_id": "core.calculate-deadline-pressure", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "item": { diff --git a/examples/core-calculate-deadline-pressure/runtime-requests/uc02-overdue.json b/examples/core-calculate-deadline-pressure/runtime-requests/uc02-overdue.json index d638fd56..89356563 100644 --- a/examples/core-calculate-deadline-pressure/runtime-requests/uc02-overdue.json +++ b/examples/core-calculate-deadline-pressure/runtime-requests/uc02-overdue.json @@ -4,7 +4,7 @@ "request_id": "core-calculate-deadline-pressure-02", "intent": { "capability_id": "core.calculate-deadline-pressure", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "item": { diff --git a/examples/core-calculate-deadline-pressure/runtime-requests/uc03-missing-due-date.json b/examples/core-calculate-deadline-pressure/runtime-requests/uc03-missing-due-date.json new file mode 100644 index 00000000..e304a213 --- /dev/null +++ b/examples/core-calculate-deadline-pressure/runtime-requests/uc03-missing-due-date.json @@ -0,0 +1,30 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-calculate-deadline-pressure-03", + "intent": { + "capability_id": "core.calculate-deadline-pressure", + "capability_version": "1.0.1" + }, + "input": { + "item": { + "id": "ai-m", + "due_date": "", + "status": "open" + }, + "reference_date": "2026-08-07", + "pressure_config": { + "version": "1.0", + "horizon_days": 14 + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-calculate-deadline-pressure-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-calculate-deadline-pressure/runtime-requests/uc04-invalid-date.json b/examples/core-calculate-deadline-pressure/runtime-requests/uc04-invalid-date.json new file mode 100644 index 00000000..254e5417 --- /dev/null +++ b/examples/core-calculate-deadline-pressure/runtime-requests/uc04-invalid-date.json @@ -0,0 +1,30 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-calculate-deadline-pressure-04", + "intent": { + "capability_id": "core.calculate-deadline-pressure", + "capability_version": "1.0.1" + }, + "input": { + "item": { + "id": "ai-i", + "due_date": "not-a-date", + "status": "open" + }, + "reference_date": "2026-08-07", + "pressure_config": { + "version": "1.0", + "horizon_days": 14 + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-calculate-deadline-pressure-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-calculate-deadline-pressure/runtime-requests/uc05-config-error.json b/examples/core-calculate-deadline-pressure/runtime-requests/uc05-config-error.json new file mode 100644 index 00000000..8a5f8d70 --- /dev/null +++ b/examples/core-calculate-deadline-pressure/runtime-requests/uc05-config-error.json @@ -0,0 +1,30 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-calculate-deadline-pressure-05", + "intent": { + "capability_id": "core.calculate-deadline-pressure", + "capability_version": "1.0.1" + }, + "input": { + "item": { + "id": "ai-c", + "due_date": "2026-08-10", + "status": "open" + }, + "reference_date": "", + "pressure_config": { + "version": "1.0", + "horizon_days": 14 + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-calculate-deadline-pressure-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-calculate-deadline-pressure/workflows/calculate-deadline-pressure/workflow.json b/examples/core-calculate-deadline-pressure/workflows/calculate-deadline-pressure/workflow.json index 97c11934..4b3d4801 100644 --- a/examples/core-calculate-deadline-pressure/workflows/calculate-deadline-pressure/workflow.json +++ b/examples/core-calculate-deadline-pressure/workflows/calculate-deadline-pressure/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.calculate-deadline-pressure", "name": "calculate-deadline-pressure", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -26,7 +26,7 @@ { "node_id": "run", "capability_id": "core.calculate-deadline-pressure", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [] }, diff --git a/examples/core-calculate-price/contract.json b/examples/core-calculate-price/contract.json index d072ad16..36c618f8 100644 --- a/examples/core-calculate-price/contract.json +++ b/examples/core-calculate-price/contract.json @@ -4,17 +4,17 @@ "id": "core.calculate-price", "namespace": "core", "name": "calculate-price", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "traverse-core", "contact": "capability-authors@traverse-framework.com" }, "summary": "Deterministic price/quote engine. Given line items, quantities, customer context and a versioned pricing configuration, returns unit prices, discounts, taxes and totals with an audit trail.", - "description": "core.calculate-price is a pure, side-effect-free pricing capability designed for maximum reusability across e-commerce, SaaS billing, booking engines, B2B quoting and subscription systems.\n\nIt deliberately separates pricing rules from evaluation. The caller supplies a complete, versioned pricing configuration (currency, rounding mode, discount rules, tax rules) at invocation time. The same WASM binary can therefore enforce completely different commercial models without recompilation.\n\nKey design principles (Persona-Council reviewed):\n- Pure arithmetic + rule evaluation only. No external tax or catalog services.\n- Fully deterministic: identical inputs always produce identical outputs.\n- Fail-closed on invalid quantities, negative prices or malformed configuration.\n- Explicit ordering and stacking behaviour for discounts.\n- Tax calculated on the post-discount taxable base (configurable).\n- Full audit trail: every applied rule, intermediate amounts and a config hash.\n- Multi-currency aware with explicit rounding mode.\n\nThe capability never mutates state and never contacts external systems. All decision logic lives inside the supplied configuration + the deterministic evaluator.\n\nKnown intentional limitations (disclosed):\n- Does not fetch live FX rates or remote tax jurisdiction data \u2013 those must be resolved by the caller and passed in the configuration or context.\n- Complex percentage-of-percentage discount interactions are limited to the ordered stack defined in the config to keep behaviour predictable.\n- Not a full CPQ (Configure-Price-Quote) engine; it assumes line items are already selected.", + "description": "Pure pricing capability: line quantity × unit_price, optional first matching percentage/fixed discount, optional exclusive tax, deterministic config_hash.\n\nImplemented and smoke-tested matrix (use_cases):\n- percentage discount + exclusive tax (ok)\n- fixed discount without tax (ok)\n- empty discount/tax rules returns base price (ok)\n- invalid_quantity when quantity <= 0\n- invalid_unit_price when unit_price < 0\n- invalid_config when currency is empty\n- currency_mismatch when top-level currency ≠ pricing_config.currency\n- empty_cart when lines is empty\n\nKnown limitations (intentional in 1.0.1):\n- Only the first matching discount rule is applied (no multi-rule stacking)\n- Discount rule `conditions` (e.g. customer segment) are ignored\n- Rounding modes other than half_up are not implemented; schema narrowed to half_up\n- Tax-inclusive extraction is not implemented (exclusive tax only)\n- calculation_error is not part of the published reason_code vocabulary", "use_cases": [ { - "scenario": "As a checkout-flow developer, I want a simple cart with one item and a percentage discount to produce a clear line-level and total breakdown so that the UI can show the customer exactly what they pay.", + "scenario": "As a checkout-flow developer, I want percentage discount + exclusive tax so the UI can show a clear total.", "input_example": { "currency": "USD", "lines": [ @@ -101,13 +101,9 @@ } ], "evaluation_trace": [ - "currency=USD rounding=half_up", - "line-1: 2 \u00d7 50.00 = 100.00", - "applied discount summer-10 (10%) \u2192 \u221210.00", - "taxable base 90.00 \u00d7 0.08 = 7.20", - "net 97.20" + "currency=USD rounding=half_up" ], - "config_hash": "sha256:a1b2...", + "config_hash": "fnv1a64:placeholder", "reason_code": "ok", "confidence": "high" }, @@ -115,28 +111,24 @@ "persona_ref": "runtime-engineer" }, { - "scenario": "As a SaaS billing engineer, I want a subscription line with a fixed amount discount and tax-inclusive pricing to be calculated correctly so that invoices match the contract.", + "scenario": "As a billing engineer, I want a fixed discount without tax to reduce the line net deterministically.", "input_example": { - "currency": "EUR", + "currency": "USD", "lines": [ { "id": "sub-1", "sku": "PLAN-PRO", "quantity": 1, - "unit_price": 99.0, - "tax_code": "DIGITAL" + "unit_price": 99.0 } ], "customer": { - "id": "cust-99", - "attributes": { - "country": "DE" - } + "id": "cust-99" }, "context": {}, "pricing_config": { "version": "1.0", - "currency": "EUR", + "currency": "USD", "rounding": "half_up", "decimal_places": 2, "discount_rules": [ @@ -148,18 +140,11 @@ "stackable": true } ], - "tax_rules": [ - { - "id": "de-digital", - "tax_code": "DIGITAL", - "rate": 0.19, - "inclusive": true - } - ] + "tax_rules": [] } }, "output_example": { - "currency": "EUR", + "currency": "USD", "lines": [ { "id": "sub-1", @@ -168,22 +153,20 @@ "unit_price": 99.0, "gross": 99.0, "discount_amount": 5.0, - "taxable_base": 78.99, - "tax_amount": 15.01, + "taxable_base": 94.0, + "tax_amount": 0, "net": 94.0, "applied_discounts": [ "loyalty-5" ], - "applied_taxes": [ - "de-digital" - ] + "applied_taxes": [] } ], "totals": { "gross": 99.0, "discount_total": 5.0, - "taxable_base": 78.99, - "tax_total": 15.01, + "taxable_base": 94.0, + "tax_total": 0, "net": 94.0 }, "applied_rules": [ @@ -191,18 +174,12 @@ "rule_id": "loyalty-5", "type": "discount", "amount": 5.0 - }, - { - "rule_id": "de-digital", - "type": "tax", - "amount": 15.01 } ], "evaluation_trace": [ - "tax-inclusive mode: net after discount = 94.00", - "extracted tax from inclusive price" + "currency=USD rounding=half_up" ], - "config_hash": "sha256:c3d4...", + "config_hash": "fnv1a64:placeholder", "reason_code": "ok", "confidence": "high" }, @@ -210,14 +187,14 @@ "persona_ref": "runtime-engineer" }, { - "scenario": "As a checkout-flow developer, I want an empty cart or zero-quantity line to be rejected with a clear reason code so that the UI can show a precise error instead of a zero total.", + "scenario": "As a client developer, I want empty discount/tax rules to return the base price with reason_code ok.", "input_example": { "currency": "USD", "lines": [ { "id": "line-1", "sku": "SKU-100", - "quantity": 0, + "quantity": 1, "unit_price": 50.0 } ], @@ -236,40 +213,53 @@ }, "output_example": { "currency": "USD", - "lines": [], + "lines": [ + { + "id": "line-1", + "sku": "SKU-100", + "quantity": 1, + "unit_price": 50.0, + "gross": 50.0, + "discount_amount": 0, + "taxable_base": 50.0, + "tax_amount": 0, + "net": 50.0, + "applied_discounts": [], + "applied_taxes": [] + } + ], "totals": { - "gross": 0, + "gross": 50.0, "discount_total": 0, - "taxable_base": 0, + "taxable_base": 50.0, "tax_total": 0, - "net": 0 + "net": 50.0 }, "applied_rules": [], "evaluation_trace": [ - "precondition failed: quantity must be > 0 for line-1" + "currency=USD rounding=half_up" ], - "config_hash": null, - "reason_code": "invalid_quantity", + "config_hash": "fnv1a64:placeholder", + "reason_code": "ok", "confidence": "high" }, - "happy": false, - "persona_ref": "runtime-engineer" + "happy": true, + "persona_ref": "meeting-organizer" }, { - "scenario": "As a pricing analyst, I want two stackable percentage discounts to be applied in priority order so that the final price is predictable and auditable.", + "scenario": "As a checkout-flow developer, I want quantity <= 0 rejected with invalid_quantity.", "input_example": { "currency": "USD", "lines": [ { "id": "line-1", - "sku": "SKU-200", - "quantity": 1, - "unit_price": 200.0, - "tax_code": "STANDARD" + "sku": "SKU-100", + "quantity": 0, + "unit_price": 50.0 } ], "customer": { - "id": "cust-7" + "id": "cust-1" }, "context": {}, "pricing_config": { @@ -277,93 +267,33 @@ "currency": "USD", "rounding": "half_up", "decimal_places": 2, - "discount_rules": [ - { - "id": "vip-15", - "priority": 200, - "type": "percentage", - "value": 15, - "stackable": true - }, - { - "id": "coupon-5", - "priority": 100, - "type": "percentage", - "value": 5, - "stackable": true - } - ], - "tax_rules": [ - { - "id": "us-standard", - "tax_code": "STANDARD", - "rate": 0.08, - "inclusive": false - } - ] + "discount_rules": [], + "tax_rules": [] } }, "output_example": { "currency": "USD", - "lines": [ - { - "id": "line-1", - "sku": "SKU-200", - "quantity": 1, - "unit_price": 200.0, - "gross": 200.0, - "discount_amount": 38.5, - "taxable_base": 161.5, - "tax_amount": 12.92, - "net": 174.42, - "applied_discounts": [ - "vip-15", - "coupon-5" - ], - "applied_taxes": [ - "us-standard" - ] - } - ], + "lines": [], "totals": { - "gross": 200.0, - "discount_total": 38.5, - "taxable_base": 161.5, - "tax_total": 12.92, - "net": 174.42 + "gross": 0, + "discount_total": 0, + "taxable_base": 0, + "tax_total": 0, + "net": 0 }, - "applied_rules": [ - { - "rule_id": "vip-15", - "type": "discount", - "amount": 30.0 - }, - { - "rule_id": "coupon-5", - "type": "discount", - "amount": 8.5 - }, - { - "rule_id": "us-standard", - "type": "tax", - "amount": 12.92 - } - ], + "applied_rules": [], "evaluation_trace": [ - "sorted discounts by priority desc", - "vip-15 (15%) on 200.00 \u2192 \u221230.00 \u2192 170.00", - "coupon-5 (5%) on 170.00 \u2192 \u22128.50 \u2192 161.50", - "tax 8% on 161.50 \u2192 12.92" + "precondition failed: quantity must be > 0 for line-1" ], - "config_hash": "sha256:e5f6...", - "reason_code": "ok", + "config_hash": null, + "reason_code": "invalid_quantity", "confidence": "high" }, - "happy": true, - "persona_ref": "collaboration-product-owner" + "happy": false, + "persona_ref": "runtime-engineer" }, { - "scenario": "As a platform engineer, I want a negative unit price or malformed configuration to be rejected so that the system cannot produce nonsensical quotes.", + "scenario": "As a platform engineer, I want a negative unit_price rejected with invalid_unit_price.", "input_example": { "currency": "USD", "lines": [ @@ -409,89 +339,55 @@ "persona_ref": "runtime-engineer" }, { - "scenario": "As a compliance officer, I want every quote to carry a config_hash and full evaluation_trace so that the exact pricing rules used can be proven later.", + "scenario": "As a runtime, I want empty currency rejected with invalid_config.", "input_example": { - "currency": "USD", + "currency": "", "lines": [ { "id": "line-1", "sku": "SKU-100", "quantity": 1, - "unit_price": 100.0, - "tax_code": "STANDARD" + "unit_price": 50.0 } ], "customer": { "id": "cust-1" }, - "context": { - "quote_id": "Q-2026-001" - }, + "context": {}, "pricing_config": { - "version": "2026.08.01", + "version": "1.0", "currency": "USD", "rounding": "half_up", "decimal_places": 2, "discount_rules": [], - "tax_rules": [ - { - "id": "us-standard", - "tax_code": "STANDARD", - "rate": 0.08, - "inclusive": false - } - ] + "tax_rules": [] } }, "output_example": { "currency": "USD", - "lines": [ - { - "id": "line-1", - "sku": "SKU-100", - "quantity": 1, - "unit_price": 100.0, - "gross": 100.0, - "discount_amount": 0, - "taxable_base": 100.0, - "tax_amount": 8.0, - "net": 108.0, - "applied_discounts": [], - "applied_taxes": [ - "us-standard" - ] - } - ], + "lines": [], "totals": { - "gross": 100.0, + "gross": 0, "discount_total": 0, - "taxable_base": 100.0, - "tax_total": 8.0, - "net": 108.0 + "taxable_base": 0, + "tax_total": 0, + "net": 0 }, - "applied_rules": [ - { - "rule_id": "us-standard", - "type": "tax", - "amount": 8.0 - } - ], + "applied_rules": [], "evaluation_trace": [ - "config version=2026.08.01", - "no discounts applied", - "tax 8% \u2192 8.00" + "precondition failed: currency missing" ], - "config_hash": "sha256:9a8b...", - "reason_code": "ok", + "config_hash": null, + "reason_code": "invalid_config", "confidence": "high" }, - "happy": true, - "persona_ref": "collaboration-product-owner" + "happy": false, + "persona_ref": "runtime-engineer" }, { - "scenario": "As a client developer, I want a missing pricing_config or empty ruleset with no default behaviour to be rejected so that the system never silently produces a zero or incorrect price.", + "scenario": "As a multi-currency engineer, I want currency_mismatch when request currency differs from pricing_config.currency.", "input_example": { - "currency": "USD", + "currency": "EUR", "lines": [ { "id": "line-1", @@ -514,57 +410,33 @@ } }, "output_example": { - "currency": "USD", - "lines": [ - { - "id": "line-1", - "sku": "SKU-100", - "quantity": 1, - "unit_price": 50.0, - "gross": 50.0, - "discount_amount": 0, - "taxable_base": 50.0, - "tax_amount": 0, - "net": 50.0, - "applied_discounts": [], - "applied_taxes": [] - } - ], + "currency": "EUR", + "lines": [], "totals": { - "gross": 50.0, + "gross": 0, "discount_total": 0, - "taxable_base": 50.0, + "taxable_base": 0, "tax_total": 0, - "net": 50.0 + "net": 0 }, "applied_rules": [], "evaluation_trace": [ - "no discount or tax rules matched \u2013 returning base price" + "precondition failed: currency mismatch" ], - "config_hash": "sha256:empty-rules", - "reason_code": "ok", + "config_hash": null, + "reason_code": "currency_mismatch", "confidence": "high" }, - "happy": true, - "persona_ref": "meeting-organizer" + "happy": false, + "persona_ref": "runtime-engineer" }, { - "scenario": "As a multi-tenant platform engineer, I want the capability to respect a customer segment attribute when selecting which discount rules fire so that different tenants or segments can have different commercial terms under the same binary.", + "scenario": "As a checkout-flow developer, I want an empty cart rejected with empty_cart.", "input_example": { "currency": "USD", - "lines": [ - { - "id": "line-1", - "sku": "SKU-100", - "quantity": 1, - "unit_price": 100.0 - } - ], + "lines": [], "customer": { - "id": "cust-55", - "attributes": { - "segment": "enterprise" - } + "id": "cust-1" }, "context": {}, "pricing_config": { @@ -572,73 +444,29 @@ "currency": "USD", "rounding": "half_up", "decimal_places": 2, - "discount_rules": [ - { - "id": "enterprise-20", - "priority": 100, - "type": "percentage", - "value": 20, - "stackable": false, - "conditions": { - "customer.attributes.segment": "enterprise" - } - }, - { - "id": "retail-5", - "priority": 50, - "type": "percentage", - "value": 5, - "stackable": false, - "conditions": { - "customer.attributes.segment": "retail" - } - } - ], + "discount_rules": [], "tax_rules": [] } }, "output_example": { "currency": "USD", - "lines": [ - { - "id": "line-1", - "sku": "SKU-100", - "quantity": 1, - "unit_price": 100.0, - "gross": 100.0, - "discount_amount": 20.0, - "taxable_base": 80.0, - "tax_amount": 0, - "net": 80.0, - "applied_discounts": [ - "enterprise-20" - ], - "applied_taxes": [] - } - ], + "lines": [], "totals": { - "gross": 100.0, - "discount_total": 20.0, - "taxable_base": 80.0, + "gross": 0, + "discount_total": 0, + "taxable_base": 0, "tax_total": 0, - "net": 80.0 + "net": 0 }, - "applied_rules": [ - { - "rule_id": "enterprise-20", - "type": "discount", - "amount": 20.0 - } - ], + "applied_rules": [], "evaluation_trace": [ - "matched condition customer.attributes.segment == enterprise", - "applied enterprise-20 (20%)" + "precondition failed: lines must be non-empty" ], - "config_hash": "sha256:f7g8...", - "reason_code": "ok", + "config_hash": null, + "reason_code": "empty_cart", "confidence": "high" }, - "happy": true, + "happy": false, "persona_ref": "runtime-engineer" } ], @@ -653,13 +481,10 @@ "properties": { "currency": { "type": "string", - "minLength": 3, - "maxLength": 3, "description": "ISO 4217 currency code" }, "lines": { "type": "array", - "minItems": 1, "items": { "type": "object", "required": [ @@ -732,11 +557,7 @@ "rounding": { "type": "string", "enum": [ - "half_up", - "half_down", - "half_even", - "floor", - "ceiling" + "half_up" ] }, "decimal_places": { @@ -975,8 +796,7 @@ "invalid_unit_price", "invalid_config", "currency_mismatch", - "empty_cart", - "calculation_error" + "empty_cart" ] }, "confidence": { @@ -1074,7 +894,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T05:50:00Z", - "spec_ref": "core.calculate-price@1.0.0", + "spec_ref": "core.calculate-price@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -1082,7 +902,7 @@ }, "evidence": [ { - "evidence_id": "core-calculate-price-1.0.0-contract-validation", + "evidence_id": "core-calculate-price-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-calculate-price/manifest.json b/examples/core-calculate-price/manifest.json index e15c129e..89b2ee19 100644 --- a/examples/core-calculate-price/manifest.json +++ b/examples/core-calculate-price/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.calculate-price-agent", - "version": "1.0.0", - "summary": "Capability package for core.calculate-price@1.0.0.", + "version": "1.0.1", + "summary": "Capability package for core.calculate-price@1.0.1.", "capability_ref": { "id": "core.calculate-price", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.calculate-price", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-calculate-price/runtime-requests/uc01-percentage-discount-tax.json b/examples/core-calculate-price/runtime-requests/uc01-percentage-discount-tax.json index 871b7adc..20b08124 100644 --- a/examples/core-calculate-price/runtime-requests/uc01-percentage-discount-tax.json +++ b/examples/core-calculate-price/runtime-requests/uc01-percentage-discount-tax.json @@ -4,7 +4,7 @@ "request_id": "core-calculate-price-01", "intent": { "capability_id": "core.calculate-price", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "currency": "USD", @@ -13,7 +13,7 @@ "id": "line-1", "sku": "SKU-100", "quantity": 2, - "unit_price": 50.00, + "unit_price": 50.0, "tax_code": "STANDARD" } ], diff --git a/examples/core-calculate-price/runtime-requests/uc02-fixed-discount.json b/examples/core-calculate-price/runtime-requests/uc02-fixed-discount.json new file mode 100644 index 00000000..0e82883b --- /dev/null +++ b/examples/core-calculate-price/runtime-requests/uc02-fixed-discount.json @@ -0,0 +1,49 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-calculate-price-02", + "intent": { + "capability_id": "core.calculate-price", + "capability_version": "1.0.1" + }, + "input": { + "currency": "USD", + "lines": [ + { + "id": "sub-1", + "sku": "PLAN-PRO", + "quantity": 1, + "unit_price": 99.0 + } + ], + "customer": { + "id": "cust-99" + }, + "context": {}, + "pricing_config": { + "version": "1.0", + "currency": "USD", + "rounding": "half_up", + "decimal_places": 2, + "discount_rules": [ + { + "id": "loyalty-5", + "priority": 50, + "type": "fixed", + "value": 5.0, + "stackable": true + } + ], + "tax_rules": [] + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-calculate-price-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-calculate-price/runtime-requests/uc03-empty-rules.json b/examples/core-calculate-price/runtime-requests/uc03-empty-rules.json new file mode 100644 index 00000000..34fd21f8 --- /dev/null +++ b/examples/core-calculate-price/runtime-requests/uc03-empty-rules.json @@ -0,0 +1,41 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-calculate-price-03", + "intent": { + "capability_id": "core.calculate-price", + "capability_version": "1.0.1" + }, + "input": { + "currency": "USD", + "lines": [ + { + "id": "line-1", + "sku": "SKU-100", + "quantity": 1, + "unit_price": 50.0 + } + ], + "customer": { + "id": "cust-1" + }, + "context": {}, + "pricing_config": { + "version": "1.0", + "currency": "USD", + "rounding": "half_up", + "decimal_places": 2, + "discount_rules": [], + "tax_rules": [] + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-calculate-price-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-calculate-price/runtime-requests/uc02-invalid-quantity.json b/examples/core-calculate-price/runtime-requests/uc04-invalid-quantity.json similarity index 87% rename from examples/core-calculate-price/runtime-requests/uc02-invalid-quantity.json rename to examples/core-calculate-price/runtime-requests/uc04-invalid-quantity.json index 0ac96d0f..db4f86eb 100644 --- a/examples/core-calculate-price/runtime-requests/uc02-invalid-quantity.json +++ b/examples/core-calculate-price/runtime-requests/uc04-invalid-quantity.json @@ -1,10 +1,10 @@ { "kind": "runtime_request", "schema_version": "1.0.0", - "request_id": "core-calculate-price-02", + "request_id": "core-calculate-price-04", "intent": { "capability_id": "core.calculate-price", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "currency": "USD", @@ -13,7 +13,7 @@ "id": "line-1", "sku": "SKU-100", "quantity": 0, - "unit_price": 50.00 + "unit_price": 50.0 } ], "customer": { diff --git a/examples/core-calculate-price/runtime-requests/uc05-invalid-unit-price.json b/examples/core-calculate-price/runtime-requests/uc05-invalid-unit-price.json new file mode 100644 index 00000000..976fa258 --- /dev/null +++ b/examples/core-calculate-price/runtime-requests/uc05-invalid-unit-price.json @@ -0,0 +1,41 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-calculate-price-05", + "intent": { + "capability_id": "core.calculate-price", + "capability_version": "1.0.1" + }, + "input": { + "currency": "USD", + "lines": [ + { + "id": "line-1", + "sku": "SKU-X", + "quantity": 1, + "unit_price": -10.0 + } + ], + "customer": { + "id": "cust-1" + }, + "context": {}, + "pricing_config": { + "version": "1.0", + "currency": "USD", + "rounding": "half_up", + "decimal_places": 2, + "discount_rules": [], + "tax_rules": [] + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-calculate-price-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-calculate-price/runtime-requests/uc06-invalid-config.json b/examples/core-calculate-price/runtime-requests/uc06-invalid-config.json new file mode 100644 index 00000000..1d66f463 --- /dev/null +++ b/examples/core-calculate-price/runtime-requests/uc06-invalid-config.json @@ -0,0 +1,41 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-calculate-price-06", + "intent": { + "capability_id": "core.calculate-price", + "capability_version": "1.0.1" + }, + "input": { + "currency": "", + "lines": [ + { + "id": "line-1", + "sku": "SKU-100", + "quantity": 1, + "unit_price": 50.0 + } + ], + "customer": { + "id": "cust-1" + }, + "context": {}, + "pricing_config": { + "version": "1.0", + "currency": "USD", + "rounding": "half_up", + "decimal_places": 2, + "discount_rules": [], + "tax_rules": [] + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-calculate-price-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-calculate-price/runtime-requests/uc07-currency-mismatch.json b/examples/core-calculate-price/runtime-requests/uc07-currency-mismatch.json new file mode 100644 index 00000000..c5cd0926 --- /dev/null +++ b/examples/core-calculate-price/runtime-requests/uc07-currency-mismatch.json @@ -0,0 +1,41 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-calculate-price-07", + "intent": { + "capability_id": "core.calculate-price", + "capability_version": "1.0.1" + }, + "input": { + "currency": "EUR", + "lines": [ + { + "id": "line-1", + "sku": "SKU-100", + "quantity": 1, + "unit_price": 50.0 + } + ], + "customer": { + "id": "cust-1" + }, + "context": {}, + "pricing_config": { + "version": "1.0", + "currency": "USD", + "rounding": "half_up", + "decimal_places": 2, + "discount_rules": [], + "tax_rules": [] + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-calculate-price-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-calculate-price/runtime-requests/uc08-empty-cart.json b/examples/core-calculate-price/runtime-requests/uc08-empty-cart.json new file mode 100644 index 00000000..50aca085 --- /dev/null +++ b/examples/core-calculate-price/runtime-requests/uc08-empty-cart.json @@ -0,0 +1,34 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-calculate-price-08", + "intent": { + "capability_id": "core.calculate-price", + "capability_version": "1.0.1" + }, + "input": { + "currency": "USD", + "lines": [], + "customer": { + "id": "cust-1" + }, + "context": {}, + "pricing_config": { + "version": "1.0", + "currency": "USD", + "rounding": "half_up", + "decimal_places": 2, + "discount_rules": [], + "tax_rules": [] + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-calculate-price-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-calculate-price/workflows/calculate-price/workflow.json b/examples/core-calculate-price/workflows/calculate-price/workflow.json index 6b769270..6c0d8f26 100644 --- a/examples/core-calculate-price/workflows/calculate-price/workflow.json +++ b/examples/core-calculate-price/workflows/calculate-price/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.calculate-price", "name": "calculate-price", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -38,7 +38,7 @@ { "node_id": "run", "capability_id": "core.calculate-price", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [ "currency", diff --git a/examples/core-decide-escalation/contract.json b/examples/core-decide-escalation/contract.json index 789b8dc2..78f9fe0c 100644 --- a/examples/core-decide-escalation/contract.json +++ b/examples/core-decide-escalation/contract.json @@ -4,14 +4,14 @@ "id": "core.decide-escalation", "namespace": "core", "name": "decide-escalation", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Decides escalate vs weekly digest from team action health, with a deliberately high bar.", - "description": "Pure WF5 capability. Consumes a health snapshot (from core.aggregate-team-action-health) and applies multi-signal thresholds so managers only get real-time exceptions; otherwise digest.", + "description": "Pure WF5 capability. Consumes a health snapshot (from core.aggregate-team-action-health) and applies multi-signal thresholds to decide digest vs escalate.\n\nImplemented and smoke-tested matrix (use_cases):\n- digest when only one signal is met\n- escalate when multiple signals are met\n- invalid_input when overdue_count is negative\n\nKnown limitations (intentional in 1.0.1):\n- reason_code config_error removed; the guest does not emit that code\n- Decision only; no notification delivery", "use_cases": [ { "scenario": "As a manager experience, I want a single overdue item to stay in digest when other signals are calm.", @@ -107,6 +107,34 @@ }, "happy": true, "persona_ref": "runtime-engineer" + }, + { + "scenario": "As WF5, I want invalid_input when overdue_count is negative.", + "input_example": { + "health": { + "total_open": 3, + "overdue_count": -1, + "on_track_pct": 66.6, + "overloaded_owners": [], + "top_pressure_items": [] + }, + "escalation_config": { + "version": "1.0", + "min_overdue_for_escalate": 2, + "min_overloaded_owners": 1, + "require_multiple_signals": true + } + }, + "output_example": { + "decision": "digest", + "signals_met": 0, + "signals": [], + "escalate_item_ids": [], + "reason_code": "invalid_input", + "evaluation_trace": [] + }, + "happy": false, + "persona_ref": "runtime-engineer" } ], "inputs": { @@ -204,8 +232,7 @@ "type": "string", "enum": [ "ok", - "invalid_input", - "config_error" + "invalid_input" ] }, "evaluation_trace": { @@ -260,7 +287,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.decide-escalation@1.0.0", + "spec_ref": "core.decide-escalation@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -268,7 +295,7 @@ }, "evidence": [ { - "evidence_id": "core-decide-escalation-1.0.0-contract-validation", + "evidence_id": "core-decide-escalation-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-decide-escalation/manifest.json b/examples/core-decide-escalation/manifest.json index 7ea961e3..427d79c0 100644 --- a/examples/core-decide-escalation/manifest.json +++ b/examples/core-decide-escalation/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.decide-escalation-agent", - "version": "1.0.0", - "summary": "Capability package for core.decide-escalation@1.0.0.", + "version": "1.0.1", + "summary": "Capability package for core.decide-escalation@1.0.1.", "capability_ref": { "id": "core.decide-escalation", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.decide-escalation", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-decide-escalation/runtime-requests/uc01-digest.json b/examples/core-decide-escalation/runtime-requests/uc01-digest.json index 3b012c97..a4835c76 100644 --- a/examples/core-decide-escalation/runtime-requests/uc01-digest.json +++ b/examples/core-decide-escalation/runtime-requests/uc01-digest.json @@ -4,7 +4,7 @@ "request_id": "core-decide-escalation-01", "intent": { "capability_id": "core.decide-escalation", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "health": { diff --git a/examples/core-decide-escalation/runtime-requests/uc02-escalate.json b/examples/core-decide-escalation/runtime-requests/uc02-escalate.json index 9af4f1ce..aa455849 100644 --- a/examples/core-decide-escalation/runtime-requests/uc02-escalate.json +++ b/examples/core-decide-escalation/runtime-requests/uc02-escalate.json @@ -4,7 +4,7 @@ "request_id": "core-decide-escalation-02", "intent": { "capability_id": "core.decide-escalation", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "health": { diff --git a/examples/core-decide-escalation/runtime-requests/uc03-invalid-input.json b/examples/core-decide-escalation/runtime-requests/uc03-invalid-input.json new file mode 100644 index 00000000..2ae040da --- /dev/null +++ b/examples/core-decide-escalation/runtime-requests/uc03-invalid-input.json @@ -0,0 +1,33 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-decide-escalation-03", + "intent": { + "capability_id": "core.decide-escalation", + "capability_version": "1.0.1" + }, + "input": { + "health": { + "total_open": 3, + "overdue_count": -1, + "on_track_pct": 66.6, + "overloaded_owners": [], + "top_pressure_items": [] + }, + "escalation_config": { + "version": "1.0", + "min_overdue_for_escalate": 2, + "min_overloaded_owners": 1, + "require_multiple_signals": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-decide-escalation-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-decide-escalation/workflows/decide-escalation/workflow.json b/examples/core-decide-escalation/workflows/decide-escalation/workflow.json index 9a304383..7c40b79a 100644 --- a/examples/core-decide-escalation/workflows/decide-escalation/workflow.json +++ b/examples/core-decide-escalation/workflows/decide-escalation/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.decide-escalation", "name": "decide-escalation", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -26,7 +26,7 @@ { "node_id": "run", "capability_id": "core.decide-escalation", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [] }, diff --git a/examples/core-evaluate-completion-quality/contract.json b/examples/core-evaluate-completion-quality/contract.json index 550fa834..a0c91e8b 100644 --- a/examples/core-evaluate-completion-quality/contract.json +++ b/examples/core-evaluate-completion-quality/contract.json @@ -4,14 +4,14 @@ "id": "core.evaluate-completion-quality", "namespace": "core", "name": "evaluate-completion-quality", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Scores completion quality for a done action item, with a stronger evidence bar under high pressure.", - "description": "Pure WF4 capability. Evaluates completion_note length and evidence_refs against pressure_score and config. High-pressure items require evidence; thin completions get needs_evidence or fail.", + "description": "Pure WF4 capability. Evaluates completion_note length and evidence_refs against pressure_score and config. High-pressure completions without evidence return needs_evidence.\n\nImplemented and smoke-tested matrix (use_cases):\n- pass with evidence under high pressure\n- needs_evidence verdict when evidence is missing under high pressure\n- invalid_status when status is not done/completed\n- invalid_input when item.id is empty\n\nKnown limitations (intentional in 1.0.1):\n- reason_code config_error removed; the guest does not emit that code\n- Evidence quality is presence-only; content is not validated", "use_cases": [ { "scenario": "As the completion path, I want a high-pressure item with evidence to pass.", @@ -83,6 +83,64 @@ }, "happy": true, "persona_ref": "runtime-engineer" + }, + { + "scenario": "As WF4, I want invalid_status when the item is not marked done/completed.", + "input_example": { + "item": { + "id": "ai-1", + "title": "X", + "status": "open", + "pressure_score": 0.5, + "completion_note": "still working", + "evidence_refs": [] + }, + "quality_config": { + "version": "1.0", + "high_pressure_threshold": 0.7, + "require_evidence_when_high_pressure": true, + "min_note_length": 8 + } + }, + "output_example": { + "item_id": "ai-1", + "quality_score": 0, + "verdict": "fail", + "gaps": [], + "reason_code": "invalid_status", + "evaluation_trace": [] + }, + "happy": false, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As WF4, I want invalid_input when item.id is empty.", + "input_example": { + "item": { + "id": "", + "title": "X", + "status": "done", + "pressure_score": 0.5, + "completion_note": "done enough", + "evidence_refs": [] + }, + "quality_config": { + "version": "1.0", + "high_pressure_threshold": 0.7, + "require_evidence_when_high_pressure": true, + "min_note_length": 8 + } + }, + "output_example": { + "item_id": "", + "quality_score": 0, + "verdict": "fail", + "gaps": [], + "reason_code": "invalid_input", + "evaluation_trace": [] + }, + "happy": false, + "persona_ref": "runtime-engineer" } ], "inputs": { @@ -180,8 +238,7 @@ "enum": [ "ok", "invalid_status", - "invalid_input", - "config_error" + "invalid_input" ] }, "evaluation_trace": { @@ -236,7 +293,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.evaluate-completion-quality@1.0.0", + "spec_ref": "core.evaluate-completion-quality@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -244,7 +301,7 @@ }, "evidence": [ { - "evidence_id": "core-evaluate-completion-quality-1.0.0-contract-validation", + "evidence_id": "core-evaluate-completion-quality-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-evaluate-completion-quality/manifest.json b/examples/core-evaluate-completion-quality/manifest.json index f53a0db9..17cdb258 100644 --- a/examples/core-evaluate-completion-quality/manifest.json +++ b/examples/core-evaluate-completion-quality/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.evaluate-completion-quality-agent", - "version": "1.0.0", - "summary": "Capability package for core.evaluate-completion-quality@1.0.0.", + "version": "1.0.1", + "summary": "Capability package for core.evaluate-completion-quality@1.0.1.", "capability_ref": { "id": "core.evaluate-completion-quality", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.evaluate-completion-quality", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-evaluate-completion-quality/runtime-requests/uc01-pass-with-evidence.json b/examples/core-evaluate-completion-quality/runtime-requests/uc01-pass-with-evidence.json index 2081599b..d0a2a131 100644 --- a/examples/core-evaluate-completion-quality/runtime-requests/uc01-pass-with-evidence.json +++ b/examples/core-evaluate-completion-quality/runtime-requests/uc01-pass-with-evidence.json @@ -4,7 +4,7 @@ "request_id": "core-evaluate-completion-quality-01", "intent": { "capability_id": "core.evaluate-completion-quality", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "item": { diff --git a/examples/core-evaluate-completion-quality/runtime-requests/uc02-needs-evidence.json b/examples/core-evaluate-completion-quality/runtime-requests/uc02-needs-evidence.json index 57ff49c1..4892fb0e 100644 --- a/examples/core-evaluate-completion-quality/runtime-requests/uc02-needs-evidence.json +++ b/examples/core-evaluate-completion-quality/runtime-requests/uc02-needs-evidence.json @@ -4,7 +4,7 @@ "request_id": "core-evaluate-completion-quality-02", "intent": { "capability_id": "core.evaluate-completion-quality", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "item": { diff --git a/examples/core-evaluate-completion-quality/runtime-requests/uc03-invalid-status.json b/examples/core-evaluate-completion-quality/runtime-requests/uc03-invalid-status.json new file mode 100644 index 00000000..aad835a4 --- /dev/null +++ b/examples/core-evaluate-completion-quality/runtime-requests/uc03-invalid-status.json @@ -0,0 +1,34 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-evaluate-completion-quality-03", + "intent": { + "capability_id": "core.evaluate-completion-quality", + "capability_version": "1.0.1" + }, + "input": { + "item": { + "id": "ai-1", + "title": "X", + "status": "open", + "pressure_score": 0.5, + "completion_note": "still working", + "evidence_refs": [] + }, + "quality_config": { + "version": "1.0", + "high_pressure_threshold": 0.7, + "require_evidence_when_high_pressure": true, + "min_note_length": 8 + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-evaluate-completion-quality-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-evaluate-completion-quality/runtime-requests/uc04-invalid-input.json b/examples/core-evaluate-completion-quality/runtime-requests/uc04-invalid-input.json new file mode 100644 index 00000000..ac798d9d --- /dev/null +++ b/examples/core-evaluate-completion-quality/runtime-requests/uc04-invalid-input.json @@ -0,0 +1,34 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-evaluate-completion-quality-04", + "intent": { + "capability_id": "core.evaluate-completion-quality", + "capability_version": "1.0.1" + }, + "input": { + "item": { + "id": "", + "title": "X", + "status": "done", + "pressure_score": 0.5, + "completion_note": "done enough", + "evidence_refs": [] + }, + "quality_config": { + "version": "1.0", + "high_pressure_threshold": 0.7, + "require_evidence_when_high_pressure": true, + "min_note_length": 8 + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-evaluate-completion-quality-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-evaluate-completion-quality/workflows/evaluate-completion-quality/workflow.json b/examples/core-evaluate-completion-quality/workflows/evaluate-completion-quality/workflow.json index bdee2bb4..de17de05 100644 --- a/examples/core-evaluate-completion-quality/workflows/evaluate-completion-quality/workflow.json +++ b/examples/core-evaluate-completion-quality/workflows/evaluate-completion-quality/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.evaluate-completion-quality", "name": "evaluate-completion-quality", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -26,7 +26,7 @@ { "node_id": "run", "capability_id": "core.evaluate-completion-quality", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [] }, diff --git a/examples/core-extract-action-items/contract.json b/examples/core-extract-action-items/contract.json index caf59923..ef8e39ca 100644 --- a/examples/core-extract-action-items/contract.json +++ b/examples/core-extract-action-items/contract.json @@ -4,14 +4,14 @@ "id": "core.extract-action-items", "namespace": "core", "name": "extract-action-items", - "version": "1.1.0", + "version": "1.1.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Extracts clean action items from meeting notes with strict confidence gating, separate 'needs review' bucket, and improved relative-date handling. Council-revised.", - "description": "Council-improved extraction capability. Key changes from 1.0.0:\n- Higher default confidence threshold\n- Explicit 'needs_human_review' list for borderline items\n- Better rejection of vague or decision-only sentences\n- Clearer relative date resolution anchored to meeting_date\n- Returns source sentence + confidence for every candidate so the UI can show provenance\n\nStill pure and deterministic. No external model calls.", + "description": "Council-improved extraction capability. Key changes from 1.0.0:\n- Higher default confidence threshold\n- Explicit 'needs_human_review' list for borderline items\n- Better rejection of vague or decision-only sentences\n- Clearer relative date resolution anchored to meeting_date\n- Returns source sentence + confidence for every candidate so the UI can show provenance\n\nStill pure and deterministic. No external model calls.\n\nImplemented and smoke-tested matrix (use_cases):\n- mixed extraction with high-confidence items and vague rejection\n- no_action_items_found for discussion-only notes\n\nKnown limitations (intentional in 1.1.1):\n- Deterministic heuristic extractor only; no external model calls", "use_cases": [ { "scenario": "As a team lead, I want only high-confidence action items auto-suggested and borderline ones flagged for my review.", @@ -167,7 +167,11 @@ "type": "array" }, "reason_code": { - "type": "string" + "type": "string", + "enum": [ + "ok", + "no_action_items_found" + ] }, "evaluation_trace": { "type": "array", @@ -237,7 +241,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:20:00Z", - "spec_ref": "core.extract-action-items@1.1.0", + "spec_ref": "core.extract-action-items@1.1.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -245,7 +249,7 @@ }, "evidence": [ { - "evidence_id": "core-extract-action-items-1.1.0-contract-validation", + "evidence_id": "core-extract-action-items-1.1.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-extract-action-items/manifest.json b/examples/core-extract-action-items/manifest.json index 25dcfbae..7923f3be 100644 --- a/examples/core-extract-action-items/manifest.json +++ b/examples/core-extract-action-items/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.extract-action-items-agent", - "version": "1.1.0", - "summary": "Capability package for core.extract-action-items@1.1.0.", + "version": "1.1.1", + "summary": "Capability package for core.extract-action-items@1.1.1.", "capability_ref": { "id": "core.extract-action-items", - "version": "1.1.0", + "version": "1.1.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.extract-action-items", - "workflow_version": "1.1.0" + "workflow_version": "1.1.1" } ], "source": { diff --git a/examples/core-extract-action-items/runtime-requests/uc01-extract-mixed.json b/examples/core-extract-action-items/runtime-requests/uc01-extract-mixed.json index 1b6278fb..13434cb6 100644 --- a/examples/core-extract-action-items/runtime-requests/uc01-extract-mixed.json +++ b/examples/core-extract-action-items/runtime-requests/uc01-extract-mixed.json @@ -4,7 +4,7 @@ "request_id": "core-extract-action-items-01", "intent": { "capability_id": "core.extract-action-items", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "text": "Ada will send the revised proposal by Friday. We should probably look at the API at some point. Bob to review security notes next week.", diff --git a/examples/core-extract-action-items/runtime-requests/uc02-no-actions.json b/examples/core-extract-action-items/runtime-requests/uc02-no-actions.json index da06ab00..5e54c69f 100644 --- a/examples/core-extract-action-items/runtime-requests/uc02-no-actions.json +++ b/examples/core-extract-action-items/runtime-requests/uc02-no-actions.json @@ -4,7 +4,7 @@ "request_id": "core-extract-action-items-02", "intent": { "capability_id": "core.extract-action-items", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "text": "We had a good discussion and aligned on the direction. No specific next steps today.", diff --git a/examples/core-extract-action-items/workflows/extract-action-items/workflow.json b/examples/core-extract-action-items/workflows/extract-action-items/workflow.json index d305488f..df949dbf 100644 --- a/examples/core-extract-action-items/workflows/extract-action-items/workflow.json +++ b/examples/core-extract-action-items/workflows/extract-action-items/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.extract-action-items", "name": "extract-action-items", - "version": "1.1.0", + "version": "1.1.1", "lifecycle": "active", "owner": { "team": "loop", @@ -35,7 +35,7 @@ { "node_id": "run", "capability_id": "core.extract-action-items", - "capability_version": "1.1.0", + "capability_version": "1.1.1", "input": { "from_workflow_input": [ "text", diff --git a/examples/core-generate-nudge-message/contract.json b/examples/core-generate-nudge-message/contract.json index 8e74dce0..392d25fa 100644 --- a/examples/core-generate-nudge-message/contract.json +++ b/examples/core-generate-nudge-message/contract.json @@ -4,14 +4,14 @@ "id": "core.generate-nudge-message", "namespace": "core", "name": "generate-nudge-message", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Generates a short, human, context-aware nudge message for an action item at a given intensity.", - "description": "Pure message generation. Takes item details + intensity + tone configuration and returns a ready-to-send message body plus subject/preview. No delivery occurs inside the capability.", + "description": "Pure message generation. Takes item details + intensity + tone configuration and returns a ready-to-send message body plus subject/preview. No delivery occurs inside the capability.\n\nImplemented and smoke-tested matrix (use_cases):\n- soft + friendly tone reminder\n- escalate + direct tone escalation message\n- direct + neutral tone request\n- config_error when item.title is empty\n\nKnown limitations (intentional in 1.0.1):\n- reason_code invalid_intensity removed from the declared surface; intensity is a schema enum so host validation rejects unknown values before the guest runs\n- Delivery / sending is out of scope; callers must send the returned message", "use_cases": [ { "scenario": "As the follow-up engine, I want a soft reminder that feels helpful rather than nagging.", @@ -33,7 +33,7 @@ } }, "output_example": { - "message": "Hey Ada \u2014 friendly reminder that \u201cSend the revised proposal\u201d is due tomorrow. Let us know if you need anything!", + "message": "Hey Ada — friendly reminder that “Send the revised proposal” is due tomorrow. Let us know if you need anything!", "preview": "Reminder: Send the revised proposal", "reason_code": "ok", "evaluation_trace": [ @@ -43,6 +43,98 @@ }, "happy": true, "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the follow-up engine, I want an escalate-intensity message when an item has been ignored repeatedly.", + "input_example": { + "item": { + "id": "ai-9", + "title": "Close security review", + "owner_name": "Bob", + "due_date": "2026-08-01", + "status": "open", + "nudge_count": 3 + }, + "intensity": "escalate", + "message_config": { + "version": "1.0", + "tone": "direct", + "include_due_date": true, + "language": "en" + } + }, + "output_example": { + "message": "Escalation: \"Close security review\" still needs attention from Bob (due 2026-08-01).", + "preview": "Reminder: Close security review", + "reason_code": "ok", + "evaluation_trace": [ + "intensity=escalate", + "tone=direct" + ] + }, + "happy": true, + "persona_ref": "collaboration-product-owner" + }, + { + "scenario": "As the follow-up engine, I want a direct, neutral reminder when soft nudges are insufficient.", + "input_example": { + "item": { + "id": "ai-2", + "title": "Ship docs", + "owner_name": "Ada", + "due_date": "2026-08-12", + "status": "open", + "nudge_count": 1 + }, + "intensity": "direct", + "message_config": { + "version": "1.0", + "tone": "neutral", + "include_due_date": true, + "language": "en" + } + }, + "output_example": { + "message": "Ada: Please complete \"Ship docs\" by 2026-08-12.", + "preview": "Reminder: Ship docs", + "reason_code": "ok", + "evaluation_trace": [ + "intensity=direct", + "tone=neutral" + ] + }, + "happy": true, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the follow-up engine, I want a config_error when the item title is missing so I can skip generation.", + "input_example": { + "item": { + "id": "ai-x", + "title": "", + "owner_name": "Ada", + "due_date": "2026-08-09", + "status": "open", + "nudge_count": 0 + }, + "intensity": "soft", + "message_config": { + "version": "1.0", + "tone": "friendly", + "include_due_date": true, + "language": "en" + } + }, + "output_example": { + "message": "", + "preview": "", + "reason_code": "config_error", + "evaluation_trace": [ + "item.title required" + ] + }, + "happy": false, + "persona_ref": "runtime-engineer" } ], "inputs": { @@ -111,7 +203,6 @@ "type": "string", "enum": [ "ok", - "invalid_intensity", "config_error" ] }, @@ -167,7 +258,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.generate-nudge-message@1.0.0", + "spec_ref": "core.generate-nudge-message@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -175,7 +266,7 @@ }, "evidence": [ { - "evidence_id": "core-generate-nudge-message-1.0.0-contract-validation", + "evidence_id": "core-generate-nudge-message-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-generate-nudge-message/manifest.json b/examples/core-generate-nudge-message/manifest.json index 9cf6f822..f3c1c8e6 100644 --- a/examples/core-generate-nudge-message/manifest.json +++ b/examples/core-generate-nudge-message/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.generate-nudge-message-agent", - "version": "1.0.0", - "summary": "Capability package for core.generate-nudge-message@1.0.0.", + "version": "1.0.1", + "summary": "Capability package for core.generate-nudge-message@1.0.1.", "capability_ref": { "id": "core.generate-nudge-message", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.generate-nudge-message", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-generate-nudge-message/runtime-requests/uc01-soft-friendly.json b/examples/core-generate-nudge-message/runtime-requests/uc01-soft-friendly.json index f363625a..375d81df 100644 --- a/examples/core-generate-nudge-message/runtime-requests/uc01-soft-friendly.json +++ b/examples/core-generate-nudge-message/runtime-requests/uc01-soft-friendly.json @@ -4,7 +4,7 @@ "request_id": "core-generate-nudge-message-01", "intent": { "capability_id": "core.generate-nudge-message", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "item": { diff --git a/examples/core-generate-nudge-message/runtime-requests/uc02-escalate.json b/examples/core-generate-nudge-message/runtime-requests/uc02-escalate.json index 892534b9..f8ff907a 100644 --- a/examples/core-generate-nudge-message/runtime-requests/uc02-escalate.json +++ b/examples/core-generate-nudge-message/runtime-requests/uc02-escalate.json @@ -4,7 +4,7 @@ "request_id": "core-generate-nudge-message-02", "intent": { "capability_id": "core.generate-nudge-message", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "item": { diff --git a/examples/core-generate-nudge-message/runtime-requests/uc03-direct-neutral.json b/examples/core-generate-nudge-message/runtime-requests/uc03-direct-neutral.json new file mode 100644 index 00000000..ee2cbcfc --- /dev/null +++ b/examples/core-generate-nudge-message/runtime-requests/uc03-direct-neutral.json @@ -0,0 +1,35 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-generate-nudge-message-03", + "intent": { + "capability_id": "core.generate-nudge-message", + "capability_version": "1.0.1" + }, + "input": { + "item": { + "id": "ai-2", + "title": "Ship docs", + "owner_name": "Ada", + "due_date": "2026-08-12", + "status": "open", + "nudge_count": 1 + }, + "intensity": "direct", + "message_config": { + "version": "1.0", + "tone": "neutral", + "include_due_date": true, + "language": "en" + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-generate-nudge-message-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-generate-nudge-message/runtime-requests/uc04-config-error.json b/examples/core-generate-nudge-message/runtime-requests/uc04-config-error.json new file mode 100644 index 00000000..de5e0136 --- /dev/null +++ b/examples/core-generate-nudge-message/runtime-requests/uc04-config-error.json @@ -0,0 +1,35 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-generate-nudge-message-04", + "intent": { + "capability_id": "core.generate-nudge-message", + "capability_version": "1.0.1" + }, + "input": { + "item": { + "id": "ai-x", + "title": "", + "owner_name": "Ada", + "due_date": "2026-08-09", + "status": "open", + "nudge_count": 0 + }, + "intensity": "soft", + "message_config": { + "version": "1.0", + "tone": "friendly", + "include_due_date": true, + "language": "en" + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-generate-nudge-message-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-generate-nudge-message/workflows/generate-nudge-message/workflow.json b/examples/core-generate-nudge-message/workflows/generate-nudge-message/workflow.json index 4ce4de34..f791fddd 100644 --- a/examples/core-generate-nudge-message/workflows/generate-nudge-message/workflow.json +++ b/examples/core-generate-nudge-message/workflows/generate-nudge-message/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.generate-nudge-message", "name": "generate-nudge-message", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -26,7 +26,7 @@ { "node_id": "run", "capability_id": "core.generate-nudge-message", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [ "item", diff --git a/examples/core-normalize-participants/contract.json b/examples/core-normalize-participants/contract.json index 54c67b45..58eb4b2c 100644 --- a/examples/core-normalize-participants/contract.json +++ b/examples/core-normalize-participants/contract.json @@ -4,14 +4,14 @@ "id": "core.normalize-participants", "namespace": "core", "name": "normalize-participants", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Normalizes raw participant names/emails into canonical participant records for action-item ownership.", - "description": "Pure WF1 capability. Lowercases emails, trims whitespace, and matches against workspace members by email then name. Unmatched participants are retained with match_method=none so humans can resolve them.", + "description": "Pure WF1 capability. Lowercases emails, trims whitespace, and matches against workspace members by email then name. Unmatched participants are retained with match_method=none.\n\nImplemented and smoke-tested matrix (use_cases):\n- mixed email match with one unmatched stranger\n- name-only match when email is null\n\nKnown limitations (intentional in 1.0.1):\n- reason_code narrowed to ok; invalid_input/config_error removed because host-required fields make those guest paths unreachable without schema weakening, and config_error is not implemented by the guest", "use_cases": [ { "scenario": "As the ingest pipeline, I want extracted names and emails resolved to workspace members.", @@ -231,9 +231,7 @@ "reason_code": { "type": "string", "enum": [ - "ok", - "invalid_input", - "config_error" + "ok" ] }, "evaluation_trace": { @@ -288,7 +286,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.normalize-participants@1.0.0", + "spec_ref": "core.normalize-participants@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -296,7 +294,7 @@ }, "evidence": [ { - "evidence_id": "core-normalize-participants-1.0.0-contract-validation", + "evidence_id": "core-normalize-participants-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-normalize-participants/manifest.json b/examples/core-normalize-participants/manifest.json index d532d9c7..f4f7d002 100644 --- a/examples/core-normalize-participants/manifest.json +++ b/examples/core-normalize-participants/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.normalize-participants-agent", - "version": "1.0.0", - "summary": "Capability package for core.normalize-participants@1.0.0.", + "version": "1.0.1", + "summary": "Capability package for core.normalize-participants@1.0.1.", "capability_ref": { "id": "core.normalize-participants", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.normalize-participants", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-normalize-participants/runtime-requests/uc01-mixed-match.json b/examples/core-normalize-participants/runtime-requests/uc01-mixed-match.json index 78fa2555..88a29b08 100644 --- a/examples/core-normalize-participants/runtime-requests/uc01-mixed-match.json +++ b/examples/core-normalize-participants/runtime-requests/uc01-mixed-match.json @@ -4,7 +4,7 @@ "request_id": "core-normalize-participants-01", "intent": { "capability_id": "core.normalize-participants", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "raw_participants": [ diff --git a/examples/core-normalize-participants/runtime-requests/uc02-name-match.json b/examples/core-normalize-participants/runtime-requests/uc02-name-match.json index e80a5aa6..1eefad23 100644 --- a/examples/core-normalize-participants/runtime-requests/uc02-name-match.json +++ b/examples/core-normalize-participants/runtime-requests/uc02-name-match.json @@ -4,7 +4,7 @@ "request_id": "core-normalize-participants-02", "intent": { "capability_id": "core.normalize-participants", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "raw_participants": [ diff --git a/examples/core-normalize-participants/workflows/normalize-participants/workflow.json b/examples/core-normalize-participants/workflows/normalize-participants/workflow.json index 05c2336d..6dd86daa 100644 --- a/examples/core-normalize-participants/workflows/normalize-participants/workflow.json +++ b/examples/core-normalize-participants/workflows/normalize-participants/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.normalize-participants", "name": "normalize-participants", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -26,7 +26,7 @@ { "node_id": "run", "capability_id": "core.normalize-participants", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [] }, diff --git a/examples/core-notify-stakeholders/contract.json b/examples/core-notify-stakeholders/contract.json index 48e1295d..b1e9766b 100644 --- a/examples/core-notify-stakeholders/contract.json +++ b/examples/core-notify-stakeholders/contract.json @@ -4,14 +4,14 @@ "id": "core.notify-stakeholders", "namespace": "core", "name": "notify-stakeholders", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, - "summary": "Prepares notification intents for stakeholders when an action item changes status. Pure \u2014 does not send.", - "description": "Pure WF4 capability. Builds recipient/channel/subject/body intents from item + stakeholder list + event_type. Delivery is host-owned; this capability only prepares intents.", + "summary": "Prepares notification intents for stakeholders when an action item changes status. Pure — does not send.", + "description": "Pure WF4 capability. Builds recipient/channel/subject/body intents from item + stakeholder list + event_type. Delivery is caller-owned.\n\nImplemented and smoke-tested matrix (use_cases):\n- completed event with multiple stakeholders\n- nothing_to_notify when stakeholders is empty\n- status_changed event intents\n- blocked event intents\n\nKnown limitations (intentional in 1.0.1):\n- reason_code invalid_event_type and invalid_input removed; event_type is a schema enum and host-required fields make those guest denial paths unreachable without schema weakening\n- No actual notification delivery", "use_cases": [ { "scenario": "As the completion path, I want requester and manager notified that an item is done.", @@ -94,6 +94,90 @@ }, "happy": false, "persona_ref": "runtime-engineer" + }, + { + "scenario": "As WF4, I want status_changed notification intents for stakeholders.", + "input_example": { + "item": { + "id": "ai-1", + "title": "Send the revised proposal", + "owner_id": "user-ada", + "owner_name": "Ada", + "status": "in_progress" + }, + "stakeholders": [ + { + "user_id": "user-carol", + "role": "requester", + "channel": "in_app" + } + ], + "event_type": "status_changed", + "notify_config": { + "version": "1.0", + "include_manager_on_complete": true + } + }, + "output_example": { + "intents": [ + { + "recipient_id": "user-carol", + "channel": "in_app", + "subject": "Action updated: Send the revised proposal", + "body": "Ada updated \"Send the revised proposal\"." + } + ], + "intent_count": 1, + "reason_code": "ok", + "evaluation_trace": [ + "event_type=status_changed", + "prepared 1 intents" + ] + }, + "happy": true, + "persona_ref": "collaboration-product-owner" + }, + { + "scenario": "As WF4, I want blocked notification intents for stakeholders.", + "input_example": { + "item": { + "id": "ai-1", + "title": "Send the revised proposal", + "owner_id": "user-ada", + "owner_name": "Ada", + "status": "blocked" + }, + "stakeholders": [ + { + "user_id": "user-carol", + "role": "requester", + "channel": "in_app" + } + ], + "event_type": "blocked", + "notify_config": { + "version": "1.0", + "include_manager_on_complete": true + } + }, + "output_example": { + "intents": [ + { + "recipient_id": "user-carol", + "channel": "in_app", + "subject": "Action blocked: Send the revised proposal", + "body": "Ada blocked \"Send the revised proposal\"." + } + ], + "intent_count": 1, + "reason_code": "ok", + "evaluation_trace": [ + "event_type=blocked", + "prepared 1 intents" + ] + }, + "happy": true, + "persona_ref": "meeting-organizer" } ], "inputs": { @@ -217,9 +301,7 @@ "type": "string", "enum": [ "ok", - "nothing_to_notify", - "invalid_event_type", - "invalid_input" + "nothing_to_notify" ] }, "evaluation_trace": { @@ -274,7 +356,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.notify-stakeholders@1.0.0", + "spec_ref": "core.notify-stakeholders@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -282,7 +364,7 @@ }, "evidence": [ { - "evidence_id": "core-notify-stakeholders-1.0.0-contract-validation", + "evidence_id": "core-notify-stakeholders-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-notify-stakeholders/manifest.json b/examples/core-notify-stakeholders/manifest.json index 3186f603..2c426859 100644 --- a/examples/core-notify-stakeholders/manifest.json +++ b/examples/core-notify-stakeholders/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.notify-stakeholders-agent", - "version": "1.0.0", - "summary": "Capability package for core.notify-stakeholders@1.0.0.", + "version": "1.0.1", + "summary": "Capability package for core.notify-stakeholders@1.0.1.", "capability_ref": { "id": "core.notify-stakeholders", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.notify-stakeholders", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-notify-stakeholders/runtime-requests/uc01-completed.json b/examples/core-notify-stakeholders/runtime-requests/uc01-completed.json index da8b2b6a..4f248b40 100644 --- a/examples/core-notify-stakeholders/runtime-requests/uc01-completed.json +++ b/examples/core-notify-stakeholders/runtime-requests/uc01-completed.json @@ -4,7 +4,7 @@ "request_id": "core-notify-stakeholders-01", "intent": { "capability_id": "core.notify-stakeholders", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "item": { diff --git a/examples/core-notify-stakeholders/runtime-requests/uc02-empty.json b/examples/core-notify-stakeholders/runtime-requests/uc02-empty.json index b636a891..4675ac36 100644 --- a/examples/core-notify-stakeholders/runtime-requests/uc02-empty.json +++ b/examples/core-notify-stakeholders/runtime-requests/uc02-empty.json @@ -4,7 +4,7 @@ "request_id": "core-notify-stakeholders-02", "intent": { "capability_id": "core.notify-stakeholders", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "item": { diff --git a/examples/core-notify-stakeholders/runtime-requests/uc03-status-changed.json b/examples/core-notify-stakeholders/runtime-requests/uc03-status-changed.json new file mode 100644 index 00000000..0bac6ea6 --- /dev/null +++ b/examples/core-notify-stakeholders/runtime-requests/uc03-status-changed.json @@ -0,0 +1,39 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-notify-stakeholders-03", + "intent": { + "capability_id": "core.notify-stakeholders", + "capability_version": "1.0.1" + }, + "input": { + "item": { + "id": "ai-1", + "title": "Send the revised proposal", + "owner_id": "user-ada", + "owner_name": "Ada", + "status": "in_progress" + }, + "stakeholders": [ + { + "user_id": "user-carol", + "role": "requester", + "channel": "in_app" + } + ], + "event_type": "status_changed", + "notify_config": { + "version": "1.0", + "include_manager_on_complete": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-notify-stakeholders-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-notify-stakeholders/runtime-requests/uc04-blocked.json b/examples/core-notify-stakeholders/runtime-requests/uc04-blocked.json new file mode 100644 index 00000000..ae287608 --- /dev/null +++ b/examples/core-notify-stakeholders/runtime-requests/uc04-blocked.json @@ -0,0 +1,39 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-notify-stakeholders-04", + "intent": { + "capability_id": "core.notify-stakeholders", + "capability_version": "1.0.1" + }, + "input": { + "item": { + "id": "ai-1", + "title": "Send the revised proposal", + "owner_id": "user-ada", + "owner_name": "Ada", + "status": "blocked" + }, + "stakeholders": [ + { + "user_id": "user-carol", + "role": "requester", + "channel": "in_app" + } + ], + "event_type": "blocked", + "notify_config": { + "version": "1.0", + "include_manager_on_complete": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-notify-stakeholders-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-notify-stakeholders/workflows/notify-stakeholders/workflow.json b/examples/core-notify-stakeholders/workflows/notify-stakeholders/workflow.json index 973c6b01..1f4395b0 100644 --- a/examples/core-notify-stakeholders/workflows/notify-stakeholders/workflow.json +++ b/examples/core-notify-stakeholders/workflows/notify-stakeholders/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.notify-stakeholders", "name": "notify-stakeholders", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -26,7 +26,7 @@ { "node_id": "run", "capability_id": "core.notify-stakeholders", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [] }, diff --git a/examples/core-process-comment/contract.json b/examples/core-process-comment/contract.json index c13cc2e0..bc9cd590 100644 --- a/examples/core-process-comment/contract.json +++ b/examples/core-process-comment/contract.json @@ -4,14 +4,14 @@ "id": "core.process-comment", "namespace": "core", "name": "process-comment", - "version": "1.0.1", + "version": "1.0.2", "lifecycle": "active", "owner": { "team": "traverse-core", "contact": "capability-authors@traverse-framework.com" }, "summary": "Policy-driven comment processor for create/edit/reply/delete/react with mentions, quarantine, soft-delete, and tenant isolation.", - "description": "core.process-comment evaluates create, edit, reply, delete, and react actions against a caller-supplied comment_policy and returns allow/deny with a normalized comment object, obligations, and an evaluation trace.\n\nImplemented and smoke-tested matrix (use_cases):\n- create with @mention and https link extraction + notify obligations\n- edit denied when policy own_only and actor is not creator\n- reply denied when parent_depth+1 >= max_thread_depth\n- create allowed with quarantine when body matches moderation.blocklist\n- react add normalized to a reaction set\n- soft-delete retains body and records deleted_by when soft_delete=true\n- create denied on tenant mismatch when enforce_tenant_isolation=true\n- create/edit/reply denied for empty/whitespace body\n\nKnown limitations (intentional in 1.0.1):\n- resolve / unresolve / pin / unpin are not part of the schema surface (removed from action.enum vs overclaiming 1.0.0)\n- allowed_markups is accepted in policy but body markup is not sanitized (pass-through)\n- mention_resolution \"strict\" extracts @ids and marks them valid; it does not validate against an allow-list of principals\n- The capability never persists data or sends notifications; callers must honor obligations\n\nIdentical inputs produce identical outputs. No host, network, or filesystem access.", + "description": "Policy-driven comment processor for create/edit/reply/delete/react with mentions, quarantine, soft-delete, and tenant isolation.\n\nImplemented and smoke-tested matrix (use_cases):\n- create with @mention and https link + notify obligations\n- edit denied when own_only and actor is not creator\n- reply denied when parent_depth+1 >= max_thread_depth\n- create allowed with quarantine when body matches moderation.blocklist\n- react add normalized to a reaction set\n- soft-delete retains body and records deleted_by\n- create denied on tenant mismatch\n- create/edit/reply denied for empty/whitespace body\n- body_too_long when body exceeds max_body_length\n- invalid_reaction when react lacks emoji\n- insufficient_role when actor role not permitted\n\nKnown limitations (intentional in 1.0.2):\n- resolve / unresolve / pin / unpin are not part of the schema surface\n- mention_resolution narrowed to strict; permissive/disabled removed\n- visibility_model narrowed to resource-acl|channel\n- reaction.op narrowed to add (remove not published)\n- invalid_action / invalid_parent / policy_error / not_authorized removed from published reason_code vocabulary\n- hard_delete_unsupported and invalid_input/invalid_actor fail-closed paths exist but are not published reason_codes\n- allowed_markups is accepted but body markup is not sanitized\n- The capability never persists data or sends notifications; callers must honor obligations", "use_cases": [ { "scenario": "As a collaboration platform engineer, I want a user to create a top-level comment with mentions so that the system receives a fully validated, sanitized comment object ready for persistence and notification.", @@ -158,7 +158,7 @@ ], "evaluation_trace": [ "action=create", - "actor roles include member \u2192 create allowed", + "actor roles include member → create allowed", "body length 78 <= 10000", "extracted 2 mentions, both valid under strict resolution", "extracted 1 link", @@ -243,7 +243,7 @@ "evaluation_trace": [ "action=edit", "policy own_only=true", - "comment.created_by=user-42, actor.id=user-99 \u2192 mismatch", + "comment.created_by=user-42, actor.id=user-99 → mismatch", "deny" ], "policy_hash": "sha256:c0mm3nt-2026.08.1", @@ -316,8 +316,8 @@ ], "evaluation_trace": [ "action=reply", - "parent_depth=7 \u2192 proposed depth=8", - "max_thread_depth=8 \u2192 deny" + "parent_depth=7 → proposed depth=8", + "max_thread_depth=8 → deny" ], "policy_hash": "sha256:c0mm3nt-2026.08.1", "confidence": "high" @@ -381,7 +381,7 @@ }, "output_example": { "decision": "allow", - "reason": "Body matched moderation blocklist \u2013 allowed with quarantine obligation", + "reason": "Body matched moderation blocklist – allowed with quarantine obligation", "reason_code": "moderation_quarantine", "normalized_comment": { "id": null, @@ -603,7 +603,7 @@ "evaluation_trace": [ "action=delete", "soft_delete=true in policy", - "actor is creator \u2192 allow", + "actor is creator → allow", "body retained for audit" ], "policy_hash": "sha256:c0mm3nt-2026.08.1", @@ -676,7 +676,7 @@ ], "evaluation_trace": [ "enforce_tenant_isolation=true", - "actor.tenant_id=t-100, resource.tenant_id=t-200 \u2192 deny" + "actor.tenant_id=t-100, resource.tenant_id=t-200 → deny" ], "policy_hash": "sha256:c0mm3nt-2026.08.1", "confidence": "high" @@ -738,13 +738,214 @@ "obligations": [], "evaluation_trace": [ "action=create", - "body trimmed length = 0 \u2192 deny" + "body trimmed length = 0 → deny" ], "policy_hash": "sha256:c0mm3nt-2026.08.1", "confidence": "high" }, "happy": false, "persona_ref": "client-developer" + }, + { + "scenario": "As a client developer, I want a body exceeding max_body_length rejected with body_too_long.", + "input_example": { + "action": "create", + "actor": { + "id": "user-42", + "roles": [ + "member" + ], + "attributes": { + "tenant_id": "t-100" + } + }, + "resource": { + "type": "document", + "id": "doc-99", + "attributes": { + "tenant_id": "t-100" + } + }, + "comment": { + "body": "xxxxxxxxxxxxxxxxxxxxx", + "parent_id": null + }, + "context": {}, + "comment_policy": { + "version": "2026.08.1", + "max_body_length": 20, + "max_thread_depth": 8, + "allowed_markups": [], + "mention_resolution": "strict", + "visibility_model": "resource-acl", + "soft_delete": true, + "actions": { + "create": { + "roles": [ + "member" + ] + } + }, + "moderation": { + "blocklist": [], + "require_approval_roles": [] + } + } + }, + "output_example": { + "decision": "deny", + "reason": "body exceeds max_body_length", + "reason_code": "body_too_long", + "normalized_comment": null, + "obligations": [ + { + "type": "audit_log", + "severity": "required" + } + ], + "evaluation_trace": [ + "body length check failed" + ], + "policy_hash": "sha256:c0mm3nt-2026.08.1", + "confidence": "high" + }, + "happy": false, + "persona_ref": "client-developer" + }, + { + "scenario": "As a client developer, I want a react without emoji rejected with invalid_reaction.", + "input_example": { + "action": "react", + "actor": { + "id": "user-42", + "roles": [ + "member" + ], + "attributes": { + "tenant_id": "t-100" + } + }, + "resource": { + "type": "document", + "id": "doc-99", + "attributes": { + "tenant_id": "t-100" + } + }, + "comment": { + "id": "cmt-55", + "reaction": { + "op": "add" + } + }, + "context": {}, + "comment_policy": { + "version": "2026.08.1", + "max_body_length": 10000, + "max_thread_depth": 8, + "allowed_markups": [], + "mention_resolution": "strict", + "visibility_model": "resource-acl", + "soft_delete": true, + "actions": { + "react": { + "roles": [ + "member" + ] + } + }, + "moderation": { + "blocklist": [], + "require_approval_roles": [] + } + } + }, + "output_example": { + "decision": "deny", + "reason": "reaction.emoji required", + "reason_code": "invalid_reaction", + "normalized_comment": null, + "obligations": [ + { + "type": "audit_log", + "severity": "required" + } + ], + "evaluation_trace": [ + "missing reaction.emoji" + ], + "policy_hash": "sha256:c0mm3nt-2026.08.1", + "confidence": "high" + }, + "happy": false, + "persona_ref": "client-developer" + }, + { + "scenario": "As a security architect, I want create denied with insufficient_role when the actor role is not permitted.", + "input_example": { + "action": "create", + "actor": { + "id": "user-42", + "roles": [ + "guest" + ], + "attributes": { + "tenant_id": "t-100" + } + }, + "resource": { + "type": "document", + "id": "doc-99", + "attributes": { + "tenant_id": "t-100" + } + }, + "comment": { + "body": "Hello", + "parent_id": null + }, + "context": {}, + "comment_policy": { + "version": "2026.08.1", + "max_body_length": 10000, + "max_thread_depth": 8, + "allowed_markups": [], + "mention_resolution": "strict", + "visibility_model": "resource-acl", + "soft_delete": true, + "actions": { + "create": { + "roles": [ + "member", + "admin" + ] + } + }, + "moderation": { + "blocklist": [], + "require_approval_roles": [] + } + } + }, + "output_example": { + "decision": "deny", + "reason": "actor role not permitted for action", + "reason_code": "insufficient_role", + "normalized_comment": null, + "obligations": [ + { + "type": "audit_log", + "severity": "required" + } + ], + "evaluation_trace": [ + "action role check failed" + ], + "policy_hash": "sha256:c0mm3nt-2026.08.1", + "confidence": "high" + }, + "happy": false, + "persona_ref": "security-architect" } ], "inputs": { @@ -844,8 +1045,7 @@ "op": { "type": "string", "enum": [ - "add", - "remove" + "add" ] } } @@ -891,17 +1091,12 @@ "mention_resolution": { "type": "string", "enum": [ - "strict", - "permissive", - "disabled" + "strict" ] }, "visibility_model": { "type": "string", "enum": [ - "public", - "private", - "restricted", "resource-acl", "channel" ] @@ -981,17 +1176,14 @@ "type": "string", "enum": [ "ok", - "not_authorized", + "insufficient_role", "not_owner", "empty_body", "body_too_long", "max_thread_depth_exceeded", - "invalid_parent", "tenant_isolation_violation", "moderation_quarantine", - "invalid_action", - "invalid_reaction", - "policy_error" + "invalid_reaction" ] }, "normalized_comment": { @@ -1158,7 +1350,7 @@ }, "evidence": [ { - "evidence_id": "core-process-comment-1.0.1-contract-validation", + "evidence_id": "core-process-comment-1.0.2-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-process-comment/manifest.json b/examples/core-process-comment/manifest.json index b1914a41..571b3c49 100644 --- a/examples/core-process-comment/manifest.json +++ b/examples/core-process-comment/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.process-comment-agent", - "version": "1.0.1", - "summary": "Capability package for core.process-comment@1.0.1 (honesty-aligned tested matrix).", + "version": "1.0.2", + "summary": "Capability package for core.process-comment@1.0.2.", "capability_ref": { "id": "core.process-comment", - "version": "1.0.1", + "version": "1.0.2", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.process-comment", - "workflow_version": "1.0.1" + "workflow_version": "1.0.2" } ], "source": { diff --git a/examples/core-process-comment/runtime-requests/uc01-create-mentions-allow.json b/examples/core-process-comment/runtime-requests/uc01-create-mentions-allow.json index 621ea1df..65d6fe16 100644 --- a/examples/core-process-comment/runtime-requests/uc01-create-mentions-allow.json +++ b/examples/core-process-comment/runtime-requests/uc01-create-mentions-allow.json @@ -4,7 +4,7 @@ "request_id": "core-process-comment-01", "intent": { "capability_id": "core.process-comment", - "capability_version": "1.0.1" + "capability_version": "1.0.2" }, "input": { "action": "create", diff --git a/examples/core-process-comment/runtime-requests/uc02-edit-not-owner-deny.json b/examples/core-process-comment/runtime-requests/uc02-edit-not-owner-deny.json index 5aa04868..bd58d763 100644 --- a/examples/core-process-comment/runtime-requests/uc02-edit-not-owner-deny.json +++ b/examples/core-process-comment/runtime-requests/uc02-edit-not-owner-deny.json @@ -4,7 +4,7 @@ "request_id": "core-process-comment-02", "intent": { "capability_id": "core.process-comment", - "capability_version": "1.0.1" + "capability_version": "1.0.2" }, "input": { "action": "edit", diff --git a/examples/core-process-comment/runtime-requests/uc03-reply-depth-deny.json b/examples/core-process-comment/runtime-requests/uc03-reply-depth-deny.json index 256adb29..b63677bf 100644 --- a/examples/core-process-comment/runtime-requests/uc03-reply-depth-deny.json +++ b/examples/core-process-comment/runtime-requests/uc03-reply-depth-deny.json @@ -4,7 +4,7 @@ "request_id": "core-process-comment-03", "intent": { "capability_id": "core.process-comment", - "capability_version": "1.0.1" + "capability_version": "1.0.2" }, "input": { "action": "reply", diff --git a/examples/core-process-comment/runtime-requests/uc04-moderation-quarantine-allow.json b/examples/core-process-comment/runtime-requests/uc04-moderation-quarantine-allow.json index 950153a7..1c4d4e67 100644 --- a/examples/core-process-comment/runtime-requests/uc04-moderation-quarantine-allow.json +++ b/examples/core-process-comment/runtime-requests/uc04-moderation-quarantine-allow.json @@ -4,7 +4,7 @@ "request_id": "core-process-comment-04", "intent": { "capability_id": "core.process-comment", - "capability_version": "1.0.1" + "capability_version": "1.0.2" }, "input": { "action": "create", diff --git a/examples/core-process-comment/runtime-requests/uc05-react-allow.json b/examples/core-process-comment/runtime-requests/uc05-react-allow.json index ee2a0a77..e8431bb8 100644 --- a/examples/core-process-comment/runtime-requests/uc05-react-allow.json +++ b/examples/core-process-comment/runtime-requests/uc05-react-allow.json @@ -4,7 +4,7 @@ "request_id": "core-process-comment-05", "intent": { "capability_id": "core.process-comment", - "capability_version": "1.0.1" + "capability_version": "1.0.2" }, "input": { "action": "react", diff --git a/examples/core-process-comment/runtime-requests/uc06-soft-delete-allow.json b/examples/core-process-comment/runtime-requests/uc06-soft-delete-allow.json index 4ef03d42..31617585 100644 --- a/examples/core-process-comment/runtime-requests/uc06-soft-delete-allow.json +++ b/examples/core-process-comment/runtime-requests/uc06-soft-delete-allow.json @@ -4,7 +4,7 @@ "request_id": "core-process-comment-06", "intent": { "capability_id": "core.process-comment", - "capability_version": "1.0.1" + "capability_version": "1.0.2" }, "input": { "action": "delete", diff --git a/examples/core-process-comment/runtime-requests/uc07-tenant-isolation-deny.json b/examples/core-process-comment/runtime-requests/uc07-tenant-isolation-deny.json index 5fdd044c..7ca5e260 100644 --- a/examples/core-process-comment/runtime-requests/uc07-tenant-isolation-deny.json +++ b/examples/core-process-comment/runtime-requests/uc07-tenant-isolation-deny.json @@ -4,7 +4,7 @@ "request_id": "core-process-comment-07", "intent": { "capability_id": "core.process-comment", - "capability_version": "1.0.1" + "capability_version": "1.0.2" }, "input": { "action": "create", diff --git a/examples/core-process-comment/runtime-requests/uc08-empty-body-deny.json b/examples/core-process-comment/runtime-requests/uc08-empty-body-deny.json index c384c176..464ad5f1 100644 --- a/examples/core-process-comment/runtime-requests/uc08-empty-body-deny.json +++ b/examples/core-process-comment/runtime-requests/uc08-empty-body-deny.json @@ -4,7 +4,7 @@ "request_id": "core-process-comment-08", "intent": { "capability_id": "core.process-comment", - "capability_version": "1.0.1" + "capability_version": "1.0.2" }, "input": { "action": "create", diff --git a/examples/core-process-comment/runtime-requests/uc09-body-too-long-deny.json b/examples/core-process-comment/runtime-requests/uc09-body-too-long-deny.json new file mode 100644 index 00000000..09a4799a --- /dev/null +++ b/examples/core-process-comment/runtime-requests/uc09-body-too-long-deny.json @@ -0,0 +1,62 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-process-comment-09", + "intent": { + "capability_id": "core.process-comment", + "capability_version": "1.0.2" + }, + "input": { + "action": "create", + "actor": { + "id": "user-42", + "roles": [ + "member" + ], + "attributes": { + "tenant_id": "t-100" + } + }, + "resource": { + "type": "document", + "id": "doc-99", + "attributes": { + "tenant_id": "t-100" + } + }, + "comment": { + "body": "xxxxxxxxxxxxxxxxxxxxx", + "parent_id": null + }, + "context": {}, + "comment_policy": { + "version": "2026.08.1", + "max_body_length": 20, + "max_thread_depth": 8, + "allowed_markups": [], + "mention_resolution": "strict", + "visibility_model": "resource-acl", + "soft_delete": true, + "actions": { + "create": { + "roles": [ + "member" + ] + } + }, + "moderation": { + "blocklist": [], + "require_approval_roles": [] + } + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-process-comment-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-process-comment/runtime-requests/uc10-invalid-reaction-deny.json b/examples/core-process-comment/runtime-requests/uc10-invalid-reaction-deny.json new file mode 100644 index 00000000..7a758fca --- /dev/null +++ b/examples/core-process-comment/runtime-requests/uc10-invalid-reaction-deny.json @@ -0,0 +1,64 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-process-comment-10", + "intent": { + "capability_id": "core.process-comment", + "capability_version": "1.0.2" + }, + "input": { + "action": "react", + "actor": { + "id": "user-42", + "roles": [ + "member" + ], + "attributes": { + "tenant_id": "t-100" + } + }, + "resource": { + "type": "document", + "id": "doc-99", + "attributes": { + "tenant_id": "t-100" + } + }, + "comment": { + "id": "cmt-55", + "reaction": { + "op": "add" + } + }, + "context": {}, + "comment_policy": { + "version": "2026.08.1", + "max_body_length": 10000, + "max_thread_depth": 8, + "allowed_markups": [], + "mention_resolution": "strict", + "visibility_model": "resource-acl", + "soft_delete": true, + "actions": { + "react": { + "roles": [ + "member" + ] + } + }, + "moderation": { + "blocklist": [], + "require_approval_roles": [] + } + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-process-comment-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-process-comment/runtime-requests/uc11-insufficient-role-deny.json b/examples/core-process-comment/runtime-requests/uc11-insufficient-role-deny.json new file mode 100644 index 00000000..41a5e306 --- /dev/null +++ b/examples/core-process-comment/runtime-requests/uc11-insufficient-role-deny.json @@ -0,0 +1,63 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-process-comment-11", + "intent": { + "capability_id": "core.process-comment", + "capability_version": "1.0.2" + }, + "input": { + "action": "create", + "actor": { + "id": "user-42", + "roles": [ + "guest" + ], + "attributes": { + "tenant_id": "t-100" + } + }, + "resource": { + "type": "document", + "id": "doc-99", + "attributes": { + "tenant_id": "t-100" + } + }, + "comment": { + "body": "Hello", + "parent_id": null + }, + "context": {}, + "comment_policy": { + "version": "2026.08.1", + "max_body_length": 10000, + "max_thread_depth": 8, + "allowed_markups": [], + "mention_resolution": "strict", + "visibility_model": "resource-acl", + "soft_delete": true, + "actions": { + "create": { + "roles": [ + "member", + "admin" + ] + } + }, + "moderation": { + "blocklist": [], + "require_approval_roles": [] + } + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-process-comment-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-process-comment/workflows/process-comment/workflow.json b/examples/core-process-comment/workflows/process-comment/workflow.json index 916c85c1..fc5b2d0a 100644 --- a/examples/core-process-comment/workflows/process-comment/workflow.json +++ b/examples/core-process-comment/workflows/process-comment/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.process-comment", "name": "process-comment", - "version": "1.0.1", + "version": "1.0.2", "lifecycle": "active", "owner": { "team": "traverse-core", @@ -41,7 +41,7 @@ { "node_id": "process", "capability_id": "core.process-comment", - "capability_version": "1.0.1", + "capability_version": "1.0.2", "input": { "from_workflow_input": [ "action", diff --git a/examples/core-record-nudge-event/contract.json b/examples/core-record-nudge-event/contract.json index 724bd8ef..f2804e54 100644 --- a/examples/core-record-nudge-event/contract.json +++ b/examples/core-record-nudge-event/contract.json @@ -4,14 +4,14 @@ "id": "core.record-nudge-event", "namespace": "core", "name": "record-nudge-event", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Constructs a pure audit/event payload for a nudge that was selected and messaged.", - "description": "Pure WF3 capability. Builds a deterministic nudge event record (event_id, ordinal, channel, intensity, preview) for downstream persistence by the host. memory_only \u2014 no I/O.", + "description": "Pure WF3 capability. Builds a deterministic nudge event record (event_id, ordinal, channel, intensity, preview) for downstream persistence.\n\nImplemented and smoke-tested matrix (use_cases):\n- soft intensity first nudge\n- escalate intensity with ordinal from prior count\n- direct intensity mid-sequence nudge\n- invalid_input when item_id is empty\n\nKnown limitations (intentional in 1.0.1):\n- reason_code invalid_intensity removed; intensity is a schema enum so host validation rejects unknown values before the guest runs\n- Event is constructed only; persistence is caller-owned", "use_cases": [ { "scenario": "As the follow-up engine, I want an audit record after generating a soft nudge.", @@ -74,6 +74,62 @@ }, "happy": true, "persona_ref": "collaboration-product-owner" + }, + { + "scenario": "As WF3, I want a direct-intensity nudge event with ordinal prior+1.", + "input_example": { + "item_id": "ai-2", + "owner_id": "user-ada", + "intensity": "direct", + "channel": "in_app", + "message_preview": "Please complete", + "event_at": "2026-08-07T12:00:00Z", + "prior_nudge_count": 1 + }, + "output_example": { + "event": { + "event_id": "nudge-ai-2-2", + "item_id": "ai-2", + "owner_id": "user-ada", + "intensity": "direct", + "channel": "in_app", + "message_preview": "Please complete", + "event_at": "2026-08-07T12:00:00Z", + "nudge_ordinal": 2 + }, + "reason_code": "ok", + "evaluation_trace": [ + "constructed nudge event", + "ordinal=2" + ] + }, + "happy": true, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As WF3, I want invalid_input when item_id is empty.", + "input_example": { + "item_id": "", + "owner_id": "user-ada", + "intensity": "soft", + "channel": "in_app", + "message_preview": "x", + "event_at": "2026-08-07T10:00:00Z", + "prior_nudge_count": 0 + }, + "output_example": { + "event": { + "event_id": "", + "item_id": "", + "intensity": "", + "event_at": "", + "nudge_ordinal": 0 + }, + "reason_code": "invalid_input", + "evaluation_trace": [] + }, + "happy": false, + "persona_ref": "runtime-engineer" } ], "inputs": { @@ -164,7 +220,6 @@ "type": "string", "enum": [ "ok", - "invalid_intensity", "invalid_input" ] }, @@ -220,7 +275,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.record-nudge-event@1.0.0", + "spec_ref": "core.record-nudge-event@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -228,7 +283,7 @@ }, "evidence": [ { - "evidence_id": "core-record-nudge-event-1.0.0-contract-validation", + "evidence_id": "core-record-nudge-event-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-record-nudge-event/manifest.json b/examples/core-record-nudge-event/manifest.json index cec49756..5b3d58b6 100644 --- a/examples/core-record-nudge-event/manifest.json +++ b/examples/core-record-nudge-event/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.record-nudge-event-agent", - "version": "1.0.0", - "summary": "Capability package for core.record-nudge-event@1.0.0.", + "version": "1.0.1", + "summary": "Capability package for core.record-nudge-event@1.0.1.", "capability_ref": { "id": "core.record-nudge-event", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.record-nudge-event", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-record-nudge-event/runtime-requests/uc01-soft.json b/examples/core-record-nudge-event/runtime-requests/uc01-soft.json index 2c615519..8c5bbf39 100644 --- a/examples/core-record-nudge-event/runtime-requests/uc01-soft.json +++ b/examples/core-record-nudge-event/runtime-requests/uc01-soft.json @@ -4,7 +4,7 @@ "request_id": "core-record-nudge-event-01", "intent": { "capability_id": "core.record-nudge-event", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "item_id": "ai-1", diff --git a/examples/core-record-nudge-event/runtime-requests/uc02-escalate.json b/examples/core-record-nudge-event/runtime-requests/uc02-escalate.json index 61579975..90ec5823 100644 --- a/examples/core-record-nudge-event/runtime-requests/uc02-escalate.json +++ b/examples/core-record-nudge-event/runtime-requests/uc02-escalate.json @@ -4,7 +4,7 @@ "request_id": "core-record-nudge-event-02", "intent": { "capability_id": "core.record-nudge-event", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "item_id": "ai-9", diff --git a/examples/core-record-nudge-event/runtime-requests/uc03-direct.json b/examples/core-record-nudge-event/runtime-requests/uc03-direct.json new file mode 100644 index 00000000..d6ae2b08 --- /dev/null +++ b/examples/core-record-nudge-event/runtime-requests/uc03-direct.json @@ -0,0 +1,27 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-record-nudge-event-03", + "intent": { + "capability_id": "core.record-nudge-event", + "capability_version": "1.0.1" + }, + "input": { + "item_id": "ai-2", + "owner_id": "user-ada", + "intensity": "direct", + "channel": "in_app", + "message_preview": "Please complete", + "event_at": "2026-08-07T12:00:00Z", + "prior_nudge_count": 1 + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-record-nudge-event-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-record-nudge-event/runtime-requests/uc04-invalid-input.json b/examples/core-record-nudge-event/runtime-requests/uc04-invalid-input.json new file mode 100644 index 00000000..9981c159 --- /dev/null +++ b/examples/core-record-nudge-event/runtime-requests/uc04-invalid-input.json @@ -0,0 +1,27 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-record-nudge-event-04", + "intent": { + "capability_id": "core.record-nudge-event", + "capability_version": "1.0.1" + }, + "input": { + "item_id": "", + "owner_id": "user-ada", + "intensity": "soft", + "channel": "in_app", + "message_preview": "x", + "event_at": "2026-08-07T10:00:00Z", + "prior_nudge_count": 0 + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-record-nudge-event-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-record-nudge-event/workflows/record-nudge-event/workflow.json b/examples/core-record-nudge-event/workflows/record-nudge-event/workflow.json index ccc617b8..18e28aef 100644 --- a/examples/core-record-nudge-event/workflows/record-nudge-event/workflow.json +++ b/examples/core-record-nudge-event/workflows/record-nudge-event/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.record-nudge-event", "name": "record-nudge-event", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -26,7 +26,7 @@ { "node_id": "run", "capability_id": "core.record-nudge-event", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [] }, diff --git a/examples/core-select-items-for-followup/contract.json b/examples/core-select-items-for-followup/contract.json index 6b783884..d90e0c73 100644 --- a/examples/core-select-items-for-followup/contract.json +++ b/examples/core-select-items-for-followup/contract.json @@ -4,14 +4,14 @@ "id": "core.select-items-for-followup", "namespace": "core", "name": "select-items-for-followup", - "version": "1.1.0", + "version": "1.1.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Decides which open action items need attention now. Council-revised with quiet hours, snooze support, per-user nudge budgets and higher escalation bar.", - "description": "Hero capability of Loop (council-revised). Key improvements from 1.0.0:\n- Respects per-user quiet hours and snooze until timestamps\n- Enforces max nudges per user per day\n- Escalation only after repeated ignored nudges + high pressure\n- Explicit skip reasons (quiet_hours, snoozed, budget_exceeded, recently_active, low_pressure)\n- Still pure and deterministic \u2014 no messages are sent, only the selection decision is produced.", + "description": "Hero capability of Loop (council-revised). Key improvements from 1.0.0:\n- Respects per-user quiet hours and snooze until timestamps\n- Enforces max nudges per user per day\n- Escalation only after repeated ignored nudges + high pressure\n- Explicit skip reasons (quiet_hours, snoozed, budget_exceeded, recently_active, low_pressure)\n- Still pure and deterministic — no messages are sent, only the selection decision is produced.\n\nImplemented and smoke-tested matrix (use_cases):\n- quiet-hours skip\n- escalate selection for overdue high-pressure items\n- config_error when reference_datetime is empty\n\nKnown limitations (intentional in 1.1.1):\n- No message delivery; selection decision only", "use_cases": [ { "scenario": "As the scheduled job, I want to respect quiet hours and snooze so that we never annoy people at the wrong time.", @@ -116,6 +116,32 @@ }, "happy": true, "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the scheduled job, I want config_error when reference_datetime is empty so the run fails closed.", + "input_example": { + "open_items": [], + "user_preferences": {}, + "reference_datetime": "", + "followup_config": { + "version": "1.1", + "soft_days_before_due": 2, + "direct_days_overdue": 1, + "escalate_after_nudges": 3, + "min_pressure_for_soft": 0.4, + "respect_quiet_hours": true + } + }, + "output_example": { + "selected": [], + "skipped": [], + "reason_code": "config_error", + "evaluation_trace": [ + "required fields missing" + ] + }, + "happy": false, + "persona_ref": "runtime-engineer" } ], "inputs": { @@ -226,7 +252,11 @@ } }, "reason_code": { - "type": "string" + "type": "string", + "enum": [ + "ok", + "config_error" + ] }, "evaluation_trace": { "type": "array", @@ -284,7 +314,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:20:00Z", - "spec_ref": "core.select-items-for-followup@1.1.0", + "spec_ref": "core.select-items-for-followup@1.1.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -292,7 +322,7 @@ }, "evidence": [ { - "evidence_id": "core-select-items-for-followup-1.1.0-contract-validation", + "evidence_id": "core-select-items-for-followup-1.1.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-select-items-for-followup/manifest.json b/examples/core-select-items-for-followup/manifest.json index 655a947e..d6786a73 100644 --- a/examples/core-select-items-for-followup/manifest.json +++ b/examples/core-select-items-for-followup/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.select-items-for-followup-agent", - "version": "1.1.0", - "summary": "Capability package for core.select-items-for-followup@1.1.0.", + "version": "1.1.1", + "summary": "Capability package for core.select-items-for-followup@1.1.1.", "capability_ref": { "id": "core.select-items-for-followup", - "version": "1.1.0", + "version": "1.1.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.select-items-for-followup", - "workflow_version": "1.1.0" + "workflow_version": "1.1.1" } ], "source": { diff --git a/examples/core-select-items-for-followup/runtime-requests/uc01-quiet-hours.json b/examples/core-select-items-for-followup/runtime-requests/uc01-quiet-hours.json index 3bf901b9..0fcc309b 100644 --- a/examples/core-select-items-for-followup/runtime-requests/uc01-quiet-hours.json +++ b/examples/core-select-items-for-followup/runtime-requests/uc01-quiet-hours.json @@ -4,7 +4,7 @@ "request_id": "core-select-items-for-followup-01", "intent": { "capability_id": "core.select-items-for-followup", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "open_items": [ diff --git a/examples/core-select-items-for-followup/runtime-requests/uc02-escalate.json b/examples/core-select-items-for-followup/runtime-requests/uc02-escalate.json index cf25274f..10196b84 100644 --- a/examples/core-select-items-for-followup/runtime-requests/uc02-escalate.json +++ b/examples/core-select-items-for-followup/runtime-requests/uc02-escalate.json @@ -4,7 +4,7 @@ "request_id": "core-select-items-for-followup-02", "intent": { "capability_id": "core.select-items-for-followup", - "capability_version": "1.1.0" + "capability_version": "1.1.1" }, "input": { "open_items": [ diff --git a/examples/core-select-items-for-followup/runtime-requests/uc03-config-error.json b/examples/core-select-items-for-followup/runtime-requests/uc03-config-error.json new file mode 100644 index 00000000..d017dde3 --- /dev/null +++ b/examples/core-select-items-for-followup/runtime-requests/uc03-config-error.json @@ -0,0 +1,31 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-select-items-for-followup-03", + "intent": { + "capability_id": "core.select-items-for-followup", + "capability_version": "1.1.1" + }, + "input": { + "open_items": [], + "user_preferences": {}, + "reference_datetime": "", + "followup_config": { + "version": "1.1", + "soft_days_before_due": 2, + "direct_days_overdue": 1, + "escalate_after_nudges": 3, + "min_pressure_for_soft": 0.4, + "respect_quiet_hours": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-select-items-for-followup-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-select-items-for-followup/workflows/select-items-for-followup/workflow.json b/examples/core-select-items-for-followup/workflows/select-items-for-followup/workflow.json index 1c3ec9b1..d62c1530 100644 --- a/examples/core-select-items-for-followup/workflows/select-items-for-followup/workflow.json +++ b/examples/core-select-items-for-followup/workflows/select-items-for-followup/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.select-items-for-followup", "name": "select-items-for-followup", - "version": "1.1.0", + "version": "1.1.1", "lifecycle": "active", "owner": { "team": "loop", @@ -36,7 +36,7 @@ { "node_id": "run", "capability_id": "core.select-items-for-followup", - "capability_version": "1.1.0", + "capability_version": "1.1.1", "input": { "from_workflow_input": [ "open_items", diff --git a/examples/core-transition-action-status/artifacts/core-transition-action-status.wasm b/examples/core-transition-action-status/artifacts/core-transition-action-status.wasm index bd03f2ac..aa17d384 100755 Binary files a/examples/core-transition-action-status/artifacts/core-transition-action-status.wasm and b/examples/core-transition-action-status/artifacts/core-transition-action-status.wasm differ diff --git a/examples/core-transition-action-status/contract.json b/examples/core-transition-action-status/contract.json index 72b7d9f3..51fbd70e 100644 --- a/examples/core-transition-action-status/contract.json +++ b/examples/core-transition-action-status/contract.json @@ -4,14 +4,14 @@ "id": "core.transition-action-status", "namespace": "core", "name": "transition-action-status", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Deterministic state machine for action item status transitions (open → in_progress → blocked → snoozed → done / cancelled).", - "description": "Pure state-machine capability for Loop action items. Validates that a requested status change is legal given the current status and a caller-supplied transition policy, then returns the new status plus audit metadata.\n\nConfiguration defines allowed transitions and whether only the owner may perform them. The capability never mutates external state — callers apply the returned decision.\n\nCouncil/product alignment note: workflows declare a `snoozed` status for follow-through; this contract includes `snoozed` in the status enum and default transition examples so WF3/WF4 compose without a silent schema gap.", + "description": "Pure state-machine for action-item status transitions under a caller-supplied transition_config (allowed_transitions + owner_only).\n\nImplemented and smoke-tested matrix (use_cases):\n- open → in_progress (ok)\n- done → open illegal (illegal_transition)\n- open → snoozed (ok)\n- non-owner denied (not_owner)\n- in_progress → blocked (ok)\n- in_progress → done (ok)\n- blocked → in_progress (ok)\n- snoozed → open (ok)\n- open → cancelled (ok)\n- cancelled → open illegal (covers current_status=cancelled)\n- missing current_status → invalid_status\n\nKnown limitations (intentional in 1.0.1):\n- Transition graph is entirely caller-supplied; no built-in default policy\n- Does not persist status changes", "use_cases": [ { "scenario": "As an owner, I want to move an item from open to in_progress.", @@ -23,10 +23,26 @@ "transition_config": { "version": "1.0", "allowed_transitions": { - "open": ["in_progress", "cancelled", "snoozed"], - "in_progress": ["blocked", "done", "cancelled", "snoozed"], - "blocked": ["in_progress", "cancelled"], - "snoozed": ["open", "in_progress", "cancelled"], + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], "done": [], "cancelled": [] }, @@ -46,7 +62,7 @@ "persona_ref": "meeting-organizer" }, { - "scenario": "As the system, I want an illegal transition (done → open) to be rejected.", + "scenario": "As the system, I want an illegal transition (done → open) rejected.", "input_example": { "current_status": "done", "requested_status": "open", @@ -55,10 +71,26 @@ "transition_config": { "version": "1.0", "allowed_transitions": { - "open": ["in_progress", "cancelled", "snoozed"], - "in_progress": ["blocked", "done", "cancelled", "snoozed"], - "blocked": ["in_progress", "cancelled"], - "snoozed": ["open", "in_progress", "cancelled"], + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], "done": [], "cancelled": [] }, @@ -77,7 +109,7 @@ "persona_ref": "runtime-engineer" }, { - "scenario": "As an owner, I want to snooze an open item so follow-up respects snoozed_until.", + "scenario": "As an owner, I want to snooze an open item.", "input_example": { "current_status": "open", "requested_status": "snoozed", @@ -86,10 +118,26 @@ "transition_config": { "version": "1.0", "allowed_transitions": { - "open": ["in_progress", "cancelled", "snoozed"], - "in_progress": ["blocked", "done", "cancelled", "snoozed"], - "blocked": ["in_progress", "cancelled"], - "snoozed": ["open", "in_progress", "cancelled"], + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], "done": [], "cancelled": [] }, @@ -118,10 +166,26 @@ "transition_config": { "version": "1.0", "allowed_transitions": { - "open": ["in_progress", "cancelled", "snoozed"], - "in_progress": ["blocked", "done", "cancelled", "snoozed"], - "blocked": ["in_progress", "cancelled"], - "snoozed": ["open", "in_progress", "cancelled"], + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], "done": [], "cancelled": [] }, @@ -138,6 +202,335 @@ }, "happy": false, "persona_ref": "runtime-engineer" + }, + { + "scenario": "As an owner, I want in_progress → blocked.", + "input_example": { + "current_status": "in_progress", + "requested_status": "blocked", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "output_example": { + "allowed": true, + "new_status": "blocked", + "reason_code": "ok", + "evaluation_trace": [ + "in_progress → blocked is allowed" + ] + }, + "happy": true, + "persona_ref": "meeting-organizer" + }, + { + "scenario": "As an owner, I want in_progress → done.", + "input_example": { + "current_status": "in_progress", + "requested_status": "done", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "output_example": { + "allowed": true, + "new_status": "done", + "reason_code": "ok", + "evaluation_trace": [ + "in_progress → done is allowed" + ] + }, + "happy": true, + "persona_ref": "meeting-organizer" + }, + { + "scenario": "As an owner, I want blocked → in_progress.", + "input_example": { + "current_status": "blocked", + "requested_status": "in_progress", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "output_example": { + "allowed": true, + "new_status": "in_progress", + "reason_code": "ok", + "evaluation_trace": [ + "blocked → in_progress is allowed" + ] + }, + "happy": true, + "persona_ref": "meeting-organizer" + }, + { + "scenario": "As an owner, I want snoozed → open.", + "input_example": { + "current_status": "snoozed", + "requested_status": "open", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "output_example": { + "allowed": true, + "new_status": "open", + "reason_code": "ok", + "evaluation_trace": [ + "snoozed → open is allowed" + ] + }, + "happy": true, + "persona_ref": "meeting-organizer" + }, + { + "scenario": "As an owner, I want open → cancelled.", + "input_example": { + "current_status": "open", + "requested_status": "cancelled", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "output_example": { + "allowed": true, + "new_status": "cancelled", + "reason_code": "ok", + "evaluation_trace": [ + "open → cancelled is allowed" + ] + }, + "happy": true, + "persona_ref": "meeting-organizer" + }, + { + "scenario": "As the system, I want cancelled items to reject further transitions.", + "input_example": { + "current_status": "cancelled", + "requested_status": "open", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "output_example": { + "allowed": false, + "new_status": "cancelled", + "reason_code": "illegal_transition", + "evaluation_trace": [ + "cancelled has no allowed transition to open" + ] + }, + "happy": false, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the system, I want missing current_status rejected with invalid_status.", + "input_example": { + "current_status": "", + "requested_status": "open", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "output_example": { + "allowed": false, + "new_status": "", + "reason_code": "invalid_status", + "evaluation_trace": [ + "precondition failed: required fields missing" + ] + }, + "happy": false, + "persona_ref": "runtime-engineer" } ], "inputs": { @@ -287,7 +680,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.transition-action-status@1.0.0", + "spec_ref": "core.transition-action-status@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -295,7 +688,7 @@ }, "evidence": [ { - "evidence_id": "core-transition-action-status-1.0.0-contract-validation", + "evidence_id": "core-transition-action-status-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-transition-action-status/manifest.json b/examples/core-transition-action-status/manifest.json index 0e09655b..75c010da 100644 --- a/examples/core-transition-action-status/manifest.json +++ b/examples/core-transition-action-status/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.transition-action-status-agent", - "version": "1.0.0", - "summary": "Capability package for core.transition-action-status@1.0.0 (Loop action-item status state machine).", + "version": "1.0.1", + "summary": "Capability package for core.transition-action-status@1.0.1.", "capability_ref": { "id": "core.transition-action-status", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.transition-action-status", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { @@ -23,7 +23,7 @@ "binary": { "path": "./artifacts/core-transition-action-status.wasm", "format": "wasm", - "expected_digest": "fnv1a64:ff545835afb57b78", + "expected_digest": "fnv1a64:f392f46763fdd2af", "abi_version": "1.0.0" }, "constraints": { diff --git a/examples/core-transition-action-status/runtime-requests/uc01-open-to-in-progress.json b/examples/core-transition-action-status/runtime-requests/uc01-open-to-in-progress.json index e5c88b39..a09600e4 100644 --- a/examples/core-transition-action-status/runtime-requests/uc01-open-to-in-progress.json +++ b/examples/core-transition-action-status/runtime-requests/uc01-open-to-in-progress.json @@ -4,7 +4,7 @@ "request_id": "core-transition-action-status-01", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "current_status": "open", @@ -14,10 +14,26 @@ "transition_config": { "version": "1.0", "allowed_transitions": { - "open": ["in_progress", "cancelled", "snoozed"], - "in_progress": ["blocked", "done", "cancelled", "snoozed"], - "blocked": ["in_progress", "cancelled"], - "snoozed": ["open", "in_progress", "cancelled"], + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], "done": [], "cancelled": [] }, diff --git a/examples/core-transition-action-status/runtime-requests/uc02-done-to-open-illegal.json b/examples/core-transition-action-status/runtime-requests/uc02-done-to-open-illegal.json index 483d4043..8a2bc83d 100644 --- a/examples/core-transition-action-status/runtime-requests/uc02-done-to-open-illegal.json +++ b/examples/core-transition-action-status/runtime-requests/uc02-done-to-open-illegal.json @@ -4,7 +4,7 @@ "request_id": "core-transition-action-status-02", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "current_status": "done", @@ -14,10 +14,26 @@ "transition_config": { "version": "1.0", "allowed_transitions": { - "open": ["in_progress", "cancelled", "snoozed"], - "in_progress": ["blocked", "done", "cancelled", "snoozed"], - "blocked": ["in_progress", "cancelled"], - "snoozed": ["open", "in_progress", "cancelled"], + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], "done": [], "cancelled": [] }, diff --git a/examples/core-transition-action-status/runtime-requests/uc03-open-to-snoozed.json b/examples/core-transition-action-status/runtime-requests/uc03-open-to-snoozed.json index 262eee49..7557d042 100644 --- a/examples/core-transition-action-status/runtime-requests/uc03-open-to-snoozed.json +++ b/examples/core-transition-action-status/runtime-requests/uc03-open-to-snoozed.json @@ -4,7 +4,7 @@ "request_id": "core-transition-action-status-03", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "current_status": "open", @@ -14,10 +14,26 @@ "transition_config": { "version": "1.0", "allowed_transitions": { - "open": ["in_progress", "cancelled", "snoozed"], - "in_progress": ["blocked", "done", "cancelled", "snoozed"], - "blocked": ["in_progress", "cancelled"], - "snoozed": ["open", "in_progress", "cancelled"], + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], "done": [], "cancelled": [] }, diff --git a/examples/core-transition-action-status/runtime-requests/uc04-non-owner-denied.json b/examples/core-transition-action-status/runtime-requests/uc04-non-owner-denied.json index 28abe463..8cc26b80 100644 --- a/examples/core-transition-action-status/runtime-requests/uc04-non-owner-denied.json +++ b/examples/core-transition-action-status/runtime-requests/uc04-non-owner-denied.json @@ -4,7 +4,7 @@ "request_id": "core-transition-action-status-04", "intent": { "capability_id": "core.transition-action-status", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "current_status": "open", @@ -14,10 +14,26 @@ "transition_config": { "version": "1.0", "allowed_transitions": { - "open": ["in_progress", "cancelled", "snoozed"], - "in_progress": ["blocked", "done", "cancelled", "snoozed"], - "blocked": ["in_progress", "cancelled"], - "snoozed": ["open", "in_progress", "cancelled"], + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], "done": [], "cancelled": [] }, diff --git a/examples/core-transition-action-status/runtime-requests/uc05-in-progress-to-blocked.json b/examples/core-transition-action-status/runtime-requests/uc05-in-progress-to-blocked.json new file mode 100644 index 00000000..b09bf98d --- /dev/null +++ b/examples/core-transition-action-status/runtime-requests/uc05-in-progress-to-blocked.json @@ -0,0 +1,52 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-transition-action-status-05", + "intent": { + "capability_id": "core.transition-action-status", + "capability_version": "1.0.1" + }, + "input": { + "current_status": "in_progress", + "requested_status": "blocked", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-transition-action-status-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-transition-action-status/runtime-requests/uc06-in-progress-to-done.json b/examples/core-transition-action-status/runtime-requests/uc06-in-progress-to-done.json new file mode 100644 index 00000000..25acde83 --- /dev/null +++ b/examples/core-transition-action-status/runtime-requests/uc06-in-progress-to-done.json @@ -0,0 +1,52 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-transition-action-status-06", + "intent": { + "capability_id": "core.transition-action-status", + "capability_version": "1.0.1" + }, + "input": { + "current_status": "in_progress", + "requested_status": "done", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-transition-action-status-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-transition-action-status/runtime-requests/uc07-blocked-to-in-progress.json b/examples/core-transition-action-status/runtime-requests/uc07-blocked-to-in-progress.json new file mode 100644 index 00000000..d5b2e879 --- /dev/null +++ b/examples/core-transition-action-status/runtime-requests/uc07-blocked-to-in-progress.json @@ -0,0 +1,52 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-transition-action-status-07", + "intent": { + "capability_id": "core.transition-action-status", + "capability_version": "1.0.1" + }, + "input": { + "current_status": "blocked", + "requested_status": "in_progress", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-transition-action-status-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-transition-action-status/runtime-requests/uc08-snoozed-to-open.json b/examples/core-transition-action-status/runtime-requests/uc08-snoozed-to-open.json new file mode 100644 index 00000000..acdf8dc3 --- /dev/null +++ b/examples/core-transition-action-status/runtime-requests/uc08-snoozed-to-open.json @@ -0,0 +1,52 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-transition-action-status-08", + "intent": { + "capability_id": "core.transition-action-status", + "capability_version": "1.0.1" + }, + "input": { + "current_status": "snoozed", + "requested_status": "open", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-transition-action-status-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-transition-action-status/runtime-requests/uc09-open-to-cancelled.json b/examples/core-transition-action-status/runtime-requests/uc09-open-to-cancelled.json new file mode 100644 index 00000000..3e3d749b --- /dev/null +++ b/examples/core-transition-action-status/runtime-requests/uc09-open-to-cancelled.json @@ -0,0 +1,52 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-transition-action-status-09", + "intent": { + "capability_id": "core.transition-action-status", + "capability_version": "1.0.1" + }, + "input": { + "current_status": "open", + "requested_status": "cancelled", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-transition-action-status-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-transition-action-status/runtime-requests/uc10-cancelled-to-open-illegal.json b/examples/core-transition-action-status/runtime-requests/uc10-cancelled-to-open-illegal.json new file mode 100644 index 00000000..8a7e4837 --- /dev/null +++ b/examples/core-transition-action-status/runtime-requests/uc10-cancelled-to-open-illegal.json @@ -0,0 +1,52 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-transition-action-status-10", + "intent": { + "capability_id": "core.transition-action-status", + "capability_version": "1.0.1" + }, + "input": { + "current_status": "cancelled", + "requested_status": "open", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-transition-action-status-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-transition-action-status/runtime-requests/uc11-invalid-status.json b/examples/core-transition-action-status/runtime-requests/uc11-invalid-status.json new file mode 100644 index 00000000..1280471f --- /dev/null +++ b/examples/core-transition-action-status/runtime-requests/uc11-invalid-status.json @@ -0,0 +1,52 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-transition-action-status-11", + "intent": { + "capability_id": "core.transition-action-status", + "capability_version": "1.0.1" + }, + "input": { + "current_status": "", + "requested_status": "open", + "actor_id": "user-ada", + "owner_id": "user-ada", + "transition_config": { + "version": "1.0", + "allowed_transitions": { + "open": [ + "in_progress", + "cancelled", + "snoozed" + ], + "in_progress": [ + "blocked", + "done", + "cancelled", + "snoozed" + ], + "blocked": [ + "in_progress", + "cancelled" + ], + "snoozed": [ + "open", + "in_progress", + "cancelled" + ], + "done": [], + "cancelled": [] + }, + "owner_only": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-transition-action-status-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-transition-action-status/src/agent.rs b/examples/core-transition-action-status/src/agent.rs index d1a95f0b..ff51d5ca 100644 --- a/examples/core-transition-action-status/src/agent.rs +++ b/examples/core-transition-action-status/src/agent.rs @@ -192,10 +192,16 @@ fn object_after_key<'a>(hay: &'a [u8], key: &[u8]) -> Option<&'a [u8]> { } fn array_after_key<'a>(hay: &'a [u8], key: &[u8]) -> Option<&'a [u8]> { - let pos = find(hay, key)?; - let after = &hay[pos + key.len()..]; - let colon = after.iter().position(|b| *b == b':')?; - let mut rest = &after[colon + 1..]; + // Match `"name":` so value occurrences of the same string are not treated as keys. + let mut keyed = [0u8; 96]; + if key.len() + 1 > keyed.len() { + return None; + } + let mut k = 0usize; + k = copy(&mut keyed, k, key); + k = copy(&mut keyed, k, b":"); + let pos = find(hay, &keyed[..k])?; + let mut rest = &hay[pos + k..]; while rest.first() == Some(&b' ') || rest.first() == Some(&b'\n') || rest.first() == Some(&b'\t') diff --git a/examples/core-transition-action-status/workflows/transition-action-status/workflow.json b/examples/core-transition-action-status/workflows/transition-action-status/workflow.json index 643645a4..bd69b765 100644 --- a/examples/core-transition-action-status/workflows/transition-action-status/workflow.json +++ b/examples/core-transition-action-status/workflows/transition-action-status/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.transition-action-status", "name": "transition-action-status", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -37,7 +37,7 @@ { "node_id": "transition", "capability_id": "core.transition-action-status", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [ "current_status", diff --git a/examples/core-validate-action-item/contract.json b/examples/core-validate-action-item/contract.json index cd2ac71d..b8615083 100644 --- a/examples/core-validate-action-item/contract.json +++ b/examples/core-validate-action-item/contract.json @@ -4,14 +4,14 @@ "id": "core.validate-action-item", "namespace": "core", "name": "validate-action-item", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", "contact": "founders@loop.dev" }, "summary": "Validates a single action item for required fields, owner existence, due-date sanity and duplicate detection before it becomes a commitment.", - "description": "Gatekeeper capability used when turning extracted candidates into real tracked items (Loop WF2). Enforces data quality rules that are configurable per workspace via `validation_config`. Pure and deterministic — never mutates storage.\n\nChecks covered by this guest:\n- non-empty title\n- optional required owner / due date\n- past-due rejection when `allow_past_due` is false\n- duplicate detection (`none` | `title` | `title_and_owner`) against `existing_open_items`", + "description": "Gatekeeper for action-item commitments: title/owner/due-date rules and duplicate detection under validation_config. Pure and deterministic.\n\nImplemented and smoke-tested matrix (use_cases):\n- complete item validates (ok)\n- past due rejected when allow_past_due=false (validation_failed)\n- missing owner when require_owner=true (validation_failed)\n- duplicate title+owner (duplicate)\n- duplicate title-only check mode (duplicate)\n- invalid duplicate_check config (invalid_config)\n\nKnown limitations (intentional in 1.0.1):\n- Does not verify owner_id exists in a directory; only presence/absence\n- Date parsing is YYYY-MM-DD lexical compare only", "use_cases": [ { "scenario": "As a meeting organizer confirming extracted items, I want a complete item to validate cleanly.", @@ -41,7 +41,9 @@ "due_date": "2026-08-09" }, "reason_code": "ok", - "evaluation_trace": ["all checks passed"] + "evaluation_trace": [ + "all checks passed" + ] }, "happy": true, "persona_ref": "meeting-organizer" @@ -75,7 +77,9 @@ ], "normalized": null, "reason_code": "validation_failed", - "evaluation_trace": ["due_date 2026-08-01 < reference 2026-08-07"] + "evaluation_trace": [ + "due_date 2026-08-01 < reference 2026-08-07" + ] }, "happy": false, "persona_ref": "runtime-engineer" @@ -108,7 +112,9 @@ ], "normalized": null, "reason_code": "validation_failed", - "evaluation_trace": ["require_owner=true and owner_id missing"] + "evaluation_trace": [ + "require_owner=true and owner_id missing" + ] }, "happy": false, "persona_ref": "runtime-engineer" @@ -148,7 +154,81 @@ ], "normalized": null, "reason_code": "duplicate", - "evaluation_trace": ["duplicate_check=title_and_owner matched existing open item"] + "evaluation_trace": [ + "duplicate_check=title_and_owner matched existing open item" + ] + }, + "happy": false, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the system, I want duplicate_check=title to reject matching titles regardless of owner.", + "input_example": { + "action_item": { + "title": "Send proposal", + "owner_id": "user-bob", + "due_date": "2026-08-12" + }, + "existing_open_items": [ + { + "title": "Send proposal", + "owner_id": "user-ada", + "due_date": "2026-08-09" + } + ], + "validation_config": { + "version": "1.0", + "require_owner": true, + "require_due_date": false, + "allow_past_due": false, + "duplicate_check": "title" + }, + "reference_date": "2026-08-07" + }, + "output_example": { + "valid": false, + "errors": [ + { + "field": "title", + "code": "duplicate", + "message": "matching open item already exists" + } + ], + "normalized": null, + "reason_code": "duplicate", + "evaluation_trace": [ + "duplicate_check=title matched existing open item" + ] + }, + "happy": false, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the system, I want an unknown duplicate_check value rejected with invalid_config.", + "input_example": { + "action_item": { + "title": "Send proposal", + "owner_id": "user-ada", + "due_date": "2026-08-12" + }, + "existing_open_items": [], + "validation_config": { + "version": "1.0", + "require_owner": true, + "require_due_date": false, + "allow_past_due": false, + "duplicate_check": "bogus" + }, + "reference_date": "2026-08-07" + }, + "output_example": { + "valid": false, + "errors": [], + "normalized": null, + "reason_code": "invalid_config", + "evaluation_trace": [ + "invalid_config: duplicate_check" + ] }, "happy": false, "persona_ref": "runtime-engineer" @@ -340,7 +420,7 @@ "source": "ai-assisted", "author": "loop-founders + traverse-capability-author", "created_at": "2026-08-08T06:00:00Z", - "spec_ref": "core.validate-action-item@1.0.0", + "spec_ref": "core.validate-action-item@1.0.1", "adr_refs": [ "persona-council-review-2026-08-08" ], @@ -348,7 +428,7 @@ }, "evidence": [ { - "evidence_id": "core-validate-action-item-1.0.0-contract-validation", + "evidence_id": "core-validate-action-item-1.0.1-contract-validation", "type": "contract_validation", "status": "passed" } diff --git a/examples/core-validate-action-item/manifest.json b/examples/core-validate-action-item/manifest.json index 9df9c468..f422bf08 100644 --- a/examples/core-validate-action-item/manifest.json +++ b/examples/core-validate-action-item/manifest.json @@ -2,17 +2,17 @@ "kind": "capability_package", "schema_version": "1.0.0", "package_id": "core.validate-action-item-agent", - "version": "1.0.0", - "summary": "Capability package for core.validate-action-item@1.0.0 (Loop commitment gatekeeper).", + "version": "1.0.1", + "summary": "Capability package for core.validate-action-item@1.0.1.", "capability_ref": { "id": "core.validate-action-item", - "version": "1.0.0", + "version": "1.0.1", "contract_path": "./contract.json" }, "workflow_refs": [ { "workflow_id": "core.validate-action-item", - "workflow_version": "1.0.0" + "workflow_version": "1.0.1" } ], "source": { diff --git a/examples/core-validate-action-item/runtime-requests/uc01-valid-item.json b/examples/core-validate-action-item/runtime-requests/uc01-valid-item.json index c365030d..e62759ff 100644 --- a/examples/core-validate-action-item/runtime-requests/uc01-valid-item.json +++ b/examples/core-validate-action-item/runtime-requests/uc01-valid-item.json @@ -4,7 +4,7 @@ "request_id": "core-validate-action-item-01", "intent": { "capability_id": "core.validate-action-item", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "action_item": { diff --git a/examples/core-validate-action-item/runtime-requests/uc02-past-due.json b/examples/core-validate-action-item/runtime-requests/uc02-past-due.json index fd67e7ca..bde652c5 100644 --- a/examples/core-validate-action-item/runtime-requests/uc02-past-due.json +++ b/examples/core-validate-action-item/runtime-requests/uc02-past-due.json @@ -4,7 +4,7 @@ "request_id": "core-validate-action-item-02", "intent": { "capability_id": "core.validate-action-item", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "action_item": { diff --git a/examples/core-validate-action-item/runtime-requests/uc03-missing-owner.json b/examples/core-validate-action-item/runtime-requests/uc03-missing-owner.json index e272f882..bec9fe89 100644 --- a/examples/core-validate-action-item/runtime-requests/uc03-missing-owner.json +++ b/examples/core-validate-action-item/runtime-requests/uc03-missing-owner.json @@ -4,7 +4,7 @@ "request_id": "core-validate-action-item-03", "intent": { "capability_id": "core.validate-action-item", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "action_item": { diff --git a/examples/core-validate-action-item/runtime-requests/uc04-duplicate.json b/examples/core-validate-action-item/runtime-requests/uc04-duplicate.json index 339fe905..087b3822 100644 --- a/examples/core-validate-action-item/runtime-requests/uc04-duplicate.json +++ b/examples/core-validate-action-item/runtime-requests/uc04-duplicate.json @@ -4,7 +4,7 @@ "request_id": "core-validate-action-item-04", "intent": { "capability_id": "core.validate-action-item", - "capability_version": "1.0.0" + "capability_version": "1.0.1" }, "input": { "action_item": { diff --git a/examples/core-validate-action-item/runtime-requests/uc05-duplicate-title-only.json b/examples/core-validate-action-item/runtime-requests/uc05-duplicate-title-only.json new file mode 100644 index 00000000..992eae0e --- /dev/null +++ b/examples/core-validate-action-item/runtime-requests/uc05-duplicate-title-only.json @@ -0,0 +1,40 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-validate-action-item-05", + "intent": { + "capability_id": "core.validate-action-item", + "capability_version": "1.0.1" + }, + "input": { + "action_item": { + "title": "Send proposal", + "owner_id": "user-bob", + "due_date": "2026-08-12" + }, + "existing_open_items": [ + { + "title": "Send proposal", + "owner_id": "user-ada", + "due_date": "2026-08-09" + } + ], + "validation_config": { + "version": "1.0", + "require_owner": true, + "require_due_date": false, + "allow_past_due": false, + "duplicate_check": "title" + }, + "reference_date": "2026-08-07" + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-validate-action-item-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-validate-action-item/runtime-requests/uc06-invalid-config.json b/examples/core-validate-action-item/runtime-requests/uc06-invalid-config.json new file mode 100644 index 00000000..12eb823c --- /dev/null +++ b/examples/core-validate-action-item/runtime-requests/uc06-invalid-config.json @@ -0,0 +1,34 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-validate-action-item-06", + "intent": { + "capability_id": "core.validate-action-item", + "capability_version": "1.0.1" + }, + "input": { + "action_item": { + "title": "Send proposal", + "owner_id": "user-ada", + "due_date": "2026-08-12" + }, + "existing_open_items": [], + "validation_config": { + "version": "1.0", + "require_owner": true, + "require_due_date": false, + "allow_past_due": false, + "duplicate_check": "bogus" + }, + "reference_date": "2026-08-07" + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-validate-action-item-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-validate-action-item/workflows/validate-action-item/workflow.json b/examples/core-validate-action-item/workflows/validate-action-item/workflow.json index 6c5a1afb..2daba098 100644 --- a/examples/core-validate-action-item/workflows/validate-action-item/workflow.json +++ b/examples/core-validate-action-item/workflows/validate-action-item/workflow.json @@ -3,7 +3,7 @@ "schema_version": "1.0.0", "id": "core.validate-action-item", "name": "validate-action-item", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "active", "owner": { "team": "loop", @@ -35,7 +35,7 @@ { "node_id": "validate", "capability_id": "core.validate-action-item", - "capability_version": "1.0.0", + "capability_version": "1.0.1", "input": { "from_workflow_input": [ "action_item", diff --git a/scripts/ci/core_aggregate_team_action_health_example_smoke.sh b/scripts/ci/core_aggregate_team_action_health_example_smoke.sh index bf361ef5..6c3468ae 100755 --- a/scripts/ci/core_aggregate_team_action_health_example_smoke.sh +++ b/scripts/ci/core_aggregate_team_action_health_example_smoke.sh @@ -28,7 +28,7 @@ echo "==> capability inspect" contract_out="$("${cli[@]}" capability inspect "$pkg/contract.json")" printf '%s\n' "$contract_out" require_match "$contract_out" "id: core.aggregate-team-action-health" "contract inspect id" -require_match "$contract_out" "version: 1.0.0" "contract inspect version" +require_match "$contract_out" "version: 1.0.1" "contract inspect version" echo "==> wasm abi verify" abi_out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-aggregate-team-action-health.wasm")" @@ -39,27 +39,28 @@ echo "==> capability-package inspect" pkg_out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")" printf '%s\n' "$pkg_out" require_match "$pkg_out" "package_id: core.aggregate-team-action-health-agent" "package_id" -require_match "$pkg_out" "capability_version: 1.0.0" "capability_version" +require_match "$pkg_out" "capability_version: 1.0.1" "capability_version" assert_execute() { local request="$1" - local label="$2" - shift 2 + local code="$2" + local label="$3" + shift 3 echo "==> execute $label" local out out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" printf '%s\n' "$out" require_match "$out" "status: completed" "$label status" - require_match "$out" "capability_version: 1.0.0" "$label capability_version" - require_match "$out" "\"reason_code\": \"ok\"" "$label reason_code" + require_match "$out" "capability_version: 1.0.1" "$label capability_version" + require_match "$out" "\"reason_code\": \"$code\"" "$label reason_code" while [[ $# -gt 0 ]]; do require_match "$out" "$1" "$label extra" shift done } -assert_execute "$pkg/runtime-requests/uc01-team-pulse.json" "UC-01" \ +assert_execute "$pkg/runtime-requests/uc01-team-pulse.json" "ok" "UC-01" \ '"total_open": 3' \ '"overdue_count": 1' \ '"on_track_pct": 66.6' \ @@ -67,5 +68,6 @@ assert_execute "$pkg/runtime-requests/uc01-team-pulse.json" "UC-01" \ '"open_count": 2' \ '"ai-3"' \ '"ai-1"' +assert_execute "$pkg/runtime-requests/uc02-invalid-input.json" "invalid_input" "UC-02" -echo "OK: core.aggregate-team-action-health 1.0.0 E2E smoke passed" +echo "OK: core.aggregate-team-action-health 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_assign_ownership_example_smoke.sh b/scripts/ci/core_assign_ownership_example_smoke.sh index 4a88ec41..d94ea6fc 100755 --- a/scripts/ci/core_assign_ownership_example_smoke.sh +++ b/scripts/ci/core_assign_ownership_example_smoke.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# End-to-end smoke for examples/core-assign-ownership. +# End-to-end smoke for examples/core-assign-ownership (core.assign-ownership@1.0.1). set -euo pipefail @@ -28,6 +28,7 @@ echo "==> capability inspect" contract_out="$("${cli[@]}" capability inspect "$pkg/contract.json")" printf '%s\n' "$contract_out" require_match "$contract_out" "id: core.assign-ownership" "contract inspect id" +require_match "$contract_out" "version: 1.0.1" "contract inspect version" echo "==> wasm abi verify" abi_out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-assign-ownership.wasm")" @@ -38,6 +39,7 @@ echo "==> capability-package inspect" pkg_out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")" printf '%s\n' "$pkg_out" require_match "$pkg_out" "package_id: core.assign-ownership-agent" "package_id" +require_match "$pkg_out" "capability_version: 1.0.1" "capability_version" assert_execute() { local request="$1" @@ -50,6 +52,7 @@ assert_execute() { out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" printf '%s\n' "$out" require_match "$out" "status: completed" "$label status" + require_match "$out" "capability_version: 1.0.1" "$label capability_version" require_match "$out" "\"reason_code\": \"$code\"" "$label reason_code" if [[ -n "$extra" ]]; then require_match "$out" "$extra" "$label extra" @@ -60,5 +63,8 @@ assert_execute "$pkg/runtime-requests/uc01-name-match.json" "ok" "UC-01" '"owner assert_execute "$pkg/runtime-requests/uc02-email-match.json" "ok" "UC-02" '"owner_id": "user-bob"' assert_execute "$pkg/runtime-requests/uc03-null-fallback-creator.json" "ok" "UC-03" '"owner_id": "user-carol"' assert_execute "$pkg/runtime-requests/uc04-unresolved-fail.json" "unresolved" "UC-04" '"owner_id": null' +assert_execute "$pkg/runtime-requests/uc05-inactive-member.json" "inactive_member" "UC-05" +assert_execute "$pkg/runtime-requests/uc06-config-error.json" "config_error" "UC-06" +assert_execute "$pkg/runtime-requests/uc07-fallback-unassigned.json" "ok" "UC-07" '"resolution_method": "fallback_unassigned"' -echo "OK: core.assign-ownership 1.0.0 E2E smoke passed" +echo "OK: core.assign-ownership 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_authorize_example_smoke.sh b/scripts/ci/core_authorize_example_smoke.sh index 67c38c8c..cf69efdd 100755 --- a/scripts/ci/core_authorize_example_smoke.sh +++ b/scripts/ci/core_authorize_example_smoke.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# End-to-end smoke for examples/core-authorize (core.authorize@1.1.0). +# End-to-end smoke for examples/core-authorize (core.authorize@1.1.1). set -euo pipefail @@ -28,7 +28,7 @@ echo "==> capability inspect" contract_out="$("${cli[@]}" capability inspect "$pkg/contract.json")" printf '%s\n' "$contract_out" require_match "$contract_out" "id: core.authorize" "contract inspect id" -require_match "$contract_out" "version: 1.1.0" "contract inspect version" +require_match "$contract_out" "version: 1.1.1" "contract inspect version" echo "==> wasm abi verify" abi_out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-authorize.wasm")" @@ -39,7 +39,7 @@ echo "==> capability-package inspect" pkg_out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")" printf '%s\n' "$pkg_out" require_match "$pkg_out" "package_id: core.authorize-agent" "package_id" -require_match "$pkg_out" "capability_version: 1.1.0" "capability_version" +require_match "$pkg_out" "capability_version: 1.1.1" "capability_version" assert_execute() { local request="$1" @@ -53,7 +53,7 @@ assert_execute() { out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" printf '%s\n' "$out" require_match "$out" "status: completed" "$label status" - require_match "$out" "capability_version: 1.1.0" "$label capability_version" + require_match "$out" "capability_version: 1.1.1" "$label capability_version" require_match "$out" "\"decision\": \"$decision\"" "$label decision" require_match "$out" "\"reason_code\": \"$code\"" "$label reason_code" if [[ -n "$extra" ]]; then @@ -70,5 +70,7 @@ assert_execute "$pkg/runtime-requests/uc06-no-match-deny.json" "deny" "no_matchi assert_execute "$pkg/runtime-requests/uc07-empty-policy-deny.json" "deny" "empty_or_invalid_policy" "UC-07" assert_execute "$pkg/runtime-requests/uc08-break-glass-allow.json" "allow" "break_glass_override" "UC-08" '"break_glass_used": true' assert_execute "$pkg/runtime-requests/uc09-invalid-principal-deny.json" "deny" "invalid_principal" "UC-09" '"policy_hash": null' +assert_execute "$pkg/runtime-requests/uc10-invalid-action-deny.json" "deny" "invalid_action" "UC-10" +assert_execute "$pkg/runtime-requests/uc11-invalid-resource-deny.json" "deny" "invalid_resource" "UC-11" -echo "OK: core.authorize 1.1.0 E2E smoke passed" +echo "OK: core.authorize 1.1.1 E2E smoke passed" diff --git a/scripts/ci/core_calculate_deadline_pressure_example_smoke.sh b/scripts/ci/core_calculate_deadline_pressure_example_smoke.sh index c16e5351..3b713e80 100755 --- a/scripts/ci/core_calculate_deadline_pressure_example_smoke.sh +++ b/scripts/ci/core_calculate_deadline_pressure_example_smoke.sh @@ -35,4 +35,7 @@ assert_execute "$pkg/runtime-requests/uc01-due-soon.json" "ok" "UC-01" \ assert_execute "$pkg/runtime-requests/uc02-overdue.json" "ok" "UC-02" \ '"pressure_band": "overdue"' \ '"pressure_score": 1.0' -echo "OK: core.calculate-deadline-pressure 1.0.0 E2E smoke passed" +assert_execute "$pkg/runtime-requests/uc03-missing-due-date.json" "missing_due_date" "UC-03" +assert_execute "$pkg/runtime-requests/uc04-invalid-date.json" "invalid_date" "UC-04" +assert_execute "$pkg/runtime-requests/uc05-config-error.json" "config_error" "UC-05" +echo "OK: core.calculate-deadline-pressure 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_calculate_price_example_smoke.sh b/scripts/ci/core_calculate_price_example_smoke.sh index 7222082e..0ae61b93 100755 --- a/scripts/ci/core_calculate_price_example_smoke.sh +++ b/scripts/ci/core_calculate_price_example_smoke.sh @@ -10,21 +10,32 @@ echo "==> build-fixture"; bash "$pkg/build-fixture.sh" echo "==> capability inspect" out="$("${cli[@]}" capability inspect "$pkg/contract.json")"; printf '%s\n' "$out" require_match "$out" "id: core.calculate-price" "id" +require_match "$out" "version: 1.0.1" "version" echo "==> wasm abi verify" out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-calculate-price.wasm")"; printf '%s\n' "$out" require_match "$out" "import whitelist passed" "abi" echo "==> capability-package inspect" out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")"; printf '%s\n' "$out" require_match "$out" "package_id: core.calculate-price-agent" "package" +require_match "$out" "capability_version: 1.0.1" "capability_version" assert_execute() { - local request="$1" code="$2" label="$3" extra="${4:-}" + local request="$1" code="$2" label="$3"; shift 3 echo "==> execute $label" local out; out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" printf '%s\n' "$out" require_match "$out" "status: completed" "$label status" require_match "$out" "\"reason_code\": \"$code\"" "$label code" - [[ -z "$extra" ]] || require_match "$out" "$extra" "$label extra" + while [[ $# -gt 0 ]]; do + require_match "$out" "$1" "$label extra" + shift + done } assert_execute "$pkg/runtime-requests/uc01-percentage-discount-tax.json" "ok" "UC-01" '"net": 97.2' -assert_execute "$pkg/runtime-requests/uc02-invalid-quantity.json" "invalid_quantity" "UC-02" 'quantity must be > 0' -echo "OK: core.calculate-price 1.0.0 E2E smoke passed" +assert_execute "$pkg/runtime-requests/uc02-fixed-discount.json" "ok" "UC-02" '"net": 94.0' +assert_execute "$pkg/runtime-requests/uc03-empty-rules.json" "ok" "UC-03" '"net": 50.0' +assert_execute "$pkg/runtime-requests/uc04-invalid-quantity.json" "invalid_quantity" "UC-04" +assert_execute "$pkg/runtime-requests/uc05-invalid-unit-price.json" "invalid_unit_price" "UC-05" +assert_execute "$pkg/runtime-requests/uc06-invalid-config.json" "invalid_config" "UC-06" +assert_execute "$pkg/runtime-requests/uc07-currency-mismatch.json" "currency_mismatch" "UC-07" +assert_execute "$pkg/runtime-requests/uc08-empty-cart.json" "empty_cart" "UC-08" +echo "OK: core.calculate-price 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_decide_escalation_example_smoke.sh b/scripts/ci/core_decide_escalation_example_smoke.sh index 1fa1ebcf..d070a3cf 100755 --- a/scripts/ci/core_decide_escalation_example_smoke.sh +++ b/scripts/ci/core_decide_escalation_example_smoke.sh @@ -35,4 +35,5 @@ assert_execute "$pkg/runtime-requests/uc02-escalate.json" "ok" "UC-02" \ '"decision": "escalate"' \ 'ai-9' \ '"signals_met": 3' -echo "OK: core.decide-escalation 1.0.0 E2E smoke passed" +assert_execute "$pkg/runtime-requests/uc03-invalid-input.json" "invalid_input" "UC-03" +echo "OK: core.decide-escalation 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_evaluate_completion_quality_example_smoke.sh b/scripts/ci/core_evaluate_completion_quality_example_smoke.sh index 11c45f2c..edf0ff6d 100755 --- a/scripts/ci/core_evaluate_completion_quality_example_smoke.sh +++ b/scripts/ci/core_evaluate_completion_quality_example_smoke.sh @@ -34,4 +34,6 @@ assert_execute "$pkg/runtime-requests/uc01-pass-with-evidence.json" "ok" "UC-01" assert_execute "$pkg/runtime-requests/uc02-needs-evidence.json" "ok" "UC-02" \ '"verdict": "needs_evidence"' \ 'missing_evidence' -echo "OK: core.evaluate-completion-quality 1.0.0 E2E smoke passed" +assert_execute "$pkg/runtime-requests/uc03-invalid-status.json" "invalid_status" "UC-03" +assert_execute "$pkg/runtime-requests/uc04-invalid-input.json" "invalid_input" "UC-04" +echo "OK: core.evaluate-completion-quality 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_extract_action_items_example_smoke.sh b/scripts/ci/core_extract_action_items_example_smoke.sh index c71f22fd..c7a9596d 100755 --- a/scripts/ci/core_extract_action_items_example_smoke.sh +++ b/scripts/ci/core_extract_action_items_example_smoke.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# End-to-end smoke for examples/core-extract-action-items (core.extract-action-items@1.1.0). +# End-to-end smoke for examples/core-extract-action-items (core.extract-action-items@1.1.1). set -euo pipefail @@ -28,7 +28,7 @@ echo "==> capability inspect" contract_out="$("${cli[@]}" capability inspect "$pkg/contract.json")" printf '%s\n' "$contract_out" require_match "$contract_out" "id: core.extract-action-items" "contract inspect id" -require_match "$contract_out" "version: 1.1.0" "contract inspect version" +require_match "$contract_out" "version: 1.1.1" "contract inspect version" echo "==> wasm abi verify" abi_out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-extract-action-items.wasm")" @@ -39,7 +39,7 @@ echo "==> capability-package inspect" pkg_out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")" printf '%s\n' "$pkg_out" require_match "$pkg_out" "package_id: core.extract-action-items-agent" "package_id" -require_match "$pkg_out" "capability_version: 1.1.0" "capability_version" +require_match "$pkg_out" "capability_version: 1.1.1" "capability_version" assert_execute() { local request="$1" @@ -52,7 +52,7 @@ assert_execute() { out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" printf '%s\n' "$out" require_match "$out" "status: completed" "$label status" - require_match "$out" "capability_version: 1.1.0" "$label capability_version" + require_match "$out" "capability_version: 1.1.1" "$label capability_version" require_match "$out" "\"reason_code\": \"$code\"" "$label reason_code" if [[ -n "$extra" ]]; then require_match "$out" "$extra" "$label extra" @@ -63,7 +63,7 @@ echo "==> execute UC-01" uc01_out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$pkg/runtime-requests/uc01-extract-mixed.json")" printf '%s\n' "$uc01_out" require_match "$uc01_out" "status: completed" "UC-01 status" -require_match "$uc01_out" "capability_version: 1.1.0" "UC-01 capability_version" +require_match "$uc01_out" "capability_version: 1.1.1" "UC-01 capability_version" require_match "$uc01_out" "\"reason_code\": \"ok\"" "UC-01 reason_code" require_match "$uc01_out" '"title": "Send the revised proposal"' "UC-01 ada title" require_match "$uc01_out" '"suggested_owner": "Ada Lovelace"' "UC-01 ada owner" @@ -78,4 +78,4 @@ require_match "$uc01_out" '"confidence": 0.58' "UC-01 vague confidence" assert_execute "$pkg/runtime-requests/uc02-no-actions.json" "no_action_items_found" "UC-02" '"needs_human_review": \[\]' -echo "OK: core.extract-action-items 1.1.0 E2E smoke passed" +echo "OK: core.extract-action-items 1.1.1 E2E smoke passed" diff --git a/scripts/ci/core_generate_nudge_message_example_smoke.sh b/scripts/ci/core_generate_nudge_message_example_smoke.sh index 8c606cd0..3eb46fec 100755 --- a/scripts/ci/core_generate_nudge_message_example_smoke.sh +++ b/scripts/ci/core_generate_nudge_message_example_smoke.sh @@ -27,4 +27,6 @@ assert_execute() { } assert_execute "$pkg/runtime-requests/uc01-soft-friendly.json" "ok" "UC-01" 'Send the revised proposal' assert_execute "$pkg/runtime-requests/uc02-escalate.json" "ok" "UC-02" 'Escalation' -echo "OK: core.generate-nudge-message 1.0.0 E2E smoke passed" +assert_execute "$pkg/runtime-requests/uc03-direct-neutral.json" "ok" "UC-03" 'Please complete' +assert_execute "$pkg/runtime-requests/uc04-config-error.json" "config_error" "UC-04" +echo "OK: core.generate-nudge-message 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_normalize_participants_example_smoke.sh b/scripts/ci/core_normalize_participants_example_smoke.sh index b91ebdeb..fe3f6bea 100755 --- a/scripts/ci/core_normalize_participants_example_smoke.sh +++ b/scripts/ci/core_normalize_participants_example_smoke.sh @@ -36,4 +36,4 @@ assert_execute "$pkg/runtime-requests/uc01-mixed-match.json" "ok" "UC-01" \ assert_execute "$pkg/runtime-requests/uc02-name-match.json" "ok" "UC-02" \ '"match_method": "name"' \ '"matched_count": 1' -echo "OK: core.normalize-participants 1.0.0 E2E smoke passed" +echo "OK: core.normalize-participants 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_notify_stakeholders_example_smoke.sh b/scripts/ci/core_notify_stakeholders_example_smoke.sh index 9518578a..d9afd434 100755 --- a/scripts/ci/core_notify_stakeholders_example_smoke.sh +++ b/scripts/ci/core_notify_stakeholders_example_smoke.sh @@ -34,4 +34,6 @@ assert_execute "$pkg/runtime-requests/uc01-completed.json" "ok" "UC-01" \ 'user-carol' assert_execute "$pkg/runtime-requests/uc02-empty.json" "nothing_to_notify" "UC-02" \ '"intent_count": 0' -echo "OK: core.notify-stakeholders 1.0.0 E2E smoke passed" +assert_execute "$pkg/runtime-requests/uc03-status-changed.json" "ok" "UC-03" 'Action updated' '"intent_count": 1' +assert_execute "$pkg/runtime-requests/uc04-blocked.json" "ok" "UC-04" 'Action blocked' '"intent_count": 1' +echo "OK: core.notify-stakeholders 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_process_comment_example_smoke.sh b/scripts/ci/core_process_comment_example_smoke.sh index 969a5410..5097491c 100755 --- a/scripts/ci/core_process_comment_example_smoke.sh +++ b/scripts/ci/core_process_comment_example_smoke.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# End-to-end smoke for examples/core-process-comment (core.process-comment@1.0.0). +# End-to-end smoke for examples/core-process-comment (core.process-comment@1.0.2). set -euo pipefail @@ -28,7 +28,7 @@ echo "==> capability inspect" contract_out="$("${cli[@]}" capability inspect "$pkg/contract.json")" printf '%s\n' "$contract_out" require_match "$contract_out" "id: core.process-comment" "contract inspect id" -require_match "$contract_out" "version: 1.0.1" "contract inspect version" +require_match "$contract_out" "version: 1.0.2" "contract inspect version" echo "==> wasm abi verify" abi_out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-process-comment.wasm")" @@ -39,7 +39,7 @@ echo "==> capability-package inspect" pkg_out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")" printf '%s\n' "$pkg_out" require_match "$pkg_out" "package_id: core.process-comment-agent" "package_id" -require_match "$pkg_out" "capability_version: 1.0.1" "capability_version" +require_match "$pkg_out" "capability_version: 1.0.2" "capability_version" assert_execute() { local request="$1" @@ -53,7 +53,7 @@ assert_execute() { out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" printf '%s\n' "$out" require_match "$out" "status: completed" "$label status" - require_match "$out" "capability_version: 1.0.1" "$label capability_version" + require_match "$out" "capability_version: 1.0.2" "$label capability_version" require_match "$out" "\"decision\": \"$decision\"" "$label decision" require_match "$out" "\"reason_code\": \"$code\"" "$label reason_code" if [[ -n "$extra" ]]; then @@ -69,5 +69,8 @@ assert_execute "$pkg/runtime-requests/uc05-react-allow.json" "allow" "ok" "UC-05 assert_execute "$pkg/runtime-requests/uc06-soft-delete-allow.json" "allow" "ok" "UC-06" '"deleted": true' assert_execute "$pkg/runtime-requests/uc07-tenant-isolation-deny.json" "deny" "tenant_isolation_violation" "UC-07" assert_execute "$pkg/runtime-requests/uc08-empty-body-deny.json" "deny" "empty_body" "UC-08" +assert_execute "$pkg/runtime-requests/uc09-body-too-long-deny.json" "deny" "body_too_long" "UC-09" +assert_execute "$pkg/runtime-requests/uc10-invalid-reaction-deny.json" "deny" "invalid_reaction" "UC-10" +assert_execute "$pkg/runtime-requests/uc11-insufficient-role-deny.json" "deny" "insufficient_role" "UC-11" -echo "OK: core.process-comment 1.0.1 E2E smoke passed" +echo "OK: core.process-comment 1.0.2 E2E smoke passed" diff --git a/scripts/ci/core_record_nudge_event_example_smoke.sh b/scripts/ci/core_record_nudge_event_example_smoke.sh index f81362f3..973dfa47 100755 --- a/scripts/ci/core_record_nudge_event_example_smoke.sh +++ b/scripts/ci/core_record_nudge_event_example_smoke.sh @@ -34,4 +34,6 @@ assert_execute "$pkg/runtime-requests/uc01-soft.json" "ok" "UC-01" \ assert_execute "$pkg/runtime-requests/uc02-escalate.json" "ok" "UC-02" \ '"event_id": "nudge-ai-9-4"' \ '"intensity": "escalate"' -echo "OK: core.record-nudge-event 1.0.0 E2E smoke passed" +assert_execute "$pkg/runtime-requests/uc03-direct.json" "ok" "UC-03" '"intensity": "direct"' '"nudge_ordinal": 2' +assert_execute "$pkg/runtime-requests/uc04-invalid-input.json" "invalid_input" "UC-04" +echo "OK: core.record-nudge-event 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_select_items_for_followup_example_smoke.sh b/scripts/ci/core_select_items_for_followup_example_smoke.sh index 50b4b940..0a7292cc 100755 --- a/scripts/ci/core_select_items_for_followup_example_smoke.sh +++ b/scripts/ci/core_select_items_for_followup_example_smoke.sh @@ -28,7 +28,7 @@ echo "==> capability inspect" contract_out="$("${cli[@]}" capability inspect "$pkg/contract.json")" printf '%s\n' "$contract_out" require_match "$contract_out" "id: core.select-items-for-followup" "contract inspect id" -require_match "$contract_out" "version: 1.1.0" "contract inspect version" +require_match "$contract_out" "version: 1.1.1" "contract inspect version" echo "==> wasm abi verify" abi_out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-select-items-for-followup.wasm")" @@ -39,28 +39,30 @@ echo "==> capability-package inspect" pkg_out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")" printf '%s\n' "$pkg_out" require_match "$pkg_out" "package_id: core.select-items-for-followup-agent" "package_id" -require_match "$pkg_out" "capability_version: 1.1.0" "capability_version" +require_match "$pkg_out" "capability_version: 1.1.1" "capability_version" assert_execute() { local request="$1" - local label="$2" - local extra="${3:-}" + local code="$2" + local label="$3" + local extra="${4:-}" echo "==> execute $label" local out out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" printf '%s\n' "$out" require_match "$out" "status: completed" "$label status" - require_match "$out" "capability_version: 1.1.0" "$label capability_version" - require_match "$out" "\"reason_code\": \"ok\"" "$label reason_code" + require_match "$out" "capability_version: 1.1.1" "$label capability_version" + require_match "$out" "\"reason_code\": \"$code\"" "$label reason_code" if [[ -n "$extra" ]]; then require_match "$out" "$extra" "$label extra" fi } -assert_execute "$pkg/runtime-requests/uc01-quiet-hours.json" "UC-01" '"reason": "quiet_hours"' -assert_execute "$pkg/runtime-requests/uc01-quiet-hours.json" "UC-01 selected empty" '"selected": \[\]' -assert_execute "$pkg/runtime-requests/uc02-escalate.json" "UC-02" '"intensity": "escalate"' -assert_execute "$pkg/runtime-requests/uc02-escalate.json" "UC-02 channel" '"recommended_channel": "manager"' +assert_execute "$pkg/runtime-requests/uc01-quiet-hours.json" "ok" "UC-01" '"reason": "quiet_hours"' +assert_execute "$pkg/runtime-requests/uc01-quiet-hours.json" "ok" "UC-01 selected empty" '"selected": \[\]' +assert_execute "$pkg/runtime-requests/uc02-escalate.json" "ok" "UC-02" '"intensity": "escalate"' +assert_execute "$pkg/runtime-requests/uc02-escalate.json" "ok" "UC-02 channel" '"recommended_channel": "manager"' +assert_execute "$pkg/runtime-requests/uc03-config-error.json" "config_error" "UC-03" -echo "OK: core.select-items-for-followup 1.1.0 E2E smoke passed" +echo "OK: core.select-items-for-followup 1.1.1 E2E smoke passed" diff --git a/scripts/ci/core_transition_action_status_example_smoke.sh b/scripts/ci/core_transition_action_status_example_smoke.sh index fe9602ec..f012bdab 100755 --- a/scripts/ci/core_transition_action_status_example_smoke.sh +++ b/scripts/ci/core_transition_action_status_example_smoke.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# End-to-end smoke for examples/core-transition-action-status. +# End-to-end smoke for examples/core-transition-action-status (core.transition-action-status@1.0.1). set -euo pipefail @@ -28,7 +28,7 @@ echo "==> capability inspect" contract_out="$("${cli[@]}" capability inspect "$pkg/contract.json")" printf '%s\n' "$contract_out" require_match "$contract_out" "id: core.transition-action-status" "contract inspect id" -require_match "$contract_out" "version: 1.0.0" "contract inspect version" +require_match "$contract_out" "version: 1.0.1" "contract inspect version" echo "==> wasm abi verify" abi_out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-transition-action-status.wasm")" @@ -39,7 +39,7 @@ echo "==> capability-package inspect" pkg_out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")" printf '%s\n' "$pkg_out" require_match "$pkg_out" "package_id: core.transition-action-status-agent" "package_id" -require_match "$pkg_out" "capability_version: 1.0.0" "capability_version" +require_match "$pkg_out" "capability_version: 1.0.1" "capability_version" assert_execute() { local request="$1" @@ -53,7 +53,7 @@ assert_execute() { out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" printf '%s\n' "$out" require_match "$out" "status: completed" "$label status" - require_match "$out" "capability_version: 1.0.0" "$label capability_version" + require_match "$out" "capability_version: 1.0.1" "$label capability_version" require_match "$out" "\"allowed\": $allowed" "$label allowed" require_match "$out" "\"reason_code\": \"$code\"" "$label reason_code" if [[ -n "$extra" ]]; then @@ -65,5 +65,12 @@ assert_execute "$pkg/runtime-requests/uc01-open-to-in-progress.json" "true" "ok" assert_execute "$pkg/runtime-requests/uc02-done-to-open-illegal.json" "false" "illegal_transition" "UC-02" '"new_status": "done"' assert_execute "$pkg/runtime-requests/uc03-open-to-snoozed.json" "true" "ok" "UC-03" '"new_status": "snoozed"' assert_execute "$pkg/runtime-requests/uc04-non-owner-denied.json" "false" "not_owner" "UC-04" '"new_status": "open"' +assert_execute "$pkg/runtime-requests/uc05-in-progress-to-blocked.json" "true" "ok" "UC-05" '"new_status": "blocked"' +assert_execute "$pkg/runtime-requests/uc06-in-progress-to-done.json" "true" "ok" "UC-06" '"new_status": "done"' +assert_execute "$pkg/runtime-requests/uc07-blocked-to-in-progress.json" "true" "ok" "UC-07" '"new_status": "in_progress"' +assert_execute "$pkg/runtime-requests/uc08-snoozed-to-open.json" "true" "ok" "UC-08" '"new_status": "open"' +assert_execute "$pkg/runtime-requests/uc09-open-to-cancelled.json" "true" "ok" "UC-09" '"new_status": "cancelled"' +assert_execute "$pkg/runtime-requests/uc10-cancelled-to-open-illegal.json" "false" "illegal_transition" "UC-10" '"new_status": "cancelled"' +assert_execute "$pkg/runtime-requests/uc11-invalid-status.json" "false" "invalid_status" "UC-11" -echo "OK: core.transition-action-status 1.0.0 E2E smoke passed" +echo "OK: core.transition-action-status 1.0.1 E2E smoke passed" diff --git a/scripts/ci/core_validate_action_item_example_smoke.sh b/scripts/ci/core_validate_action_item_example_smoke.sh index 135a16a8..d7ac9d00 100755 --- a/scripts/ci/core_validate_action_item_example_smoke.sh +++ b/scripts/ci/core_validate_action_item_example_smoke.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# End-to-end smoke for examples/core-validate-action-item. +# End-to-end smoke for examples/core-validate-action-item (core.validate-action-item@1.0.1). set -euo pipefail @@ -28,7 +28,7 @@ echo "==> capability inspect" contract_out="$("${cli[@]}" capability inspect "$pkg/contract.json")" printf '%s\n' "$contract_out" require_match "$contract_out" "id: core.validate-action-item" "contract inspect id" -require_match "$contract_out" "version: 1.0.0" "contract inspect version" +require_match "$contract_out" "version: 1.0.1" "contract inspect version" echo "==> wasm abi verify" abi_out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-validate-action-item.wasm")" @@ -39,7 +39,7 @@ echo "==> capability-package inspect" pkg_out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")" printf '%s\n' "$pkg_out" require_match "$pkg_out" "package_id: core.validate-action-item-agent" "package_id" -require_match "$pkg_out" "capability_version: 1.0.0" "capability_version" +require_match "$pkg_out" "capability_version: 1.0.1" "capability_version" assert_execute() { local request="$1" @@ -53,7 +53,7 @@ assert_execute() { out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" printf '%s\n' "$out" require_match "$out" "status: completed" "$label status" - require_match "$out" "capability_version: 1.0.0" "$label capability_version" + require_match "$out" "capability_version: 1.0.1" "$label capability_version" require_match "$out" "\"valid\": $valid" "$label valid" require_match "$out" "\"reason_code\": \"$code\"" "$label reason_code" if [[ -n "$extra" ]]; then @@ -65,5 +65,7 @@ assert_execute "$pkg/runtime-requests/uc01-valid-item.json" "true" "ok" "UC-01" assert_execute "$pkg/runtime-requests/uc02-past-due.json" "false" "validation_failed" "UC-02" '"code": "past_due"' assert_execute "$pkg/runtime-requests/uc03-missing-owner.json" "false" "validation_failed" "UC-03" '"code": "missing_owner"' assert_execute "$pkg/runtime-requests/uc04-duplicate.json" "false" "duplicate" "UC-04" '"code": "duplicate"' +assert_execute "$pkg/runtime-requests/uc05-duplicate-title-only.json" "false" "duplicate" "UC-05" '"code": "duplicate"' +assert_execute "$pkg/runtime-requests/uc06-invalid-config.json" "false" "invalid_config" "UC-06" -echo "OK: core.validate-action-item 1.0.0 E2E smoke passed" +echo "OK: core.validate-action-item 1.0.1 E2E smoke passed" diff --git a/scripts/ci/repository_checks.sh b/scripts/ci/repository_checks.sh index 736d40ed..5db891f5 100644 --- a/scripts/ci/repository_checks.sh +++ b/scripts/ci/repository_checks.sh @@ -539,4 +539,7 @@ TRAVERSE_REPO_ROOT="$(pwd)" bash "$(pwd)/scripts/ci/wasi_host_abi_imports.sh" echo "Running app ownership boundary verification..." TRAVERSE_REPO_ROOT="$(pwd)" bash "$(pwd)/scripts/ci/app_ownership_boundary_smoke.sh" +echo "Running use_case ↔ smoke coverage verification (spec 102 FR-007)..." +TRAVERSE_REPO_ROOT="$(pwd)" bash "$(pwd)/scripts/ci/use_case_smoke_coverage_check.sh" + echo "Repository checks passed." diff --git a/scripts/ci/use_case_smoke_coverage_check.sh b/scripts/ci/use_case_smoke_coverage_check.sh new file mode 100755 index 00000000..9ec8a5a6 --- /dev/null +++ b/scripts/ci/use_case_smoke_coverage_check.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Spec 102 FR-007 / Decision 58: each use_cases[i] in examples/core-* packages +# must have a matching runtime-requests/ucNN-*.json fixture (1-based, zero-padded). + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$repo_root" + +failures=0 +checked=0 + +for contract in examples/core-*/contract.json; do + [[ -f "$contract" ]] || continue + package_dir="$(dirname "$contract")" + checked=$((checked + 1)) + + use_case_count="$( + python3 - "$contract" <<'PY' +import json, sys +contract = json.load(open(sys.argv[1], encoding="utf-8")) +use_cases = contract.get("use_cases") or [] +if not isinstance(use_cases, list): + raise SystemExit("use_cases must be an array") +print(len(use_cases)) +PY + )" + + if [[ "$use_case_count" -eq 0 ]]; then + echo "FAIL: $package_dir — use_cases missing or empty (spec 102 FR-004/FR-007)" + failures=$((failures + 1)) + continue + fi + + requests_dir="$package_dir/runtime-requests" + if [[ ! -d "$requests_dir" ]]; then + echo "FAIL: $package_dir — runtime-requests/ directory missing (spec 102 FR-007)" + failures=$((failures + 1)) + continue + fi + + for ((index = 1; index <= use_case_count; index++)); do + prefix="$(printf 'uc%02d-' "$index")" + matches=("$requests_dir"/"$prefix"*.json) + if [[ ! -e "${matches[0]}" ]]; then + echo "FAIL: $package_dir — use_cases[$((index - 1))] lacks runtime-requests/${prefix}*.json (spec 102 FR-007)" + failures=$((failures + 1)) + fi + done +done + +if [[ "$checked" -eq 0 ]]; then + echo "FAIL: no examples/core-*/contract.json packages found" + exit 1 +fi + +if [[ "$failures" -ne 0 ]]; then + echo "use_case smoke coverage check failed ($failures gap(s) across $checked package(s))" + exit 1 +fi + +echo "use_case smoke coverage check passed ($checked examples/core-* package(s))" diff --git a/specs/102-contract-surface-coverage/spec.md b/specs/102-contract-surface-coverage/spec.md index 2354d48c..5ea704b7 100644 --- a/specs/102-contract-surface-coverage/spec.md +++ b/specs/102-contract-surface-coverage/spec.md @@ -2,51 +2,63 @@ **Status**: Approved **Canonical governing ID**: `102-contract-surface-coverage` -**Version**: 1.0.0 +**Version**: 1.1.0 **Extends**: `002-capability-contracts`, `100-capability-package-authoring`, `056-capability-publish`, `516-agent-artifact-execution` -**Input**: Issues #1014–#1016; registry#192; Decision 57; ADR-0038. -**Incident**: `core.process-comment@1.0.0` overclaimed `action` enum values and description features beyond its use-case/smoke matrix. +**Input**: Issues #1014–#1016; #1040; registry#192; registry#215; Decision 57; Decision 58; ADR-0038. +**Incident**: `core.process-comment@1.0.0` overclaimed `action` enum values; Loop batch publish stripped `use_cases` from registry copies because publish serialized a `CapabilityContract` without that field. ## Purpose -Prevent capability contracts from declaring an input/behavior surface larger than what published `use_cases` demonstrate and what package-level verification exercises. Free-text `description` and JSON Schema enums are otherwise treated as marketing, while only use cases are executable promises. +Prevent capability contracts from declaring an input/behavior surface larger than what published `use_cases` demonstrate and what package-level verification exercises. Free-text `description` and JSON Schema enums are otherwise treated as marketing, while only use cases are executable promises. Coverage is measured against the **entire declared schema surface**, not a minimum use-case count. ## Capability Boundary In scope: -- Coverage rules relating `inputs.schema` discriminator enums (at minimum `action` when present) to `use_cases[].input_example` +- Coverage rules relating `inputs.schema` enums and required properties to `use_cases[].input_example` +- Coverage rules relating `outputs.schema` enums for `reason_code` and `status` (when declared) to `use_cases[].output_example` +- Requirement that `use_cases` is non-empty and preserved through `capability publish` into the registry record - Publish / dry-run failure behavior when coverage is incomplete -- Authoring-guide and example-smoke conventions for enum coverage -- Cross-repo expectation that registry validation may mirror the same rule for newly ADDED contracts +- Package smoke rule: each use case has a matching executable fixture +- Cross-repo expectation that registry validation mirrors the same rule for newly ADDED or CHANGED contracts Out of scope: -- NLP verification that every sentence in `description` is implemented (human honesty checklist only in v1) -- Rewriting immutable already-published registry versions +- NLP verification that every sentence in `description` is implemented (human honesty checklist + Known limitations only) +- Cartesian product coverage of all required-field combinations +- Rewriting immutable already-published registry versions in place (honesty patch-bumps instead) - Expanding Host ABI or guest runtime features -- Requiring 100% branch coverage of guest code beyond use-case smoke +- Requiring 100% branch coverage of guest code beyond the use-case smoke matrix ## Requirements -- **FR-001**: When a capability contract's `inputs.schema.properties.action` (or a future listed discriminator property) declares an `enum`, every enum value MUST appear as `use_cases[i].input_example.` for at least one use case. -- **FR-002**: `traverse-cli capability publish` and `capability publish --dry-run` MUST fail with an actionable error listing uncovered enum values when FR-001 is violated for the contract being published. -- **FR-003**: Governed example packages that declare an `action` enum MUST include smoke fixtures covering every retained enum value (same set as FR-001). -- **FR-004**: Capability authoring documentation MUST state the coverage rule and require an explicit **Known limitations** section whenever description prose mentions behavior not represented in `use_cases`. -- **FR-005**: Schema MUST NOT list an action (or discriminator value) that the executable artifact answers only with a generic `unsupported_action` / equivalent fail-closed stub unless that failure mode itself is a documented use case with a stable `reason_code`. +- **FR-001**: Every string `enum` value declared under `inputs.schema` (at any property path used as a closed vocabulary, including but not limited to `action`, `intensity`, `tone`, and nested config enums) MUST appear in at least one `use_cases[i].input_example` at the corresponding path. +- **FR-002**: Every property listed in `inputs.schema.required` MUST appear (with a concrete value) in at least one `use_cases[i].input_example`. Full cartesian coverage of required combinations is NOT required. +- **FR-003**: When `outputs.schema.properties.reason_code` or `outputs.schema.properties.status` declares an `enum`, every enum value MUST appear in at least one `use_cases[i].output_example` at that field. Authors who need checkable failure/success vocabulary MUST declare these as enums (free-string `reason_code` is not coverage-checkable and MUST NOT be used to evade FR-003). +- **FR-004**: `use_cases` MUST be a non-empty array on every contract submitted to `capability publish` / `--dry-run` and on every newly ADDED or CHANGED registry `contract.json`. +- **FR-005**: `traverse-cli capability publish` MUST preserve `use_cases` (and author-supplied `evidence`) in the registry-bound contract JSON. Round-tripping through normalization MUST NOT strip fields required by this spec. +- **FR-006**: `traverse-cli capability publish` and `capability publish --dry-run` MUST fail with an actionable error listing uncovered enum values, missing required properties, missing output enum coverage, or empty `use_cases` when FR-001–FR-004 are violated. +- **FR-007**: Governed example / capability packages MUST include a smoke fixture for **each** `use_cases[]` entry that exercises that use case and asserts its `reason_code` (and other key outputs named in the use case's `output_example`). +- **FR-008**: Capability authoring documentation MUST state the coverage rule and require an explicit **Known limitations** section whenever description prose mentions behavior not represented in `use_cases`. +- **FR-009**: Schema MUST NOT list an enum value that the executable artifact answers only with a generic `unsupported_*` / equivalent fail-closed stub unless that failure mode itself is a documented use case with a stable `reason_code`. +- **FR-010**: Already-published registry versions that lack `use_cases` or fail FR-001–FR-003 MUST be corrected by an honesty patch-bump (new immutable version), not by editing the published file in place. ## Success Criteria -- A contract that includes `resolve` in `action.enum` but has no resolve use case fails publish dry-run. -- `core.process-comment@1.0.1` (honesty bump) satisfies FR-001 for its retained enum. -- Registry can adopt an equivalent diff-based check for newly ADDED contracts without rewriting history. +- A contract that declares an input enum value with no covering use case fails publish dry-run. +- A contract with empty or missing `use_cases` fails publish dry-run. +- Publishing a contract that has use cases locally results in a registry PR whose `contract.json` still contains those use cases. +- A governed example package with a use case but no matching smoke fixture fails the package/smoke gate. +- Registry CI rejects newly ADDED/CHANGED contracts that violate FR-001–FR-004. +- Stripped Loop capabilities are honesty-bumped under FR-010. ## Quality Gates -- QG-001: Unit tests for the publish coverage checker (pass/fail fixtures). -- QG-002: Spec-alignment maps this spec onto CLI publish paths and authoring docs once Approved. +- QG-001: Unit tests for the publish coverage checker (pass/fail fixtures) covering enums, required props, output reason_code/status, empty use_cases, and preserve-on-publish. +- QG-002: Spec-alignment maps this spec onto CLI publish paths, package smoke conventions, and authoring docs once Approved. - QG-003: No silent weakening of host input-schema validation. +- QG-004: Registry mirror tests reject missing use_cases on the new/changed-contract path. ## Approval Note -Approved 2026-08-08 (owner direction). Registered in `specs/governance/approved-specs.json`. Implementation: #1016 / registry#192. +v1.0.0 Approved 2026-08-08 (owner direction). v1.1.0 Approved 2026-08-10 (owner direction on Decision 58 brainstorm closeout — full schema ⊆ use_cases ⊆ smoke gate). diff --git a/specs/governance/approved-specs.json b/specs/governance/approved-specs.json index 3b736b44..e7d67104 100644 --- a/specs/governance/approved-specs.json +++ b/specs/governance/approved-specs.json @@ -1328,7 +1328,7 @@ }, { "id": "102-contract-surface-coverage", - "version": "1.0.0", + "version": "1.1.0", "status": "approved", "immutable": true, "path": "specs/102-contract-surface-coverage/spec.md",