完成40%

This commit is contained in:
rain
2026-07-23 17:21:27 +08:00
parent f1edc6b533
commit bb6431b319
114 changed files with 10931 additions and 877 deletions
@@ -0,0 +1,990 @@
$ErrorActionPreference = 'Stop'
$contractPath = Join-Path $PSScriptRoot 'family-feed-read-openapi-contract.ps1'
$temporaryPath = [System.IO.Path]::GetTempFileName()
$identifierPattern = '^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$'
$cursorPattern = '^[A-Za-z0-9_-]{1,512}$'
function New-Ref([string]$Ref) {
return [ordered]@{ '$ref' = $Ref }
}
function New-StringOwner(
[int]$MinLength,
[int]$MaxLength,
[string]$Pattern = ''
) {
$schema = [ordered]@{
type = 'string'
minLength = $MinLength
maxLength = $MaxLength
nullable = $false
}
if ($Pattern) { $schema.pattern = $Pattern }
return $schema
}
function New-ClosedObject(
[System.Collections.Specialized.OrderedDictionary]$Properties,
[string[]]$Required
) {
return [ordered]@{
type = 'object'
properties = $Properties
required = $Required
additionalProperties = $false
nullable = $false
}
}
function New-SuccessEnvelope([string]$DataRef) {
return New-ClosedObject ([ordered]@{
code = [ordered]@{
type = 'integer'
enum = @(200)
nullable = $false
}
data = New-Ref $DataRef
}) @('code', 'data')
}
function New-ErrorEnvelope([int]$Status, [string[]]$BusinessCodes) {
return New-ClosedObject ([ordered]@{
businessCode = [ordered]@{
type = 'string'
enum = $BusinessCodes
nullable = $false
}
code = [ordered]@{
type = 'integer'
enum = @($Status)
nullable = $false
}
message = [ordered]@{
type = 'string'
minLength = 1
maxLength = 200
nullable = $false
}
}) @('businessCode', 'code', 'message')
}
function New-Response([string]$SchemaRef, [bool]$RateLimited = $false) {
$headers = [ordered]@{
'Cache-Control' = New-Ref '#/components/headers/PrivateNoStore'
}
if ($RateLimited) {
$headers.'Retry-After' = New-Ref '#/components/headers/RetryAfter'
}
return [ordered]@{
description = 'typed response'
headers = $headers
content = [ordered]@{
'application/json' = [ordered]@{
schema = New-Ref $SchemaRef
}
}
}
}
function New-Responses([string]$SuccessRef) {
return [ordered]@{
'200' = New-Response $SuccessRef
'400' = New-Response '#/components/schemas/RFamilyFeedReadBadRequest'
'401' = New-Response '#/components/schemas/RFamilyFeedReadUnauthorized'
'404' = New-Response '#/components/schemas/RFamilyFeedReadNotFound'
'429' = New-Response '#/components/schemas/RFamilyFeedReadRateLimited' $true
'500' = New-Response '#/components/schemas/RFamilyFeedReadUnavailable'
}
}
function New-ClientIdParameter {
return [ordered]@{
name = 'clientid'
in = 'header'
required = $true
schema = [ordered]@{
type = 'string'
minLength = 1
maxLength = 128
nullable = $false
}
}
}
function New-IdParameter([string]$Name, [string]$SchemaRef) {
return [ordered]@{
name = $Name
in = 'path'
required = $true
schema = New-Ref $SchemaRef
}
}
function New-CursorParameter([string]$SchemaRef) {
return [ordered]@{
name = 'cursor'
in = 'query'
required = $false
schema = New-Ref $SchemaRef
}
}
function New-LimitParameter {
return [ordered]@{
name = 'limit'
in = 'query'
required = $false
schema = [ordered]@{
type = 'integer'
minimum = 1
maximum = 50
default = 20
nullable = $false
}
}
}
function New-ReadOperation(
[string]$OperationId,
[object[]]$Parameters,
[string]$SuccessRef,
[string[]]$AuthorizationScope
) {
return [ordered]@{
operationId = $OperationId
parameters = $Parameters
responses = New-Responses $SuccessRef
security = @([ordered]@{ SaToken = @() })
'x-read-only' = $true
'x-non-disclosing-not-found' = $true
'x-authorize-every-request' = $true
'x-cors-policy-owner' = 'APP_GATEWAY_PREFLIGHT'
'x-authorization-scope' = $AuthorizationScope
}
}
function Add-CursorContract(
[System.Collections.Specialized.OrderedDictionary]$Operation,
[string[]]$Scope,
[string[]]$Order
) {
$Operation.'x-cursor-scope' = $Scope
$Operation.'x-cursor-order' = $Order
$Operation.'x-read-window' = 'UPPER_BOUND_KEYSET_LATEST_VISIBLE'
$Operation.'x-cursor-no-total' = $true
$Operation.'x-refresh-discards-cursor' = $true
$Operation.'x-authorize-every-page' = $true
$Operation.'x-invalid-or-expired-cursor' = '400_FAMILY_FEED_CURSOR_INVALID'
$Operation.'x-cross-scope-cursor' = '404_FAMILY_FEED_NOT_AVAILABLE'
}
function New-CursorPage(
[string]$ItemRef,
[string]$CursorRef,
[string[]]$Order
) {
$page = New-ClosedObject ([ordered]@{
items = [ordered]@{
type = 'array'
items = New-Ref $ItemRef
minItems = 0
maxItems = 50
nullable = $false
}
nextCursor = New-Ref $CursorRef
}) @('items')
$page.'x-no-total' = $true
$page.'x-next-cursor-absent-at-end' = $true
$page.'x-order' = $Order
$page.'x-read-window' = 'UPPER_BOUND_KEYSET_LATEST_VISIBLE'
$page.'x-new-items-after-window' = 'EXCLUDED_UNTIL_REFRESH'
$page.'x-deletion-or-visibility-change' = 'OMIT_ON_LATER_PAGE'
$page.'x-edit-policy' = 'LATEST_VISIBLE_AT_PAGE_READ'
return $page
}
function New-ValidDocument {
$genealogyId = New-StringOwner 1 128 $identifierPattern
$feedId = New-StringOwner 1 128 $identifierPattern
$feedId.'x-opaque' = $true
$feedId.'x-client-semantics' = 'COMPARE_ONLY'
$commentId = New-StringOwner 1 128 $identifierPattern
$commentId.'x-opaque' = $true
$commentId.'x-client-semantics' = 'COMPARE_ONLY'
$feedCursor = New-StringOwner 1 512 $cursorPattern
$feedCursor.'x-opaque' = $true
$feedCursor.'x-purpose' = 'FAMILY_FEED_PAGE'
$commentCursor = New-StringOwner 1 512 $cursorPattern
$commentCursor.'x-opaque' = $true
$commentCursor.'x-purpose' = 'FAMILY_FEED_ROOT_COMMENT_PAGE'
$feedContent = New-StringOwner 1 300
$feedContent.'x-text-normalizer' = 'FAMILY_FEED_TEXT_V1'
$feedContent.'x-length-unit' = 'UNICODE_CODE_POINT'
$commentContent = New-StringOwner 1 1000
$commentContent.'x-text-normalizer' = 'FAMILY_FEED_COMMENT_TEXT_V1'
$commentContent.'x-length-unit' = 'UNICODE_CODE_POINT'
$displayName = New-StringOwner 1 100
$displayName.'x-projection' = 'AUTHORIZED_DISPLAY_NAME_ONLY'
$displayName.'x-missing-author-policy' = 'NON_EMPTY_SERVER_FALLBACK'
$publishedAt = [ordered]@{
type = 'string'
format = 'date-time'
nullable = $false
'x-server-generated' = $true
'x-immutable' = $true
}
$feedItem = New-ClosedObject ([ordered]@{
authorDisplayName = New-Ref '#/components/schemas/FamilyFeedAuthorDisplayName'
feedContent = New-Ref '#/components/schemas/FamilyFeedContent'
feedId = New-Ref '#/components/schemas/FamilyFeedId'
hasMedia = [ordered]@{
type = 'boolean'
nullable = $false
}
publishedAt = New-Ref '#/components/schemas/FamilyFeedPublishedAt'
}) @('authorDisplayName', 'feedContent', 'feedId', 'hasMedia', 'publishedAt')
$feedItem.description = 'Annotations may mention phone or audit examples without becoming response fields.'
$feedItem.example = [ordered]@{ annotationOnly = 'appUserPhone is not a schema property' }
$feedItem.'x-projection' = 'VISIBLE_FEED_PRESENTATION_ONLY'
$feedItem.'x-media-policy' = 'HAS_MEDIA_REQUIRES_HONEST_CLIENT_PLACEHOLDER_UNTIL_MEDIA_READ_CONTRACT'
$commentItem = New-ClosedObject ([ordered]@{
authorDisplayName = New-Ref '#/components/schemas/FamilyFeedAuthorDisplayName'
commentContent = New-Ref '#/components/schemas/FamilyFeedCommentContent'
commentId = New-Ref '#/components/schemas/FamilyFeedCommentId'
publishedAt = New-Ref '#/components/schemas/FamilyFeedPublishedAt'
}) @('authorDisplayName', 'commentContent', 'commentId', 'publishedAt')
$commentItem.'x-projection' = 'VISIBLE_COMMENT_PRESENTATION_ONLY'
$commentItem.'x-comment-level' = 'ROOT_ONLY'
$commentItem.'x-deleted-placeholder-policy' = 'EXCLUDE'
$feedOrder = @('publishedAt:DESC', 'feedId:DESC_ORDINAL')
$commentOrder = @('publishedAt:ASC', 'commentId:ASC_ORDINAL')
$feedList = New-ReadOperation 'appListFamilyFeeds' @(
(New-ClientIdParameter),
(New-IdParameter 'genealogyId' '#/components/schemas/GenealogyId'),
(New-CursorParameter '#/components/schemas/FamilyFeedCursor'),
(New-LimitParameter)
) '#/components/schemas/RAppFamilyFeedCursorPage' @('tenant', 'genealogy', 'membership')
Add-CursorContract $feedList @(
'tenant', 'account', 'authSession', 'client', 'genealogyId', 'projection',
'order', 'limit', 'windowUpperBound', 'lastTuple'
) $feedOrder
$feedDetail = New-ReadOperation 'appGetFamilyFeed' @(
(New-ClientIdParameter),
(New-IdParameter 'genealogyId' '#/components/schemas/GenealogyId'),
(New-IdParameter 'feedId' '#/components/schemas/FamilyFeedId')
) '#/components/schemas/RAppFamilyFeedReadItem' @(
'tenant', 'genealogy', 'membership', 'feedBelongsToGenealogy', 'feedVisibility'
)
$comments = New-ReadOperation 'appListFamilyFeedRootComments' @(
(New-ClientIdParameter),
(New-IdParameter 'genealogyId' '#/components/schemas/GenealogyId'),
(New-IdParameter 'feedId' '#/components/schemas/FamilyFeedId'),
(New-CursorParameter '#/components/schemas/FamilyFeedRootCommentCursor'),
(New-LimitParameter)
) '#/components/schemas/RAppFamilyFeedRootCommentCursorPage' @(
'tenant', 'genealogy', 'membership', 'feedBelongsToGenealogy', 'feedVisibility'
)
Add-CursorContract $comments @(
'tenant', 'account', 'authSession', 'client', 'genealogyId', 'feedId',
'projection', 'order', 'limit', 'windowUpperBound', 'lastTuple'
) $commentOrder
$schemas = [ordered]@{
GenealogyId = $genealogyId
FamilyFeedId = $feedId
FamilyFeedCommentId = $commentId
FamilyFeedCursor = $feedCursor
FamilyFeedRootCommentCursor = $commentCursor
FamilyFeedContent = $feedContent
FamilyFeedCommentContent = $commentContent
FamilyFeedAuthorDisplayName = $displayName
FamilyFeedPublishedAt = $publishedAt
AppFamilyFeedReadItem = $feedItem
AppFamilyFeedRootCommentReadItem = $commentItem
AppFamilyFeedCursorPage = New-CursorPage '#/components/schemas/AppFamilyFeedReadItem' '#/components/schemas/FamilyFeedCursor' $feedOrder
AppFamilyFeedRootCommentCursorPage = New-CursorPage '#/components/schemas/AppFamilyFeedRootCommentReadItem' '#/components/schemas/FamilyFeedRootCommentCursor' $commentOrder
RAppFamilyFeedCursorPage = New-SuccessEnvelope '#/components/schemas/AppFamilyFeedCursorPage'
RAppFamilyFeedReadItem = New-SuccessEnvelope '#/components/schemas/AppFamilyFeedReadItem'
RAppFamilyFeedRootCommentCursorPage = New-SuccessEnvelope '#/components/schemas/AppFamilyFeedRootCommentCursorPage'
RFamilyFeedReadBadRequest = New-ErrorEnvelope 400 @('FAMILY_FEED_CURSOR_INVALID', 'FAMILY_FEED_QUERY_INVALID')
RFamilyFeedReadUnauthorized = New-ErrorEnvelope 401 @('AUTH_REQUIRED')
RFamilyFeedReadNotFound = New-ErrorEnvelope 404 @('FAMILY_FEED_NOT_AVAILABLE')
RFamilyFeedReadRateLimited = New-ErrorEnvelope 429 @('RATE_LIMITED')
RFamilyFeedReadUnavailable = New-ErrorEnvelope 500 @('FAMILY_FEED_READ_UNAVAILABLE')
}
$document = [ordered]@{
openapi = '3.0.1'
info = [ordered]@{
title = 'family feed read adversarial fixture'
version = '1'
}
paths = [ordered]@{
'/genealogy/app/genealogies/{genealogyId}/feeds' = [ordered]@{
get = $feedList
}
'/genealogy/app/genealogies/{genealogyId}/feeds/{feedId}' = [ordered]@{
get = $feedDetail
}
'/genealogy/app/genealogies/{genealogyId}/feeds/{feedId}/comments' = [ordered]@{
get = $comments
}
}
components = [ordered]@{
schemas = $schemas
headers = [ordered]@{
PrivateNoStore = [ordered]@{
schema = [ordered]@{
type = 'string'
enum = @('private, no-store')
nullable = $false
}
}
RetryAfter = [ordered]@{
schema = [ordered]@{
type = 'integer'
minimum = 1
maximum = 300
nullable = $false
}
}
}
securitySchemes = [ordered]@{
SaToken = [ordered]@{
type = 'apiKey'
in = 'header'
name = 'Authorization'
}
}
}
}
return ($document | ConvertTo-Json -Depth 100 | ConvertFrom-Json)
}
function Copy-Document([object]$Document) {
return ($Document | ConvertTo-Json -Depth 100 | ConvertFrom-Json)
}
function Get-ContractIssues([object]$Document) {
$json = $Document | ConvertTo-Json -Depth 100
[System.IO.File]::WriteAllText($temporaryPath, $json, [System.Text.UTF8Encoding]::new($false))
$output = @(
& $contractPath -SkipProtectedParity -ReturnIssues -DocumentPath $temporaryPath
)
return @($output | Where-Object { $_ -is [string] -and $_.Length -gt 0 })
}
function Rename-NoteProperty([object]$Owner, [string]$From, [string]$To) {
$property = @($Owner.PSObject.Properties | Where-Object { $_.Name -ceq $From })[0]
if (-not $property) { throw "mutation setup missing property: $From" }
$value = $property.Value
$Owner.PSObject.Properties.Remove($From)
$Owner.PSObject.Properties.Add([System.Management.Automation.PSNoteProperty]::new($To, $value))
}
function Add-NoteProperty([object]$Owner, [string]$Name, [object]$Value) {
$Owner.PSObject.Properties.Add([System.Management.Automation.PSNoteProperty]::new($Name, $Value))
}
function Assert-MutantRejected(
[object]$Seed,
[string]$Label,
[scriptblock]$Mutate,
[string]$ExpectedIssuePattern
) {
$mutant = Copy-Document $Seed
& $Mutate $mutant
$mutantIssues = @(Get-ContractIssues $mutant)
if ($mutantIssues.Count -eq 0) {
throw "adversarial mutant fake-greened: $Label"
}
if ($ExpectedIssuePattern -and -not (($mutantIssues -join "`n") -match $ExpectedIssuePattern)) {
throw "adversarial mutant rejected for the wrong reason: $Label`n$($mutantIssues -join "`n")"
}
}
try {
$seed = New-ValidDocument
$seedIssues = @(Get-ContractIssues $seed)
if ($seedIssues.Count -gt 0) {
throw "valid zero-issue seed was rejected:`n$($seedIssues -join "`n")"
}
$mutations = @(
@{
Label = 'legacy /feeds/page GET owner'
Pattern = 'legacy duplicate GET owner'
Apply = {
param($doc)
Add-NoteProperty $doc.paths '/genealogy/app/genealogies/{genealogyId}/feeds/page' ([pscustomobject]@{
get = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
})
}
},
@{
Label = 'operationId drift'
Pattern = 'operationId must be appListFamilyFeeds'
Apply = { param($doc) $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.operationId = 'list_17' }
},
@{
Label = 'Path Item Get keyword casing'
Pattern = 'Path Item keyword casing is invalid'
Apply = {
param($doc)
Rename-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds' 'get' 'Get'
}
},
@{
Label = 'security object instead of array'
Pattern = 'security must be a JSON array'
Apply = {
param($doc)
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.security = [pscustomobject]@{ SaToken = @() }
}
},
@{
Label = 'SaToken key casing'
Pattern = 'must require only exact SaToken'
Apply = {
param($doc)
$requirement = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.security[0]
Rename-NoteProperty $requirement 'SaToken' 'satoken'
}
},
@{
Label = 'non-empty SaToken scopes'
Pattern = 'SaToken scopes must be an empty JSON array'
Apply = {
param($doc)
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.security[0].SaToken = @('feed:read')
}
},
@{
Label = 'int64 feed identity'
Pattern = 'sole exact local ref #/components/schemas/FamilyFeedId'
Apply = {
param($doc)
$doc.components.schemas.AppFamilyFeedReadItem.properties.feedId = [pscustomobject]@{ type = 'integer'; format = 'int64' }
}
},
@{
Label = 'open feed projection'
Pattern = 'AppFamilyFeedReadItem must be closed'
Apply = { param($doc) $doc.components.schemas.AppFamilyFeedReadItem.additionalProperties = $true }
},
@{
Label = 'phone field leak'
Pattern = 'success graph leaks forbidden/internal field'
Apply = {
param($doc)
Add-NoteProperty $doc.components.schemas.AppFamilyFeedReadItem.properties 'appUserPhone' ([pscustomobject]@{
type = 'string'; nullable = $false
})
$doc.components.schemas.AppFamilyFeedReadItem.required += 'appUserPhone'
}
},
@{
Label = 'moderation field leak'
Pattern = 'success graph leaks forbidden/internal field'
Apply = {
param($doc)
Add-NoteProperty $doc.components.schemas.AppFamilyFeedRootCommentReadItem.properties 'moderationReason' ([pscustomobject]@{
type = 'string'; nullable = $false
})
$doc.components.schemas.AppFamilyFeedRootCommentReadItem.required += 'moderationReason'
}
},
@{
Label = 'offset total in cursor page'
Pattern = 'AppFamilyFeedCursorPage must be closed'
Apply = {
param($doc)
Add-NoteProperty $doc.components.schemas.AppFamilyFeedCursorPage.properties 'total' ([pscustomobject]@{
type = 'integer'; nullable = $false
})
$doc.components.schemas.AppFamilyFeedCursorPage.required += 'total'
}
},
@{
Label = 'pageNum query bypass'
Pattern = 'parameters must be exactly'
Apply = {
param($doc)
$operation = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$operation.parameters += [pscustomobject]@{
name = 'pageNum'; in = 'query'; required = $false
schema = [pscustomobject]@{ type = 'integer'; minimum = 1; nullable = $false }
}
}
},
@{
Label = 'cursor scope comma string'
Pattern = 'x-cursor-scope must be the exact JSON array'
Apply = {
param($doc)
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.'x-cursor-scope' = 'tenant,account,authSession'
}
},
@{
Label = 'cross-scope cursor returns 400'
Pattern = 'cursor/refresh/per-page authorization semantics drifted'
Apply = {
param($doc)
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.'x-cross-scope-cursor' = '400_FAMILY_FEED_CURSOR_INVALID'
}
},
@{
Label = '403 resource existence split'
Pattern = 'responses must be exactly'
Apply = {
param($doc)
Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds/{feedId}'.get.responses '403' (
New-Response '#/components/schemas/RFamilyFeedReadNotFound'
)
}
},
@{
Label = 'wildcard response media type'
Pattern = 'must expose only application/json'
Apply = {
param($doc)
$response = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200'
Rename-NoteProperty $response.content 'application/json' '*/*'
}
},
@{
Label = 'missing private cache header'
Pattern = 'must define Cache-Control'
Apply = {
param($doc)
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200'.headers.PSObject.Properties.Remove('Cache-Control')
}
},
@{
Label = 'unbounded Retry-After'
Pattern = 'Retry-After must be a non-null integer in 1..300'
Apply = { param($doc) $doc.components.headers.RetryAfter.schema.maximum = 301 }
},
@{
Label = 'generic list response'
Pattern = '200 schema ref must be #/components/schemas/RAppFamilyFeedCursorPage'
Apply = {
param($doc)
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200'.content.'application/json'.schema.'$ref' = '#/components/schemas/RObject'
}
},
@{
Label = 'external feedId ref'
Pattern = 'must be the sole exact local ref'
Apply = {
param($doc)
$doc.components.schemas.AppFamilyFeedReadItem.properties.feedId.'$ref' = 'https://example.invalid/schemas.json#/FamilyFeedId'
}
},
@{
Label = 'hasMedia removed'
Pattern = 'AppFamilyFeedReadItem must be closed'
Apply = {
param($doc)
$doc.components.schemas.AppFamilyFeedReadItem.properties.PSObject.Properties.Remove('hasMedia')
$doc.components.schemas.AppFamilyFeedReadItem.required = @(
$doc.components.schemas.AppFamilyFeedReadItem.required | Where-Object { $_ -cne 'hasMedia' }
)
}
},
@{
Label = 'feed cursor order drift'
Pattern = 'x-cursor-order must be the exact JSON array'
Apply = {
param($doc)
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.'x-cursor-order' = @('feedId:DESC_ORDINAL', 'publishedAt:DESC')
}
},
@{
Label = 'mutable publishedAt'
Pattern = 'must be a non-null immutable server-generated RFC3339'
Apply = { param($doc) $doc.components.schemas.FamilyFeedPublishedAt.'x-immutable' = $false }
},
@{
Label = 'feed content length drift'
Pattern = 'FamilyFeedContent must be a non-null string length 1..300'
Apply = { param($doc) $doc.components.schemas.FamilyFeedContent.maxLength = 301 }
},
@{
Label = 'comment level internal field'
Pattern = 'success graph leaks forbidden/internal field'
Apply = {
param($doc)
Add-NoteProperty $doc.components.schemas.AppFamilyFeedRootCommentReadItem.properties 'commentLevel' ([pscustomobject]@{
type = 'string'; enum = @('root'); nullable = $false
})
$doc.components.schemas.AppFamilyFeedRootCommentReadItem.required += 'commentLevel'
}
},
@{
Label = 'duplicate operationId'
Pattern = 'operationId must be appListFamilyFeedRootComments|must have exactly one global operation owner'
Apply = {
param($doc)
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds/{feedId}/comments'.get.operationId = 'appListFamilyFeeds'
}
},
@{
Label = 'explicit HEAD read bypass'
Pattern = 'must not expose an explicit HEAD read bypass'
Apply = {
param($doc)
Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds' 'head' ([pscustomobject]@{
responses = [pscustomobject]@{}
})
}
},
@{
Label = 'callback side channel'
Pattern = 'must not define callbacks'
Apply = {
param($doc)
Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get 'callbacks' ([pscustomobject]@{
leak = [pscustomobject]@{}
})
}
},
@{
Label = 'schema Type keyword casing'
Pattern = 'contains an unowned schema keyword: Type'
Apply = { param($doc) Rename-NoteProperty $doc.components.schemas.FamilyFeedContent 'type' 'Type' }
},
@{
Label = 'nextCursor made required'
Pattern = 'AppFamilyFeedCursorPage must be closed'
Apply = {
param($doc)
$doc.components.schemas.AppFamilyFeedCursorPage.required += 'nextCursor'
}
},
@{
Label = 'alternate APP GET reuses feed read projection'
Pattern = 'alternate APP operation exposes the family-feed read projection'
Apply = {
param($doc)
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowFamilyFeedRead'
Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow' ([pscustomobject]@{ get = $shadow })
}
},
@{
Label = 'Response Object links side channel'
Pattern = 'contains an unowned Response Object keyword: links'
Apply = {
param($doc)
Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200' 'links' ([pscustomobject]@{
next = [pscustomobject]@{ operationId = 'shadowFamilyFeedRead' }
})
}
},
@{
Label = 'Header Object content side channel'
Pattern = 'contains an unowned Header Object keyword: content'
Apply = {
param($doc)
Add-NoteProperty $doc.components.headers.PrivateNoStore 'content' ([pscustomobject]@{
'application/json' = [pscustomobject]@{ schema = [pscustomobject]@{ type = 'string' } }
})
}
},
@{
Label = 'OpenAPI 3.1 dialect drift'
Pattern = 'OpenAPI version must be exact 3.0.1'
Apply = { param($doc) $doc.openapi = '3.1.0' }
},
@{
Label = 'OpenAPI 3.1 webhooks keyword'
Pattern = 'OpenAPI root contains an unowned keyword: webhooks'
Apply = { param($doc) Add-NoteProperty $doc 'webhooks' ([pscustomobject]@{}) }
},
@{
Label = 'OpenAPI Paths keyword casing'
Pattern = 'OpenAPI root keyword casing is invalid: Paths'
Apply = { param($doc) Rename-NoteProperty $doc 'paths' 'Paths' }
},
@{
Label = 'OpenAPI Components keyword casing'
Pattern = 'OpenAPI root keyword casing is invalid: Components'
Apply = { param($doc) Rename-NoteProperty $doc 'components' 'Components' }
},
@{
Label = 'cursor extension keyword casing'
Pattern = 'operation keyword casing is invalid: X-Cursor-Scope'
Apply = {
param($doc)
Rename-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get 'x-cursor-scope' 'X-Cursor-Scope'
}
},
@{
Label = 'cursor scope comma join collision'
Pattern = 'x-cursor-scope must be the exact JSON array'
Apply = {
param($doc)
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.'x-cursor-scope' = @(
'tenant,account', 'authSession', 'client', 'genealogyId', 'projection',
'order', 'limit', 'windowUpperBound', 'lastTuple'
)
}
},
@{
Label = 'Parameter schema keyword casing'
Pattern = 'Parameter Object keyword casing is invalid: Schema'
Apply = {
param($doc)
$parameter = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.parameters[1]
Rename-NoteProperty $parameter 'schema' 'Schema'
}
},
@{
Label = 'path parameter case-shadow'
Pattern = 'parameters must be exactly'
Apply = {
param($doc)
$pathItem = $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'
Add-NoteProperty $pathItem 'parameters' @(
[pscustomobject]@{
name = 'GenealogyId'
in = 'path'
required = $true
schema = New-Ref '#/components/schemas/GenealogyId'
}
)
}
},
@{
Label = 'Parameter Reference Object sibling'
Pattern = 'parameter ref must contain only its exact local'
Apply = {
param($doc)
Add-NoteProperty $doc.components 'parameters' ([pscustomobject]@{
GenealogyIdParameter = [pscustomobject]@{
name = 'genealogyId'
in = 'path'
required = $true
schema = New-Ref '#/components/schemas/GenealogyId'
}
})
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.parameters[1] = [pscustomobject]@{
'$ref' = '#/components/parameters/GenealogyIdParameter'
description = 'forbidden sibling'
}
}
},
@{
Label = 'Schema Reference Object sibling'
Pattern = 'must be the sole exact local ref'
Apply = {
param($doc)
Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.parameters[1].schema 'description' 'forbidden sibling'
}
},
@{
Label = 'SaToken security scheme drift'
Pattern = 'SaToken must be the exact apiKey/header/Authorization security owner'
Apply = {
param($doc)
$doc.components.securitySchemes.SaToken = [pscustomobject]@{
type = 'oauth2'
flows = [pscustomobject]@{}
}
}
},
@{
Label = 'alternate 206 owner'
Pattern = 'alternate APP operation exposes the family-feed read projection'
Apply = {
param($doc)
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowFamilyFeed206'
$shadow.responses = [pscustomobject]@{
'206' = New-Response '#/components/schemas/RAppFamilyFeedCursorPage'
}
Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-206' ([pscustomobject]@{ get = $shadow })
}
},
@{
Label = 'replies path reuses canonical projection'
Pattern = 'alternate APP operation exposes the family-feed read projection'
Apply = {
param($doc)
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowRepliesFamilyFeed'
Add-NoteProperty $doc.paths '/genealogy/app/genealogies/{genealogyId}/replies-shadow' ([pscustomobject]@{ get = $shadow })
}
},
@{
Label = 'alternate HEAD projection owner'
Pattern = 'alternate APP operation exposes the family-feed read projection'
Apply = {
param($doc)
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowHeadFamilyFeed'
Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-head' ([pscustomobject]@{ head = $shadow })
}
},
@{
Label = 'alternate OPTIONS projection owner'
Pattern = 'alternate APP operation exposes the family-feed read projection'
Apply = {
param($doc)
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowOptionsFamilyFeed'
Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-options' ([pscustomobject]@{ options = $shadow })
}
},
@{
Label = 'callback GET projection owner'
Pattern = 'alternate APP operation exposes the family-feed read projection'
Apply = {
param($doc)
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowCallbackFamilyFeed'
Add-NoteProperty $doc.paths '/genealogy/app/callback-carrier' ([pscustomobject]@{
post = [pscustomobject]@{
operationId = 'callbackCarrier'
responses = [pscustomobject]@{
'204' = [pscustomobject]@{ description = 'accepted' }
}
callbacks = [pscustomobject]@{
leak = [pscustomobject]@{
'{$request.body#/callbackUrl}' = [pscustomobject]@{ get = $shadow }
}
}
}
})
}
},
@{
Label = 'inline alternate projection owner'
Pattern = 'alternate APP operation exposes the family-feed read projection'
Apply = {
param($doc)
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowInlineFamilyFeed'
$shadow.responses.'200'.content.'application/json'.schema = Copy-Document $doc.components.schemas.RAppFamilyFeedCursorPage
Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-inline' ([pscustomobject]@{ get = $shadow })
}
},
@{
Label = 'two-hop schema alias owner'
Pattern = 'alternate APP operation exposes the family-feed read projection'
Apply = {
param($doc)
Add-NoteProperty $doc.components.schemas 'FamilyFeedAliasTwo' (New-Ref '#/components/schemas/RAppFamilyFeedCursorPage')
Add-NoteProperty $doc.components.schemas 'FamilyFeedAliasOne' (New-Ref '#/components/schemas/FamilyFeedAliasTwo')
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowSchemaAliasFamilyFeed'
$shadow.responses.'200'.content.'application/json'.schema = New-Ref '#/components/schemas/FamilyFeedAliasOne'
Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-schema-alias' ([pscustomobject]@{ get = $shadow })
}
},
@{
Label = 'two-hop response alias owner'
Pattern = 'alternate APP operation exposes the family-feed read projection'
Apply = {
param($doc)
Add-NoteProperty $doc.components 'responses' ([pscustomobject]@{
FamilyFeedAliasOne = New-Ref '#/components/responses/FamilyFeedAliasTwo'
FamilyFeedAliasTwo = New-Response '#/components/schemas/RAppFamilyFeedCursorPage'
})
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowResponseAliasFamilyFeed'
$shadow.responses = [pscustomobject]@{
'200' = New-Ref '#/components/responses/FamilyFeedAliasOne'
}
Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-response-alias' ([pscustomobject]@{ get = $shadow })
}
},
@{
Label = 'external alternate response schema'
Pattern = 'schema ref is not an inspectable exact local component ref'
Apply = {
param($doc)
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowExternalFamilyFeed'
$shadow.responses.'200'.content.'application/json'.schema = New-Ref 'https://example.invalid/feed.json#/FeedPage'
Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-external' ([pscustomobject]@{ get = $shadow })
}
},
@{
Label = 'default response projection owner'
Pattern = 'alternate APP operation exposes the family-feed read projection'
Apply = {
param($doc)
$shadow = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get
$shadow.operationId = 'shadowDefaultFamilyFeed'
$shadow.responses = [pscustomobject]@{
default = New-Response '#/components/schemas/RAppFamilyFeedCursorPage'
}
Add-NoteProperty $doc.paths '/genealogy/app/family-feed-shadow-default' ([pscustomobject]@{ get = $shadow })
}
},
@{
Label = 'Response Reference Object sibling'
Pattern = 'response ref must contain only its exact local'
Apply = {
param($doc)
Add-NoteProperty $doc.components 'responses' ([pscustomobject]@{
FeedListSuccess = Copy-Document $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200'
})
$doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200' = [pscustomobject]@{
'$ref' = '#/components/responses/FeedListSuccess'
description = 'forbidden sibling'
}
}
},
@{
Label = 'Header Reference Object sibling'
Pattern = 'header ref must contain only its exact local'
Apply = {
param($doc)
Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.responses.'200'.headers.'Cache-Control' 'description' 'forbidden sibling'
}
},
@{
Label = 'Parameter content side channel'
Pattern = 'Parameter Object contains an unowned keyword: content'
Apply = {
param($doc)
Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get.parameters[1] 'content' ([pscustomobject]@{
'application/json' = [pscustomobject]@{
schema = New-Ref '#/components/schemas/GenealogyId'
}
})
}
},
@{
Label = 'unknown operation extension'
Pattern = 'operation contains an unowned keyword: x-shadow-owner'
Apply = {
param($doc)
Add-NoteProperty $doc.paths.'/genealogy/app/genealogies/{genealogyId}/feeds'.get 'x-shadow-owner' 'legacy'
}
}
)
foreach ($mutation in $mutations) {
Assert-MutantRejected $seed $mutation.Label $mutation.Apply $mutation.Pattern
}
Write-Output "FAMILY-FEED-READ-OPENAPI-ADVERSARIAL-CONTRACT PASS MUTANTS=$($mutations.Count)"
} finally {
if (Test-Path -LiteralPath $temporaryPath -PathType Leaf) {
Remove-Item -LiteralPath $temporaryPath -Force
}
}