diff --git a/.changeset/envelope-field-present-check-type.md b/.changeset/envelope-field-present-check-type.md new file mode 100644 index 0000000000..bea0498541 --- /dev/null +++ b/.changeset/envelope-field-present-check-type.md @@ -0,0 +1,5 @@ +--- +"adcontextprotocol": patch +--- + +Add `envelope_field_present` check type to the storyboard schema and update `v3-envelope-integrity.yaml` to use it for the `status` presence assertion. The new check type walks `protocol-envelope.json` rather than the step's `response_schema_ref`, eliminating the static-analysis `VERIFIER_UNREACHABLE` gap in adcp-client's storyboard-drift verifier. Requires adcp-client#1045. diff --git a/scripts/lint-storyboard-contradictions.cjs b/scripts/lint-storyboard-contradictions.cjs index b945c62ba9..09133965b1 100644 --- a/scripts/lint-storyboard-contradictions.cjs +++ b/scripts/lint-storyboard-contradictions.cjs @@ -445,15 +445,16 @@ function classifyOutcome(step) { return { kind: 'error', codes }; } - // Looks like a success assertion path (field_present, response_schema, - // field_value on happy-path fields). Distinguish from "unspecified" — - // we need at least one positive assertion to call it success. + // Looks like a success assertion path (field_present, envelope_field_present, + // response_schema, field_value on happy-path fields). Distinguish from + // "unspecified" — we need at least one positive assertion to call it success. const hasPositiveAssertion = validations.some((v) => { if (!v || typeof v !== 'object') return false; const check = v.check; return ( check === 'response_schema' || check === 'field_present' || + check === 'envelope_field_present' || check === 'field_value' || check === 'field_value_or_absent' || check === 'http_status' || diff --git a/static/compliance/source/universal/runner-output-contract.yaml b/static/compliance/source/universal/runner-output-contract.yaml index b498a87255..f78c83da50 100644 --- a/static/compliance/source/universal/runner-output-contract.yaml +++ b/static/compliance/source/universal/runner-output-contract.yaml @@ -70,7 +70,8 @@ validation_result: field carries the transport failure). required_fields: - check # Validation kind from storyboard-schema.yaml: - # response_schema | field_present | field_value | + # response_schema | field_present | + # envelope_field_present | field_value | # field_value_or_absent | status_code | http_status | # http_status_in | error_code | on_401_require_header | # resource_equals_agent_url | any_of | refs_resolve @@ -91,6 +92,8 @@ validation_result: # field_value_or_absent → value or allowed_values array # from the storyboard validation # field_present → the path that should resolve + # envelope_field_present → the path that should resolve + # in the protocol envelope # status_code → expected status # error_code → expected error code # any_of → array of acceptable forms @@ -105,6 +108,9 @@ validation_result: # field_present → null (the field was missing) # or the observed non-object # value + # envelope_field_present → null (the field was missing) + # or the observed non-object + # value (scoped to envelope) - schema_id # $id of the response schema applied. Required # when check == response_schema, null otherwise. - schema_url # Resolvable URL the implementor can fetch to diff --git a/static/compliance/source/universal/storyboard-schema.yaml b/static/compliance/source/universal/storyboard-schema.yaml index a63a206387..dc2208fbfe 100644 --- a/static/compliance/source/universal/storyboard-schema.yaml +++ b/static/compliance/source/universal/storyboard-schema.yaml @@ -575,7 +575,10 @@ # # --- Validation --- # -# check: string (what to validate: "response_schema", "field_present", "field_value", +# check: string (what to validate: "response_schema", "field_present", +# "envelope_field_present" (walks protocol-envelope.json instead of +# response_schema_ref — use for top-level envelope fields like `status`), +# "field_value", # "field_value_or_absent", "status_code", "http_status", "http_status_in", # "error_code", "on_401_require_header", "resource_equals_agent_url", # "any_of", "refs_resolve", "a2a_submitted_artifact") diff --git a/static/compliance/source/universal/v3-envelope-integrity.yaml b/static/compliance/source/universal/v3-envelope-integrity.yaml index 6205dceff7..f78bcc8851 100644 --- a/static/compliance/source/universal/v3-envelope-integrity.yaml +++ b/static/compliance/source/universal/v3-envelope-integrity.yaml @@ -15,7 +15,9 @@ narrative: | docs/building/implementation/task-lifecycle.mdx. This storyboard documents the machine-check assertion and asserts the canonical v3 - `status` field is present. The explicit envelope-root field-absence checks + `status` field is present using the `envelope_field_present` check type (added in + adcp-client#1045), which walks the protocol envelope rather than the inner response + schema. The explicit envelope-root field-absence checks (asserting task_status and response_status are absent) require `field_absent` runner support in @adcp/client — those checks are marked TODO below and are not executed until the runner ships that check type. The schema-level constraint @@ -82,7 +84,7 @@ phases: validations: - check: response_schema description: "Response matches get-adcp-capabilities-response.json schema" - - check: field_present + - check: envelope_field_present path: "status" description: "Envelope carries the canonical v3 status field" # TODO: restore these as field_absent checks once @adcp/client runner ships diff --git a/tests/lint-storyboard-contradictions.test.cjs b/tests/lint-storyboard-contradictions.test.cjs index 2a9d8bb04a..0a9debaf8f 100644 --- a/tests/lint-storyboard-contradictions.test.cjs +++ b/tests/lint-storyboard-contradictions.test.cjs @@ -164,6 +164,16 @@ test('classifyOutcome extracts error_code allowed_values as a Set', () => { assert.deepEqual([...outcome.codes].sort(), ['A', 'B']); }); +test('classifyOutcome treats envelope_field_present as success assertion', () => { + const step = { + validations: [ + { check: 'envelope_field_present', path: 'status' }, + ], + }; + const outcome = classifyOutcome(step); + assert.equal(outcome.kind, 'success'); +}); + test('outcomesAgree: error sets with overlap agree, disjoint disagree', () => { const a = { kind: 'error', codes: new Set(['X', 'Y']) }; const b = { kind: 'error', codes: new Set(['Y', 'Z']) };