From 5c4552f1edc6796114cbf9da0f6ab8c38bc68650 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Wed, 12 Aug 2026 12:48:40 +0800 Subject: [PATCH 1/4] Improve PatchBodyParametersSchema lint parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7054e94e-5ed2-43ae-9bca-490a4203c0a7 --- .../src/rules/patch-body-parameters-schema.ts | 102 +++-- .../create-only-patch-property/expect.json | 3 + .../create-only-patch-property/main.tsp | 66 +++ .../create-only-patch-property/output.json | 383 +++++++++++++++++ .../tsp-diagnostics.json | 82 ++++ .../validator-diagnostics.json | 17 + .../default-patch-property/expect.json | 3 + .../default-patch-property/main.tsp | 65 +++ .../default-patch-property/output.json | 381 +++++++++++++++++ .../tsp-diagnostics.json | 82 ++++ .../validator-diagnostics.json | 17 + .../PatchBodyParametersSchema/migration.md | 129 ++++++ .../PatchBodyParametersSchema/rule.md | 19 +- .../top-level-identity-compliant/expect.json | 29 ++ .../top-level-identity-compliant/main.tsp | 68 +++ .../top-level-identity-compliant/output.json | 390 ++++++++++++++++++ .../tsp-diagnostics.json | 77 ++++ .../validator-diagnostics.json | 1 + 18 files changed, 1852 insertions(+), 62 deletions(-) create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/expect.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/main.tsp create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/output.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/tsp-diagnostics.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/validator-diagnostics.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/expect.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/main.tsp create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/output.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/tsp-diagnostics.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/validator-diagnostics.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/expect.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/main.tsp create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/output.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/tsp-diagnostics.json create mode 100644 packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/validator-diagnostics.json diff --git a/packages/typespec-lintdiff/src/rules/patch-body-parameters-schema.ts b/packages/typespec-lintdiff/src/rules/patch-body-parameters-schema.ts index d861b1af00..4b258076a3 100644 --- a/packages/typespec-lintdiff/src/rules/patch-body-parameters-schema.ts +++ b/packages/typespec-lintdiff/src/rules/patch-body-parameters-schema.ts @@ -1,29 +1,29 @@ +import { resolveProviderNamespace } from "@azure-tools/typespec-azure-resource-manager"; import { createRule, + getLifecycleVisibilityEnum, + getVisibilityForClass, paramMessage, type Model, type ModelProperty, - type Operation, + type Program, } from "@typespec/compiler"; -import { resolveProviderNamespace } from "@azure-tools/typespec-azure-resource-manager"; import { getHttpOperation } from "@typespec/http"; export const patchBodyParametersSchemaRule = createRule({ name: "patch-body-parameters-schema", - description: - "ARM PATCH body properties must not be required and must not have defaults.", + description: "ARM PATCH body properties must not be required and must not have defaults.", severity: "warning", messages: { required: paramMessage`Properties of a PATCH request body must not be required, property:${"propertyName"}.`, default: paramMessage`Properties of a PATCH request body must not have default value, property:${"propertyName"}.`, + createOnly: paramMessage`Properties of a PATCH request body must not be x-ms-mutability: ["create"], property:${"propertyName"}.`, }, create(context) { return { operation: (operation) => { const namespace = operation.interface?.namespace ?? operation.namespace; - if ( - resolveProviderNamespace(context.program, namespace) === undefined - ) { + if (resolveProviderNamespace(context.program, namespace) === undefined) { return; } @@ -37,7 +37,7 @@ export const patchBodyParametersSchemaRule = createRule({ return; } - for (const violation of findViolations(operation, patchBody)) { + for (const violation of findViolations(context.program, patchBody)) { context.reportDiagnostic({ target: violation.target, messageId: violation.messageId, @@ -54,18 +54,18 @@ export const patchBodyParametersSchemaRule = createRule({ type Violation = { target: ModelProperty; propertyName: string; - messageId: "required" | "default"; + messageId: "required" | "default" | "createOnly"; }; -function findViolations(operation: Operation, patchModel: Model): Violation[] { +function findViolations(program: Program, patchModel: Model): Violation[] { const violations: Violation[] = []; - collectViolations(patchModel, operation.name, violations, [], new Set()); + collectViolations(program, patchModel, violations, [], new Set()); return violations; } function collectViolations( + program: Program, model: Model, - resourceName: string, violations: Violation[], path: string[] = [], visited: Set = new Set(), @@ -77,65 +77,59 @@ function collectViolations( for (const property of getModelProperties(model)) { const propertyPath = [...path, property.name]; - if ( - !isTopLevelManagedIdentityException(resourceName, propertyPath, property) - ) { - if (!property.optional) { - violations.push({ - target: property, - propertyName: propertyPath.join("."), - messageId: "required", - }); - } + if (isTopLevelIdentityProperty(propertyPath)) { + continue; + } - if (property.defaultValue !== undefined) { - violations.push({ - target: property, - propertyName: propertyPath.join("."), - messageId: "default", - }); - } + if (!property.optional) { + violations.push({ + target: property, + propertyName: propertyPath.join("."), + messageId: "required", + }); + } + + if (property.defaultValue !== undefined) { + violations.push({ + target: property, + propertyName: propertyPath.join("."), + messageId: "default", + }); + } + + if (isCreateOnlyMutability(program, property)) { + violations.push({ + target: property, + propertyName: propertyPath.join("."), + messageId: "createOnly", + }); } if (property.type.kind === "Model") { - collectViolations( - property.type, - resourceName, - violations, - propertyPath, - visited, - ); + collectViolations(program, property.type, violations, propertyPath, visited); } } } -function isTopLevelManagedIdentityException( - resourceName: string, - propertyPath: string[], - property: ModelProperty, -): boolean { - if (propertyPath.length !== 1 || property.name !== "identity") { - return false; - } +function isTopLevelIdentityProperty(propertyPath: string[]): boolean { + return propertyPath.length === 1 && propertyPath[0].toLowerCase() === "identity"; +} - if (property.type.kind !== "Model") { +function isCreateOnlyMutability(program: Program, property: ModelProperty): boolean { + const lifecycle = getLifecycleVisibilityEnum(program); + const create = lifecycle.members.get("Create"); + if (create === undefined) { return false; } - return ( - property.type.name.includes("ManagedServiceIdentity") || - property.type.name.includes("SystemAssignedServiceIdentity") - ); + const visibility = getVisibilityForClass(program, property, lifecycle); + return visibility.size === 1 && visibility.has(create); } function getModelProperties(model: Model): ModelProperty[] { const properties = new Map(); - for ( - let current: Model | undefined = model; - current !== undefined; - current = current.baseModel - ) { + for (let current: Model | undefined = model; current !== undefined; current = current.baseModel) { for (const property of current.properties.values()) { if (!properties.has(property.name)) { properties.set(property.name, property); diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/expect.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/expect.json new file mode 100644 index 0000000000..ba21e936b5 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/expect.json @@ -0,0 +1,3 @@ +{ + "violation": true +} diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/main.tsp b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/main.tsp new file mode 100644 index 0000000000..1721f25206 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/main.tsp @@ -0,0 +1,66 @@ +import "../../lib/imports.tsp"; + +using TypeSpec.Http; +using TypeSpec.Rest; +using TypeSpec.Versioning; +using Azure.ResourceManager; + +@armProviderNamespace +@service(#{ title: "Test Service" }) +@versioned(Versions) +@armCommonTypesVersion(CommonTypes.Versions.v5) +namespace Microsoft.TestService; + +enum Versions { + @useDependency(Azure.ResourceManager.CommonTypes.Versions.v5) + v2024_01_01: "2024-01-01", +} + +model Widget is TrackedResource { + @key("widgetName") + @segment("widgets") + @doc("The name of the widget") + @path + @pattern("^[a-zA-Z0-9_-]+$") + name: string; +} + +@doc("Widget resource properties.") +model WidgetProperties { + @doc("Description of the widget") + description?: string; + + @doc("Resource provisioning state") + @visibility(Lifecycle.Read) + provisioningState?: ResourceProvisioningState; +} + +@doc("Patch envelope for widget.") +model WidgetPatchBody { + @doc("Resource properties with a create-only field") + properties?: WidgetPatchProperties; +} + +@doc("Patch properties with a create-only property.") +model WidgetPatchProperties { + @doc("Create-only property in patch body -- violates rule") + @visibility(Lifecycle.Create) + createOnly?: string; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Widgets { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + delete is ArmResourceDeleteWithoutOkAsync; + + @doc("Update widget") + @patch + @armResourceUpdate(Widget) + update( + ...ResourceInstanceParameters, + @doc("The request body") @body body: WidgetPatchBody, + ): ArmResponse | ErrorResponse; +} diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/output.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/output.json new file mode 100644 index 0000000000..55edfae0c3 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/output.json @@ -0,0 +1,383 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "2024-01-01", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "host": "management.azure.com", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "description": "Azure Active Directory OAuth2 Flow.", + "flow": "implicit", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "tags": [ + { + "name": "Operations" + }, + { + "name": "Widgets" + } + ], + "paths": { + "/providers/Microsoft.TestService/operations": { + "get": { + "operationId": "Operations_List", + "tags": [ + "Operations" + ], + "description": "List the operations for the provider", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/OperationListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.TestService/widgets/{widgetName}": { + "get": { + "operationId": "Widgets_Get", + "tags": [ + "Widgets" + ], + "description": "Get a Widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/Widget" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "operationId": "Widgets_CreateOrUpdate", + "tags": [ + "Widgets" + ], + "description": "Create a Widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + { + "name": "resource", + "in": "body", + "description": "Resource create parameters.", + "required": true, + "schema": { + "$ref": "#/definitions/Widget" + } + } + ], + "responses": { + "200": { + "description": "Resource 'Widget' update operation succeeded", + "schema": { + "$ref": "#/definitions/Widget" + } + }, + "201": { + "description": "Resource 'Widget' create operation succeeded", + "schema": { + "$ref": "#/definitions/Widget" + }, + "headers": { + "Azure-AsyncOperation": { + "type": "string", + "description": "A link to the status monitor" + }, + "Retry-After": { + "type": "integer", + "format": "int32", + "description": "The Retry-After header can indicate how long the client should wait before polling the operation status." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-long-running-operation": true + }, + "patch": { + "operationId": "Widgets_Update", + "tags": [ + "Widgets" + ], + "description": "Update widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + { + "name": "body", + "in": "body", + "description": "The request body", + "required": true, + "schema": { + "$ref": "#/definitions/WidgetPatchBody" + } + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/Widget" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "operationId": "Widgets_Delete", + "tags": [ + "Widgets" + ], + "description": "Delete a Widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + } + ], + "responses": { + "202": { + "description": "Resource deletion accepted.", + "headers": { + "Location": { + "type": "string", + "description": "The Location header contains the URL where the status of the long running operation can be checked." + }, + "Retry-After": { + "type": "integer", + "format": "int32", + "description": "The Retry-After header can indicate how long the client should wait before polling the operation status." + } + } + }, + "204": { + "description": "Resource does not exist." + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-long-running-operation": true + } + } + }, + "definitions": { + "Azure.ResourceManager.ResourceProvisioningState": { + "type": "string", + "description": "The provisioning state of a resource type.", + "enum": [ + "Succeeded", + "Failed", + "Canceled" + ], + "x-ms-enum": { + "name": "ResourceProvisioningState", + "modelAsString": true, + "values": [ + { + "name": "Succeeded", + "value": "Succeeded", + "description": "Resource has been created." + }, + { + "name": "Failed", + "value": "Failed", + "description": "Resource creation failed." + }, + { + "name": "Canceled", + "value": "Canceled", + "description": "Resource creation was canceled." + } + ] + } + }, + "Widget": { + "type": "object", + "description": "Concrete tracked resource types can be created by aliasing this type using a specific property type.", + "properties": { + "properties": { + "$ref": "#/definitions/WidgetProperties", + "description": "The resource-specific properties for this resource." + } + }, + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/TrackedResource" + } + ] + }, + "WidgetPatchBody": { + "type": "object", + "description": "Patch envelope for widget.", + "properties": { + "properties": { + "$ref": "#/definitions/WidgetPatchProperties", + "description": "Resource properties with a create-only field" + } + } + }, + "WidgetPatchProperties": { + "type": "object", + "description": "Patch properties with a create-only property.", + "properties": { + "createOnly": { + "type": "string", + "description": "Create-only property in patch body -- violates rule", + "x-ms-mutability": [ + "create" + ] + } + } + }, + "WidgetProperties": { + "type": "object", + "description": "Widget resource properties.", + "properties": { + "description": { + "type": "string", + "description": "Description of the widget" + }, + "provisioningState": { + "$ref": "#/definitions/Azure.ResourceManager.ResourceProvisioningState", + "description": "Resource provisioning state", + "readOnly": true + } + } + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/tsp-diagnostics.json new file mode 100644 index 0000000000..90f1d8e832 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/tsp-diagnostics.json @@ -0,0 +1,82 @@ +[ + { + "code": "tsp-lintdiff-local-linter/latest-version-of-common-types-must-be-used", + "severity": "warning", + "message": "Use the latest ARM common-types version 'v6' instead of 'v5'." + }, + { + "code": "tsp-lintdiff-local-linter/consistent-patch-properties", + "severity": "warning", + "message": "The property 'properties.createOnly' in the request body either does not appear in the resource model or is nested at the wrong level." + }, + { + "code": "tsp-lintdiff-local-linter/top-level-resources-list-by-resource-group", + "severity": "warning", + "message": "Top-level resource 'Widget' should define a list by resource group operation." + }, + { + "code": "@azure-tools/typespec-azure-resource-manager/patch-envelope", + "severity": "warning", + "message": "The Resource PATCH request for resource 'Widget' is missing envelope properties: [tags]. Since these properties are supported in the resource, they must also be updatable via PATCH." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/patch-body-parameters-schema", + "severity": "warning", + "message": "Properties of a PATCH request body must not be x-ms-mutability: [\"create\"], property:properties.createOnly." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/validator-diagnostics.json new file mode 100644 index 0000000000..3c59e85fde --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/create-only-patch-property/validator-diagnostics.json @@ -0,0 +1,17 @@ +[ + { + "code": "PatchBodyParametersSchema", + "message": "Properties of a PATCH request body must not be x-ms-mutability: [\"create\"], property:createOnly.", + "path": [ + "paths", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.TestService/widgets/{widgetName}", + "patch", + "parameters", + "4", + "schema", + "properties", + "properties" + ], + "severity": 0 + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/expect.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/expect.json new file mode 100644 index 0000000000..ba21e936b5 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/expect.json @@ -0,0 +1,3 @@ +{ + "violation": true +} diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/main.tsp b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/main.tsp new file mode 100644 index 0000000000..2c46c259a2 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/main.tsp @@ -0,0 +1,65 @@ +import "../../lib/imports.tsp"; + +using TypeSpec.Http; +using TypeSpec.Rest; +using TypeSpec.Versioning; +using Azure.ResourceManager; + +@armProviderNamespace +@service(#{ title: "Test Service" }) +@versioned(Versions) +@armCommonTypesVersion(CommonTypes.Versions.v5) +namespace Microsoft.TestService; + +enum Versions { + @useDependency(Azure.ResourceManager.CommonTypes.Versions.v5) + v2024_01_01: "2024-01-01", +} + +model Widget is TrackedResource { + @key("widgetName") + @segment("widgets") + @doc("The name of the widget") + @path + @pattern("^[a-zA-Z0-9_-]+$") + name: string; +} + +@doc("Widget resource properties.") +model WidgetProperties { + @doc("Description of the widget") + description?: string; + + @doc("Resource provisioning state") + @visibility(Lifecycle.Read) + provisioningState?: ResourceProvisioningState; +} + +@doc("Patch envelope for widget.") +model WidgetPatchBody { + @doc("Resource properties with a defaulted field") + properties?: WidgetPatchProperties; +} + +@doc("Patch properties with a defaulted property.") +model WidgetPatchProperties { + @doc("Mode default in patch body -- violates rule") + mode?: string = "enabled"; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Widgets { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + delete is ArmResourceDeleteWithoutOkAsync; + + @doc("Update widget") + @patch + @armResourceUpdate(Widget) + update( + ...ResourceInstanceParameters, + @doc("The request body") @body body: WidgetPatchBody, + ): ArmResponse | ErrorResponse; +} diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/output.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/output.json new file mode 100644 index 0000000000..0e1429b4a9 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/output.json @@ -0,0 +1,381 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "2024-01-01", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "host": "management.azure.com", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "description": "Azure Active Directory OAuth2 Flow.", + "flow": "implicit", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "tags": [ + { + "name": "Operations" + }, + { + "name": "Widgets" + } + ], + "paths": { + "/providers/Microsoft.TestService/operations": { + "get": { + "operationId": "Operations_List", + "tags": [ + "Operations" + ], + "description": "List the operations for the provider", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/OperationListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.TestService/widgets/{widgetName}": { + "get": { + "operationId": "Widgets_Get", + "tags": [ + "Widgets" + ], + "description": "Get a Widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/Widget" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "operationId": "Widgets_CreateOrUpdate", + "tags": [ + "Widgets" + ], + "description": "Create a Widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + { + "name": "resource", + "in": "body", + "description": "Resource create parameters.", + "required": true, + "schema": { + "$ref": "#/definitions/Widget" + } + } + ], + "responses": { + "200": { + "description": "Resource 'Widget' update operation succeeded", + "schema": { + "$ref": "#/definitions/Widget" + } + }, + "201": { + "description": "Resource 'Widget' create operation succeeded", + "schema": { + "$ref": "#/definitions/Widget" + }, + "headers": { + "Azure-AsyncOperation": { + "type": "string", + "description": "A link to the status monitor" + }, + "Retry-After": { + "type": "integer", + "format": "int32", + "description": "The Retry-After header can indicate how long the client should wait before polling the operation status." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-long-running-operation": true + }, + "patch": { + "operationId": "Widgets_Update", + "tags": [ + "Widgets" + ], + "description": "Update widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + { + "name": "body", + "in": "body", + "description": "The request body", + "required": true, + "schema": { + "$ref": "#/definitions/WidgetPatchBody" + } + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/Widget" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "operationId": "Widgets_Delete", + "tags": [ + "Widgets" + ], + "description": "Delete a Widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + } + ], + "responses": { + "202": { + "description": "Resource deletion accepted.", + "headers": { + "Location": { + "type": "string", + "description": "The Location header contains the URL where the status of the long running operation can be checked." + }, + "Retry-After": { + "type": "integer", + "format": "int32", + "description": "The Retry-After header can indicate how long the client should wait before polling the operation status." + } + } + }, + "204": { + "description": "Resource does not exist." + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-long-running-operation": true + } + } + }, + "definitions": { + "Azure.ResourceManager.ResourceProvisioningState": { + "type": "string", + "description": "The provisioning state of a resource type.", + "enum": [ + "Succeeded", + "Failed", + "Canceled" + ], + "x-ms-enum": { + "name": "ResourceProvisioningState", + "modelAsString": true, + "values": [ + { + "name": "Succeeded", + "value": "Succeeded", + "description": "Resource has been created." + }, + { + "name": "Failed", + "value": "Failed", + "description": "Resource creation failed." + }, + { + "name": "Canceled", + "value": "Canceled", + "description": "Resource creation was canceled." + } + ] + } + }, + "Widget": { + "type": "object", + "description": "Concrete tracked resource types can be created by aliasing this type using a specific property type.", + "properties": { + "properties": { + "$ref": "#/definitions/WidgetProperties", + "description": "The resource-specific properties for this resource." + } + }, + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/TrackedResource" + } + ] + }, + "WidgetPatchBody": { + "type": "object", + "description": "Patch envelope for widget.", + "properties": { + "properties": { + "$ref": "#/definitions/WidgetPatchProperties", + "description": "Resource properties with a defaulted field" + } + } + }, + "WidgetPatchProperties": { + "type": "object", + "description": "Patch properties with a defaulted property.", + "properties": { + "mode": { + "type": "string", + "description": "Mode default in patch body -- violates rule", + "default": "enabled" + } + } + }, + "WidgetProperties": { + "type": "object", + "description": "Widget resource properties.", + "properties": { + "description": { + "type": "string", + "description": "Description of the widget" + }, + "provisioningState": { + "$ref": "#/definitions/Azure.ResourceManager.ResourceProvisioningState", + "description": "Resource provisioning state", + "readOnly": true + } + } + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/tsp-diagnostics.json new file mode 100644 index 0000000000..aa64220f17 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/tsp-diagnostics.json @@ -0,0 +1,82 @@ +[ + { + "code": "tsp-lintdiff-local-linter/latest-version-of-common-types-must-be-used", + "severity": "warning", + "message": "Use the latest ARM common-types version 'v6' instead of 'v5'." + }, + { + "code": "tsp-lintdiff-local-linter/consistent-patch-properties", + "severity": "warning", + "message": "The property 'properties.mode' in the request body either does not appear in the resource model or is nested at the wrong level." + }, + { + "code": "tsp-lintdiff-local-linter/top-level-resources-list-by-resource-group", + "severity": "warning", + "message": "Top-level resource 'Widget' should define a list by resource group operation." + }, + { + "code": "@azure-tools/typespec-azure-resource-manager/patch-envelope", + "severity": "warning", + "message": "The Resource PATCH request for resource 'Widget' is missing envelope properties: [tags]. Since these properties are supported in the resource, they must also be updatable via PATCH." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/patch-body-parameters-schema", + "severity": "warning", + "message": "Properties of a PATCH request body must not have default value, property:properties.mode." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/validator-diagnostics.json new file mode 100644 index 0000000000..7372181802 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/default-patch-property/validator-diagnostics.json @@ -0,0 +1,17 @@ +[ + { + "code": "PatchBodyParametersSchema", + "message": "Properties of a PATCH request body must not have default value, property:mode.", + "path": [ + "paths", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.TestService/widgets/{widgetName}", + "patch", + "parameters", + "4", + "schema", + "properties", + "properties" + ], + "severity": 0 + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md new file mode 100644 index 0000000000..a027ca4516 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md @@ -0,0 +1,129 @@ +# PatchBodyParametersSchema migration evidence + +## Conclusion + +The migrated TypeSpec rule required an update. The Swagger rule rejects three PATCH body property shapes: required properties, default-valued properties, and properties whose emitted `x-ms-mutability` is exactly `["create"]`. The previous TypeSpec rule covered required and default-valued properties, but missed create-only lifecycle visibility and was narrower than Swagger's top-level `identity` exception. + +This change adds the create-only branch and mirrors Swagger's unconditional skip for a top-level PATCH body property named `identity`. The rule is closer to functional parity, but it remains classified as **partial** because the latest full corpus still has four validator-only projects and 34 TypeSpec-only projects that need project-specific explanation before claiming complete equivalence. Raw diagnostic equality is not expected because Swagger reports emitted OpenAPI occurrences and TypeSpec reports semantic source properties. + +## Required TypeSpec changes + +- Production rule: `src/rules/patch-body-parameters-schema.ts` + - report a warning when `@visibility(Lifecycle.Create)` emits `x-ms-mutability: ["create"]`; + - skip top-level `identity` before checking or recursing, matching the Swagger implementation. +- Fixtures: + - `required-patch-property`: existing required-property violation; + - `default-patch-property`: default-valued PATCH property violation; + - `create-only-patch-property`: create-only lifecycle visibility violation; + - `top-level-identity-compliant`: validator-clean top-level `identity` regression. + +## Reports reconciled + +| Report | Source | Population | PatchBodyParametersSchema row | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| External snapshot | `docs/coverage_old.md`, source `https://gist.github.com/catalinaperalta/b2e7d29a33b4b451bcfcc87e8314565a` | 450 compiled projects, 210 validator rules | `partial`; validator fired 87 projects; local lint fired 85; official 0; 97.7% | +| Local lint-diff corpus | `specs/coverage-breakdown.md`, specs commit `f6b53f105b95da05276530a0754a1c71b4f16397`, generated `2026-08-12T04:34:01.330Z` by `test/harness/typespec-results.ts`/coverage refresh | full run, 462/468 successful TypeSpec projects | `production`, `partial`; validator fired 93 projects; TypeSpec fired 123; same-project overlap 89; validator-only 4; TypeSpec-only 34; validator diagnostics 703; TypeSpec diagnostics 1243 | + +The report differences are caused by different snapshots and coverage definitions. The external snapshot credits aggregate local-lint project coverage over 450 compiled projects and does not provide one-sided project lists. The lint-diff report uses the pinned specs dataset, excludes TypeSpec compile failures from both sides, and credits only same-project observed diagnostics. + +## Aligned project sets + +- Validator projects: 93 +- TypeSpec projects: 123 +- Same-project overlap: 89 +- Validator-only projects: + - `specification/authorization/resource-manager/Microsoft.Authorization/Authorization/AccessReview` + - `specification/confidentialledger/resource-manager/Microsoft.ConfidentialLedger/ConfidentialLedger` + - `specification/devcenter/resource-manager/Microsoft.DevCenter/DevCenter` + - `specification/hybridcompute/resource-manager/Microsoft.HybridCompute/HybridCompute` +- TypeSpec-only projects: + - `specification/apicenter/ApiCenter.Management` + - `specification/applink/AppLink.Management` + - `specification/azuredatatransfer/resource-manager/Microsoft.AzureDataTransfer/AzureDataTransfer` + - `specification/azureresiliencemanagement/resource-manager/Microsoft.AzureResilienceManagement/AzureResilienceManagement` + - `specification/billingbenefits/resource-manager/Microsoft.BillingBenefits/BillingBenefits` + - `specification/cloudhealth/resource-manager/Microsoft.CloudHealth/CloudHealth` + - `specification/computeschedule/resource-manager/Microsoft.ComputeSchedule/ComputeSchedule` + - `specification/containerservice/resource-manager/Microsoft.ContainerService/fleet` + - `specification/databasewatcher/resource-manager/Microsoft.DatabaseWatcher/DatabaseWatcher` + - `specification/discovery/Discovery.Management` + - `specification/edge/resource-manager/Microsoft.Edge/configurationmanager` + - `specification/edge/resource-manager/Microsoft.Edge/configurations` + - `specification/edge/resource-manager/Microsoft.Edge/disconnectedOperations` + - `specification/github-network/GitHub.Network.Management` + - `specification/imagebuilder/resource-manager/Microsoft.VirtualMachineImages/ImageBuilder` + - `specification/impact/Impact.Management` + - `specification/informatica/resource-manager/Informatica.DataManagement/Informatica` + - `specification/kubernetesconfiguration/resource-manager/Microsoft.KubernetesConfiguration/fluxConfigurations` + - `specification/liftrastronomer/resource-manager/Astronomer.Astro/AstronomerAstro` + - `specification/liftrmongodb/MongoDB.Atlas.Management` + - `specification/manufacturingplatform/Manufacturingplatform.Management` + - `specification/migrate/resource-manager/Microsoft.Migrate/AssessmentProjects` + - `specification/mission/resource-manager/Microsoft.Mission/Mission` + - `specification/monitoringservice/resource-manager/Microsoft.Monitor/PipelineGroups` + - `specification/onlineexperimentation/OnlineExperimentation.Management` + - `specification/oracle/resource-manager/Oracle.Database/OracleDatabase` + - `specification/postgresql/DBforPostgreSQL.Management` + - `specification/programmableconnectivity/ProgrammableConnectivity.Management` + - `specification/purestorage/resource-manager/PureStorage.Block/PureStorageBlock` + - `specification/reservations/resource-manager/Microsoft.Capacity/Reservations/Reservations` + - `specification/servicenetworking/resource-manager/Microsoft.ServiceNetworking/ServiceNetworking` + - `specification/sovereign/resource-manager/Microsoft.Sovereign/Sovereign` + - `specification/splitio/SplitIO.Experimentation.Management` + - `specification/workloads/Workloads.SAPMonitor.Management` + +The four validator-only projects are still real assessment gaps in the latest corpus. Sample validator findings are nested required discriminator/resource fields in AccessReview, required `location` in ConfidentialLedger, and required nested `name` fields in DevCenter and HybridCompute emitted PATCH schemas. They are not explained away by compile failure, because all four projects are in the successful aligned population. + +The TypeSpec-only projects mostly come from semantic source properties that do not have a same-project validator finding in the selected emitted Swagger. Samples include defaulted PATCH properties in ApiCenter and AzureDataTransfer, required nested PATCH properties in AppLink and CloudHealth, and the new create-only lifecycle branch in AzureResilienceManagement. These may reflect newer TypeSpec-only services, projection/emission differences, or Swagger validator occurrence differences, so they are retained as one-sided evidence instead of being normalized away. + +## Diagnostic cardinality + +Over the 462 successful projects: + +- Validator raw diagnostics: 703 +- Validator raw identities (`project + swaggerFile + jsonPath`): 276 +- Validator file-independent identities (`project + jsonPath`): 276 +- TypeSpec raw diagnostics: 1243 +- TypeSpec source identities (`project + sourceFile + line + column`): 996 + +Largest raw-count outliers: + +| Project | Validator | TypeSpec | Difference | Likely cause | +| ------------------------------------------------------------------------- | --------: | -------: | ---------: | ---------------------------------------------------------------------------------------------------------------- | +| `specification/iotoperationsmq/IoTOperationsMQ.Management` | 55 | 179 | +124 | TypeSpec reports many semantic nested properties that collapse or differ in emitted Swagger occurrence identity. | +| `specification/discovery/Discovery.Management` | 0 | 45 | +45 | TypeSpec-only source diagnostics with no validator project firing in the aligned Swagger output. | +| `specification/edge/resource-manager/Microsoft.Edge/configurationmanager` | 0 | 32 | +32 | TypeSpec-only source diagnostics with no validator project firing. | +| `specification/deviceregistry/DeviceRegistry.Management` | 2 | 27 | +25 | TypeSpec semantic diagnostics exceed emitted validator occurrences. | +| `specification/eventgrid/resource-manager/Microsoft.EventGrid/EventGrid` | 62 | 51 | -11 | Swagger emitted occurrence count exceeds TypeSpec source targets. | + +These counts are evidence for source-to-emission multiplicity, not a requirement for equality. + +## Compile failures + +The full corpus run had six TypeSpec compile failures, excluded from the aligned behavioral comparison: + +- `specification/deviceprovisioningservices/resource-manager/Microsoft.Devices/DeviceProvisioningServices` +- `specification/monitor/resource-manager/Microsoft.Insights/Insights/TenantActionGroups` +- `specification/network/resource-manager/Microsoft.Network/Network/Network` +- `specification/quota/resource-manager/Microsoft.Quota/Quota` +- `specification/resources/resource-manager/Microsoft.Resources/deployments` +- `specification/servicelinker/resource-manager/Microsoft.ServiceLinker/ServiceLinker` + +No PatchBodyParametersSchema validator-only project is hidden by these failures. + +## Fixture evidence + +Focused validation with `LINTDIFF_VALIDATOR_ROOT=C:\dev\azure-openapi-validator` covered four local fixtures: + +- `required-patch-property`: Swagger and TypeSpec both report the required property. +- `default-patch-property`: Swagger and TypeSpec both report the default-valued property. +- `create-only-patch-property`: Swagger reports `x-ms-mutability: ["create"]`; TypeSpec now reports the corresponding `@visibility(Lifecycle.Create)` property. +- `top-level-identity-compliant`: Swagger is clean; TypeSpec now emits no mapped `patch-body-parameters-schema` diagnostic. Unrelated fixture noise is recorded in `expect.json`. + +The TypeSpec rule intentionally does not copy the Swagger implementation's truthiness check for defaults. Swagger checks `properties[prop].default`, which misses falsy emitted defaults such as `false`, `0`, or `""`; the TypeSpec rule continues to flag any authored default value because the rule requirement is that PATCH body properties must not have defaults. + +Review suggested treating `Lifecycle.Create` plus non-emitted lifecycle members such as `Lifecycle.Delete` as create-only. A focused regression attempt showed that the property is omitted from the PATCH schema and Swagger does not report it, so that suggestion was rejected to avoid a TypeSpec-only false positive. + +## Remaining uncertainty + +The production rule now covers the known authorable Swagger branches and avoids the identified identity false positive. It should not be marked fully equivalent yet because the latest full corpus still has unexplained one-sided projects. The remaining work is project-specific analysis of those one-sided projects, not another known missing branch in the rule implementation. diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/rule.md b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/rule.md index 726911d3e5..33eadeeed5 100644 --- a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/rule.md +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/rule.md @@ -15,18 +15,21 @@ coverageKind: partial PATCH body parameters must not have required properties, defaults, or create-only members. The local lint `tsp-lintdiff-local-linter/patch-body-parameters-schema` walks ARM PATCH request -body models recursively and currently flags the authorable TypeSpec sad paths that are proven in -this repo: +body models recursively and flags the authorable TypeSpec sad paths enforced by the Swagger rule: - required properties - default-valued properties +- properties emitted with `x-ms-mutability: ["create"]` -The upstream `x-ms-mutability: ["create"]` branch is not yet covered by a clean local TypeSpec -fixture, so the migration result is intentionally tracked as **partial** rather than fully -equivalent. +The Swagger rule skips a top-level PATCH body property named `identity` before checking that +property or its children. The local lint mirrors that exception to avoid false positives on +identity envelopes. ## Test Cases -| ID | Violation | Description | -| ------------------------- | --------- | -------------------------------------------- | -| `required-patch-property` | true | PATCH body contains required property | +| ID | Violation | Description | +| ------------------------------ | --------- | ---------------------------------------------------------------- | +| `required-patch-property` | true | PATCH body contains required property | +| `default-patch-property` | true | PATCH body contains default-valued property | +| `create-only-patch-property` | true | PATCH body contains a property emitted as create-only mutable | +| `top-level-identity-compliant` | false | PATCH body top-level `identity` is skipped like the Swagger rule | diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/expect.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/expect.json new file mode 100644 index 0000000000..2611605c28 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/expect.json @@ -0,0 +1,29 @@ +{ + "violation": false, + "ambientDiagnostics": [ + { + "code": "tsp-lintdiff-local-linter/latest-version-of-common-types-must-be-used", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/top-level-resources-list-by-resource-group", + "count": 1 + }, + { + "code": "@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-envelope-property", + "count": 1 + }, + { + "code": "@azure-tools/typespec-azure-resource-manager/patch-envelope", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "count": 4 + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "count": 7 + } + ] +} diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/main.tsp b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/main.tsp new file mode 100644 index 0000000000..d4ee4132b7 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/main.tsp @@ -0,0 +1,68 @@ +import "../../lib/imports.tsp"; + +using TypeSpec.Http; +using TypeSpec.Rest; +using TypeSpec.Versioning; +using Azure.ResourceManager; + +@armProviderNamespace +@service(#{ title: "Test Service" }) +@versioned(Versions) +@armCommonTypesVersion(CommonTypes.Versions.v5) +namespace Microsoft.TestService; + +enum Versions { + @useDependency(Azure.ResourceManager.CommonTypes.Versions.v5) + v2024_01_01: "2024-01-01", +} + +model Widget is TrackedResource { + @key("widgetName") + @segment("widgets") + @doc("The name of the widget") + @path + @pattern("^[a-zA-Z0-9_-]+$") + name: string; + + @doc("Identity on the resource model") + identity?: CustomIdentity; +} + +@doc("Widget resource properties.") +model WidgetProperties { + @doc("Description of the widget") + description?: string; + + @doc("Resource provisioning state") + @visibility(Lifecycle.Read) + provisioningState?: ResourceProvisioningState; +} + +@doc("Custom identity model.") +model CustomIdentity { + @doc("Required identity field that Swagger skips under top-level identity") + principalId: string; +} + +@doc("Patch envelope for widget.") +model WidgetPatchBody { + @doc("Top-level identity is skipped by the Swagger rule") + identity: CustomIdentity; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Widgets { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + delete is ArmResourceDeleteWithoutOkAsync; + + @doc("Update widget") + @patch + @armResourceUpdate(Widget) + update( + ...ResourceInstanceParameters, + @doc("The request body") @body body: WidgetPatchBody, + ): ArmResponse | ErrorResponse; +} diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/output.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/output.json new file mode 100644 index 0000000000..24690759fd --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/output.json @@ -0,0 +1,390 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "2024-01-01", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "host": "management.azure.com", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "description": "Azure Active Directory OAuth2 Flow.", + "flow": "implicit", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "tags": [ + { + "name": "Operations" + }, + { + "name": "Widgets" + } + ], + "paths": { + "/providers/Microsoft.TestService/operations": { + "get": { + "operationId": "Operations_List", + "tags": [ + "Operations" + ], + "description": "List the operations for the provider", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/OperationListResult" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.TestService/widgets/{widgetName}": { + "get": { + "operationId": "Widgets_Get", + "tags": [ + "Widgets" + ], + "description": "Get a Widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/Widget" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "operationId": "Widgets_CreateOrUpdate", + "tags": [ + "Widgets" + ], + "description": "Create a Widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + { + "name": "resource", + "in": "body", + "description": "Resource create parameters.", + "required": true, + "schema": { + "$ref": "#/definitions/Widget" + } + } + ], + "responses": { + "200": { + "description": "Resource 'Widget' update operation succeeded", + "schema": { + "$ref": "#/definitions/Widget" + } + }, + "201": { + "description": "Resource 'Widget' create operation succeeded", + "schema": { + "$ref": "#/definitions/Widget" + }, + "headers": { + "Azure-AsyncOperation": { + "type": "string", + "description": "A link to the status monitor" + }, + "Retry-After": { + "type": "integer", + "format": "int32", + "description": "The Retry-After header can indicate how long the client should wait before polling the operation status." + } + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "azure-async-operation" + }, + "x-ms-long-running-operation": true + }, + "patch": { + "operationId": "Widgets_Update", + "tags": [ + "Widgets" + ], + "description": "Update widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + { + "name": "body", + "in": "body", + "description": "The request body", + "required": true, + "schema": { + "$ref": "#/definitions/WidgetPatchBody" + } + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/Widget" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "operationId": "Widgets_Delete", + "tags": [ + "Widgets" + ], + "description": "Delete a Widget", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ApiVersionParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/SubscriptionIdParameter" + }, + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/parameters/ResourceGroupNameParameter" + }, + { + "name": "widgetName", + "in": "path", + "description": "The name of the widget", + "required": true, + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + } + ], + "responses": { + "202": { + "description": "Resource deletion accepted.", + "headers": { + "Location": { + "type": "string", + "description": "The Location header contains the URL where the status of the long running operation can be checked." + }, + "Retry-After": { + "type": "integer", + "format": "int32", + "description": "The Retry-After header can indicate how long the client should wait before polling the operation status." + } + } + }, + "204": { + "description": "Resource does not exist." + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-long-running-operation-options": { + "final-state-via": "location" + }, + "x-ms-long-running-operation": true + } + } + }, + "definitions": { + "Azure.ResourceManager.ResourceProvisioningState": { + "type": "string", + "description": "The provisioning state of a resource type.", + "enum": [ + "Succeeded", + "Failed", + "Canceled" + ], + "x-ms-enum": { + "name": "ResourceProvisioningState", + "modelAsString": true, + "values": [ + { + "name": "Succeeded", + "value": "Succeeded", + "description": "Resource has been created." + }, + { + "name": "Failed", + "value": "Failed", + "description": "Resource creation failed." + }, + { + "name": "Canceled", + "value": "Canceled", + "description": "Resource creation was canceled." + } + ] + } + }, + "CustomIdentity": { + "type": "object", + "description": "Custom identity model.", + "properties": { + "principalId": { + "type": "string", + "description": "Required identity field that Swagger skips under top-level identity" + } + }, + "required": [ + "principalId" + ] + }, + "Widget": { + "type": "object", + "description": "Concrete tracked resource types can be created by aliasing this type using a specific property type.", + "properties": { + "properties": { + "$ref": "#/definitions/WidgetProperties", + "description": "The resource-specific properties for this resource." + }, + "identity": { + "$ref": "#/definitions/CustomIdentity", + "description": "Identity on the resource model" + } + }, + "allOf": [ + { + "$ref": "../../../../../common-types/resource-management/v5/types.json#/definitions/TrackedResource" + } + ] + }, + "WidgetPatchBody": { + "type": "object", + "description": "Patch envelope for widget.", + "properties": { + "identity": { + "$ref": "#/definitions/CustomIdentity", + "description": "Top-level identity is skipped by the Swagger rule" + } + }, + "required": [ + "identity" + ] + }, + "WidgetProperties": { + "type": "object", + "description": "Widget resource properties.", + "properties": { + "description": { + "type": "string", + "description": "Description of the widget" + }, + "provisioningState": { + "$ref": "#/definitions/Azure.ResourceManager.ResourceProvisioningState", + "description": "Resource provisioning state", + "readOnly": true + } + } + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/tsp-diagnostics.json new file mode 100644 index 0000000000..244ade0eb0 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/tsp-diagnostics.json @@ -0,0 +1,77 @@ +[ + { + "code": "tsp-lintdiff-local-linter/latest-version-of-common-types-must-be-used", + "severity": "warning", + "message": "Use the latest ARM common-types version 'v6' instead of 'v5'." + }, + { + "code": "tsp-lintdiff-local-linter/top-level-resources-list-by-resource-group", + "severity": "warning", + "message": "Top-level resource 'Widget' should define a list by resource group operation." + }, + { + "code": "@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-envelope-property", + "severity": "warning", + "message": "Property \"identity\" is not valid in the resource envelope. Please remove this property, or add it to the resource-specific property bag." + }, + { + "code": "@azure-tools/typespec-azure-resource-manager/patch-envelope", + "severity": "warning", + "message": "The Resource PATCH request for resource 'Widget' is missing envelope properties: [tags]. Since these properties are supported in the resource, they must also be updatable via PATCH." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/path-parameter-schema", + "severity": "warning", + "message": "Path parameter should specify a maximum length (maxLength)." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/validator-diagnostics.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/top-level-identity-compliant/validator-diagnostics.json @@ -0,0 +1 @@ +[] From 4e603f295c0aa3b30721267e87c4028704db04df Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Fri, 14 Aug 2026 15:18:38 +0800 Subject: [PATCH 2/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../typespec-lintdiff/src/rules/patch-body-parameters-schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typespec-lintdiff/src/rules/patch-body-parameters-schema.ts b/packages/typespec-lintdiff/src/rules/patch-body-parameters-schema.ts index 4b258076a3..df08502c51 100644 --- a/packages/typespec-lintdiff/src/rules/patch-body-parameters-schema.ts +++ b/packages/typespec-lintdiff/src/rules/patch-body-parameters-schema.ts @@ -12,7 +12,7 @@ import { getHttpOperation } from "@typespec/http"; export const patchBodyParametersSchemaRule = createRule({ name: "patch-body-parameters-schema", - description: "ARM PATCH body properties must not be required and must not have defaults.", + description: "ARM PATCH body properties must not be required, have defaults, or be create-only.", severity: "warning", messages: { required: paramMessage`Properties of a PATCH request body must not be required, property:${"propertyName"}.`, From 8c7949b9e7146cb78cf6d43fd128e87652789952 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Fri, 14 Aug 2026 16:12:33 +0800 Subject: [PATCH 3/4] Document PatchBody TSP-only analysis Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7054e94e-5ed2-43ae-9bca-490a4203c0a7 --- .../PatchBodyParametersSchema/migration.md | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md index a027ca4516..a22cd2c39d 100644 --- a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md @@ -74,7 +74,55 @@ The report differences are caused by different snapshots and coverage definition The four validator-only projects are still real assessment gaps in the latest corpus. Sample validator findings are nested required discriminator/resource fields in AccessReview, required `location` in ConfidentialLedger, and required nested `name` fields in DevCenter and HybridCompute emitted PATCH schemas. They are not explained away by compile failure, because all four projects are in the successful aligned population. -The TypeSpec-only projects mostly come from semantic source properties that do not have a same-project validator finding in the selected emitted Swagger. Samples include defaulted PATCH properties in ApiCenter and AzureDataTransfer, required nested PATCH properties in AppLink and CloudHealth, and the new create-only lifecycle branch in AzureResilienceManagement. These may reflect newer TypeSpec-only services, projection/emission differences, or Swagger validator occurrence differences, so they are retained as one-sided evidence instead of being normalized away. +The checked-in `specs/coverage-breakdown.json` row generated on `2026-08-10T09:38:18.108Z` listed 39 TypeSpec-only projects. The post-change full corpus used for the main conclusion above reduced that to 34, but the checked-in 39-project set was analyzed because it is the set visible in the current comparison report. Across those 39 projects there were 262 TypeSpec-only diagnostics: + +- 25 diagnostics are falsy defaults present in emitted PATCH Swagger. Swagger does not report them because the validator implementation checks `properties[prop].default` truthily, so `false`, `0`, or `""` are skipped. +- 177 diagnostics are source-required or source create-only shapes that are emitted as optional or otherwise non-violating in the selected PATCH Swagger schema. These are source-vs-emission/projection differences and should not be counted as validator misses. +- 60 diagnostics refer to source paths that were not found in the selected emitted PATCH Swagger body schemas. These are reachability, projection, version, or identity-shape differences rather than same-shape Swagger misses. + +| Project | TSP diagnostics | Reason Swagger has no PatchBodyParametersSchema diagnostic | Example | +| ------------------------------------------------------------------------------------------------------------------------ | --------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| `specification/apicenter/ApiCenter.Management` | 1 | 1 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.restore` at `models.tsp:240:3` | +| `specification/applink/AppLink.Management` | 7 | 5 emitted-optional-or-different-patch-schema; 2 not-found-in-patch-swagger. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.metadata` at `applinkmember.tsp:27:3` | +| `specification/azuredatatransfer/resource-manager/Microsoft.AzureDataTransfer/AzureDataTransfer` | 2 | 2 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.rulesets.archives.minimumSizeForExpansion` at `models.flowprofile.tsp:217:3` | +| `specification/azureresiliencemanagement/resource-manager/Microsoft.AzureResilienceManagement/AzureResilienceManagement` | 24 | 21 emitted-optional-or-different-patch-schema; 3 not-found-in-patch-swagger. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.goalTemplateId` at `models/goals/goalAssignment.tsp:30:3` | +| `specification/billingbenefits/resource-manager/Microsoft.BillingBenefits/BillingBenefits` | 3 | 3 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.renew` at `models.tsp:1428:3` | +| `specification/cloudhealth/resource-manager/Microsoft.CloudHealth/CloudHealth` | 2 | 2 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `properties.discovery.scope` at `main.tsp:95:3` | +| `specification/computeschedule/resource-manager/Microsoft.ComputeSchedule/ComputeSchedule` | 6 | 5 emitted-optional-or-different-patch-schema; 1 falsy-default-validator-truthiness. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.schedule.scheduledTime` at `scheduledactionmodels.tsp:19:3` | +| `specification/containerservice/resource-manager/Microsoft.ContainerService/fleet` | 2 | 2 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties` at `gate.tsp:170:3` | +| `specification/databasewatcher/resource-manager/Microsoft.DatabaseWatcher/DatabaseWatcher` | 5 | 5 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.datastore.kustoClusterUri` at `watcher.tsp:93:3` | +| `specification/datafactory/resource-manager/Microsoft.DataFactory/DataFactory` | 1 | 1 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `identity.type` at `models.tsp:3540:3` | +| `specification/discovery/Discovery.Management` | 45 | 28 not-found-in-patch-swagger; 16 emitted-optional-or-different-patch-schema; 1 falsy-default-validator-truthiness. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `properties.keyVaultProperties.keyVaultUri` at `../Discovery.Management.Shared/control-plane.tsp:199:3` | +| `specification/edge/resource-manager/Microsoft.Edge/configurationmanager` | 32 | 27 emitted-optional-or-different-patch-schema; 5 not-found-in-patch-swagger. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `name` at `DynamicSchema.tsp:49:3` | +| `specification/edge/resource-manager/Microsoft.Edge/configurations` | 4 | 4 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `name` at `Configuration.tsp:31:3` | +| `specification/edge/resource-manager/Microsoft.Edge/disconnectedOperations` | 5 | 4 emitted-optional-or-different-patch-schema; 1 not-found-in-patch-swagger. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.billingConfiguration.autoRenew` at `models.tsp:483:3` | +| `specification/elasticsan/resource-manager/Microsoft.ElasticSan/ElasticSan` | 1 | 1 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `identity.type` at `models.tsp:580:3` | +| `specification/github-network/GitHub.Network.Management` | 1 | 1 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `name` at `NetworkSettingsResource.tsp:25:3` | +| `specification/healthcareapis/resource-manager/Microsoft.HealthcareApis/HealthcareApis` | 3 | 3 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `identity.type` at `models.tsp:1044:3` | +| `specification/imagebuilder/resource-manager/Microsoft.VirtualMachineImages/ImageBuilder` | 3 | 3 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.vmProfile.vmSize` at `models.tsp:652:3` | +| `specification/impact/Impact.Management` | 8 | 5 not-found-in-patch-swagger; 3 emitted-optional-or-different-patch-schema. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `properties.connectorId` at `connectors.tsp:64:3` | +| `specification/informatica/resource-manager/Informatica.DataManagement/Informatica` | 1 | 1 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.serverlessRuntimeNetworkProfile.networkInterfaceConfiguration` at `main.tsp:840:3` | +| `specification/kubernetesconfiguration/resource-manager/Microsoft.KubernetesConfiguration/fluxConfigurations` | 2 | 2 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.ociRepository.insecure` at `models.tsp:1299:3` | +| `specification/liftrastronomer/resource-manager/Astronomer.Astro/AstronomerAstro` | 8 | 8 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.marketplace.offerDetails` at `LiftrBase/main.tsp:65:3` | +| `specification/liftrmongodb/MongoDB.Atlas.Management` | 4 | 4 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.user.firstName` at `main.tsp:45:3` | +| `specification/manufacturingplatform/Manufacturingplatform.Management` | 6 | 6 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.fabricProfile.keyUri` at `main.tsp:347:3` | +| `specification/migrate/resource-manager/Microsoft.Migrate/AssessmentProjects` | 1 | 1 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `name` at `Common/ArmModels/AssessmentProjectV2.tsp:19:3` | +| `specification/mission/resource-manager/Microsoft.Mission/Mission` | 11 | 9 emitted-optional-or-different-patch-schema; 1 falsy-default-validator-truthiness; 1 not-found-in-patch-swagger. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.enclaveVirtualNetwork` at `resourcetypes/virtualEnclave/virtualenclave.tsp:170:3` | +| `specification/monitor/resource-manager/Microsoft.Insights/Insights/ScheduledQueryRuleApi` | 1 | 1 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `identity.type` at `../Common/main.tsp:87:3` | +| `specification/monitoringservice/resource-manager/Microsoft.Monitor/PipelineGroups` | 7 | 7 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.receivers` at `typespec/pipelineGroup.tsp:50:3` | +| `specification/onlineexperimentation/OnlineExperimentation.Management` | 1 | 1 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `sku.name` at `models.tsp:144:3` | +| `specification/oracle/resource-manager/Oracle.Database/OracleDatabase` | 6 | 6 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.dataCollectionOptions.isDiagnosticsEventsEnabled` at `models/common.tsp:147:3` | +| `specification/postgresql/DBforPostgreSQL.Management` | 4 | 3 falsy-default-validator-truthiness; 1 emitted-optional-or-different-patch-schema. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.highAvailability.standbyAvailabilityZone` at `models.tsp:3517:3` | +| `specification/programmableconnectivity/ProgrammableConnectivity.Management` | 6 | 6 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `properties.configuredApplication.name` at `Gateway.tsp:167:3` | +| `specification/purestorage/resource-manager/PureStorage.Block/PureStorageBlock` | 13 | 13 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.user.firstName` at `LiftrBase/main.tsp:142:3` | +| `specification/reservations/resource-manager/Microsoft.Capacity/Reservations/Reservations` | 2 | 2 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.renew` at `models.tsp:2169:3` | +| `specification/servicenetworking/resource-manager/Microsoft.ServiceNetworking/ServiceNetworking` | 5 | 5 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.securityPolicyConfigurations.wafSecurityPolicy.id` at `main.tsp:231:3` | +| `specification/sovereign/resource-manager/Microsoft.Sovereign/Sovereign` | 21 | 21 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.storageAccount` at `landingZoneAccountResourceProperties.tsp:15:3` | +| `specification/splitio/SplitIO.Experimentation.Management` | 6 | 6 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.accessPolicy` at `main.tsp:40:5` | +| `specification/workloads/Workloads.SAPMonitor.Management` | 1 | 1 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `name` at `SapLandscapeMonitor.tsp:29:3` | +| `specification/workloads/Workloads.SAPVirtualInstance.Management` | 1 | 1 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `identity.type` at `models.tsp:2516:3` | + +The first category is not a TypeSpec false alert: ApiCenter is a representative example where the selected Swagger PATCH body contains `ServiceUpdateProperties.restore` with `"default": false`, and Swagger skips it only because of the validator's truthiness check. The second and third categories are not Swagger validator misses; they are differences between TypeSpec source diagnostics and the emitted PATCH schemas that the Swagger validator actually receives. ## Diagnostic cardinality From 5c25b507376f6afa0e2beb0e9c0a6e627987a73b Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Fri, 14 Aug 2026 16:18:08 +0800 Subject: [PATCH 4/4] Clarify PatchBody TSP-only verification Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7054e94e-5ed2-43ae-9bca-490a4203c0a7 --- .../PatchBodyParametersSchema/migration.md | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md index a22cd2c39d..84b8f0a415 100644 --- a/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md +++ b/packages/typespec-lintdiff/test/fixtures/PatchBodyParametersSchema/migration.md @@ -74,25 +74,29 @@ The report differences are caused by different snapshots and coverage definition The four validator-only projects are still real assessment gaps in the latest corpus. Sample validator findings are nested required discriminator/resource fields in AccessReview, required `location` in ConfidentialLedger, and required nested `name` fields in DevCenter and HybridCompute emitted PATCH schemas. They are not explained away by compile failure, because all four projects are in the successful aligned population. -The checked-in `specs/coverage-breakdown.json` row generated on `2026-08-10T09:38:18.108Z` listed 39 TypeSpec-only projects. The post-change full corpus used for the main conclusion above reduced that to 34, but the checked-in 39-project set was analyzed because it is the set visible in the current comparison report. Across those 39 projects there were 262 TypeSpec-only diagnostics: +The checked-in `specs/coverage-breakdown.json` row generated on `2026-08-10T09:38:18.108Z` listed 39 TypeSpec-only projects. The post-change full corpus used for the main conclusion above reduced that to 34, but the checked-in 39-project set was analyzed because it is the set visible in the current comparison report. -- 25 diagnostics are falsy defaults present in emitted PATCH Swagger. Swagger does not report them because the validator implementation checks `properties[prop].default` truthily, so `false`, `0`, or `""` are skipped. -- 177 diagnostics are source-required or source create-only shapes that are emitted as optional or otherwise non-violating in the selected PATCH Swagger schema. These are source-vs-emission/projection differences and should not be counted as validator misses. -- 60 diagnostics refer to source paths that were not found in the selected emitted PATCH Swagger body schemas. These are reachability, projection, version, or identity-shape differences rather than same-shape Swagger misses. +Each of the 39 projects was checked independently against the actual checked-in result files. For every project, the validator result file has zero `PatchBodyParametersSchema` diagnostics. The emitted Swagger under that project was then walked through PATCH body schemas with `$ref` and `allOf` resolution, the top-level `identity` skip, recursive object traversal, `required`, `default`, and `x-ms-mutability` checks matching the validator behavior. The TypeSpec diagnostic path was only treated as emitted evidence when it matched an exact emitted PATCH property path; seven weaker suffix/name matches were conservatively counted as `not-found-in-patch-swagger`. -| Project | TSP diagnostics | Reason Swagger has no PatchBodyParametersSchema diagnostic | Example | +Across those 39 projects there were 262 TypeSpec-only diagnostics: + +- 25 diagnostics are exact emitted PATCH paths with falsy defaults. Swagger does not report them because the validator implementation checks `properties[prop].default` truthily, so `false`, `0`, or `""` are skipped. +- 170 diagnostics are exact emitted PATCH paths for source-required or source create-only shapes that are emitted as optional or otherwise non-violating in the selected PATCH Swagger schema. These are source-vs-emission/projection differences and should not be counted as validator misses. +- 67 diagnostics refer to TypeSpec source paths that have no exact emitted PATCH property path in the selected project/version. These are reachability, projection, version, or identity-shape differences rather than same-shape Swagger misses. + +| Project | TSP diagnostics | Exact-path verification result | Example | | ------------------------------------------------------------------------------------------------------------------------ | --------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `specification/apicenter/ApiCenter.Management` | 1 | 1 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.restore` at `models.tsp:240:3` | | `specification/applink/AppLink.Management` | 7 | 5 emitted-optional-or-different-patch-schema; 2 not-found-in-patch-swagger. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.metadata` at `applinkmember.tsp:27:3` | | `specification/azuredatatransfer/resource-manager/Microsoft.AzureDataTransfer/AzureDataTransfer` | 2 | 2 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.rulesets.archives.minimumSizeForExpansion` at `models.flowprofile.tsp:217:3` | -| `specification/azureresiliencemanagement/resource-manager/Microsoft.AzureResilienceManagement/AzureResilienceManagement` | 24 | 21 emitted-optional-or-different-patch-schema; 3 not-found-in-patch-swagger. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.goalTemplateId` at `models/goals/goalAssignment.tsp:30:3` | +| `specification/azureresiliencemanagement/resource-manager/Microsoft.AzureResilienceManagement/AzureResilienceManagement` | 24 | 18 emitted-optional-or-different-patch-schema; 6 not-found-in-patch-swagger. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.goalTemplateId` at `models/goals/goalAssignment.tsp:30:3` | | `specification/billingbenefits/resource-manager/Microsoft.BillingBenefits/BillingBenefits` | 3 | 3 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.renew` at `models.tsp:1428:3` | | `specification/cloudhealth/resource-manager/Microsoft.CloudHealth/CloudHealth` | 2 | 2 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `properties.discovery.scope` at `main.tsp:95:3` | | `specification/computeschedule/resource-manager/Microsoft.ComputeSchedule/ComputeSchedule` | 6 | 5 emitted-optional-or-different-patch-schema; 1 falsy-default-validator-truthiness. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.schedule.scheduledTime` at `scheduledactionmodels.tsp:19:3` | | `specification/containerservice/resource-manager/Microsoft.ContainerService/fleet` | 2 | 2 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties` at `gate.tsp:170:3` | | `specification/databasewatcher/resource-manager/Microsoft.DatabaseWatcher/DatabaseWatcher` | 5 | 5 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.datastore.kustoClusterUri` at `watcher.tsp:93:3` | | `specification/datafactory/resource-manager/Microsoft.DataFactory/DataFactory` | 1 | 1 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `identity.type` at `models.tsp:3540:3` | -| `specification/discovery/Discovery.Management` | 45 | 28 not-found-in-patch-swagger; 16 emitted-optional-or-different-patch-schema; 1 falsy-default-validator-truthiness. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `properties.keyVaultProperties.keyVaultUri` at `../Discovery.Management.Shared/control-plane.tsp:199:3` | +| `specification/discovery/Discovery.Management` | 45 | 30 not-found-in-patch-swagger; 14 emitted-optional-or-different-patch-schema; 1 falsy-default-validator-truthiness. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `properties.keyVaultProperties.keyVaultUri` at `../Discovery.Management.Shared/control-plane.tsp:199:3` | | `specification/edge/resource-manager/Microsoft.Edge/configurationmanager` | 32 | 27 emitted-optional-or-different-patch-schema; 5 not-found-in-patch-swagger. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `name` at `DynamicSchema.tsp:49:3` | | `specification/edge/resource-manager/Microsoft.Edge/configurations` | 4 | 4 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `name` at `Configuration.tsp:31:3` | | `specification/edge/resource-manager/Microsoft.Edge/disconnectedOperations` | 5 | 4 emitted-optional-or-different-patch-schema; 1 not-found-in-patch-swagger. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.billingConfiguration.autoRenew` at `models.tsp:483:3` | @@ -100,7 +104,7 @@ The checked-in `specs/coverage-breakdown.json` row generated on `2026-08-10T09:3 | `specification/github-network/GitHub.Network.Management` | 1 | 1 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `name` at `NetworkSettingsResource.tsp:25:3` | | `specification/healthcareapis/resource-manager/Microsoft.HealthcareApis/HealthcareApis` | 3 | 3 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `identity.type` at `models.tsp:1044:3` | | `specification/imagebuilder/resource-manager/Microsoft.VirtualMachineImages/ImageBuilder` | 3 | 3 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.vmProfile.vmSize` at `models.tsp:652:3` | -| `specification/impact/Impact.Management` | 8 | 5 not-found-in-patch-swagger; 3 emitted-optional-or-different-patch-schema. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `properties.connectorId` at `connectors.tsp:64:3` | +| `specification/impact/Impact.Management` | 8 | 6 not-found-in-patch-swagger; 2 emitted-optional-or-different-patch-schema. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `properties.connectorId` at `connectors.tsp:64:3` | | `specification/informatica/resource-manager/Informatica.DataManagement/Informatica` | 1 | 1 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.serverlessRuntimeNetworkProfile.networkInterfaceConfiguration` at `main.tsp:840:3` | | `specification/kubernetesconfiguration/resource-manager/Microsoft.KubernetesConfiguration/fluxConfigurations` | 2 | 2 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.ociRepository.insecure` at `models.tsp:1299:3` | | `specification/liftrastronomer/resource-manager/Astronomer.Astro/AstronomerAstro` | 8 | 8 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.marketplace.offerDetails` at `LiftrBase/main.tsp:65:3` | @@ -112,7 +116,7 @@ The checked-in `specs/coverage-breakdown.json` row generated on `2026-08-10T09:3 | `specification/monitoringservice/resource-manager/Microsoft.Monitor/PipelineGroups` | 7 | 7 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.receivers` at `typespec/pipelineGroup.tsp:50:3` | | `specification/onlineexperimentation/OnlineExperimentation.Management` | 1 | 1 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `sku.name` at `models.tsp:144:3` | | `specification/oracle/resource-manager/Oracle.Database/OracleDatabase` | 6 | 6 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.dataCollectionOptions.isDiagnosticsEventsEnabled` at `models/common.tsp:147:3` | -| `specification/postgresql/DBforPostgreSQL.Management` | 4 | 3 falsy-default-validator-truthiness; 1 emitted-optional-or-different-patch-schema. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.highAvailability.standbyAvailabilityZone` at `models.tsp:3517:3` | +| `specification/postgresql/DBforPostgreSQL.Management` | 4 | 3 falsy-default-validator-truthiness; 1 not-found-in-patch-swagger. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.highAvailability.standbyAvailabilityZone` at `models.tsp:3517:3` | | `specification/programmableconnectivity/ProgrammableConnectivity.Management` | 6 | 6 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `properties.configuredApplication.name` at `Gateway.tsp:167:3` | | `specification/purestorage/resource-manager/PureStorage.Block/PureStorageBlock` | 13 | 13 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `properties.user.firstName` at `LiftrBase/main.tsp:142:3` | | `specification/reservations/resource-manager/Microsoft.Capacity/Reservations/Reservations` | 2 | 2 falsy-default-validator-truthiness. Falsy default is present in PATCH Swagger; Swagger skips because it checks default truthiness. | default `properties.renew` at `models.tsp:2169:3` | @@ -122,7 +126,7 @@ The checked-in `specs/coverage-breakdown.json` row generated on `2026-08-10T09:3 | `specification/workloads/Workloads.SAPMonitor.Management` | 1 | 1 emitted-optional-or-different-patch-schema. Source required/create-only shape is emitted optional or otherwise non-violating in PATCH Swagger. | required `name` at `SapLandscapeMonitor.tsp:29:3` | | `specification/workloads/Workloads.SAPVirtualInstance.Management` | 1 | 1 not-found-in-patch-swagger. Source diagnostic path was not found in emitted PATCH Swagger for the selected project/version. | required `identity.type` at `models.tsp:2516:3` | -The first category is not a TypeSpec false alert: ApiCenter is a representative example where the selected Swagger PATCH body contains `ServiceUpdateProperties.restore` with `"default": false`, and Swagger skips it only because of the validator's truthiness check. The second and third categories are not Swagger validator misses; they are differences between TypeSpec source diagnostics and the emitted PATCH schemas that the Swagger validator actually receives. +The falsy-default category is not a TypeSpec false alert. For each project in that category, the exact emitted PATCH property path contains a default value that Swagger skips only because of the validator's truthiness check; ApiCenter's `ServiceUpdateProperties.restore` with `"default": false` is one concrete example. The second and third categories are not Swagger validator misses; they are differences between TypeSpec source diagnostics and the emitted PATCH schemas that the Swagger validator actually receives. ## Diagnostic cardinality