完成50%

This commit is contained in:
2026-07-23 08:23:59 +08:00
parent 9b0ad62df4
commit f1edc6b533
218 changed files with 24318 additions and 5514 deletions
@@ -0,0 +1,275 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$jsonPath = Join-Path $root 'APP.openapi.json'
$yamlPath = Join-Path $root 'APP.openapi.yaml'
$document = Get-Content -Raw -Encoding UTF8 -LiteralPath $jsonPath | ConvertFrom-Json
$yaml = Get-Content -Raw -Encoding UTF8 -LiteralPath $yamlPath
$issues = New-Object System.Collections.Generic.List[string]
function Add-Issue {
param([string]$Message)
$script:issues.Add($Message)
}
function Get-JsonOperation {
param([string]$Path, [string]$Method)
$pathProperty = $document.paths.PSObject.Properties[$Path]
if (-not $pathProperty) {
Add-Issue "JSON missing path: $Path"
return $null
}
$operationProperty = $pathProperty.Value.PSObject.Properties[$Method]
if (-not $operationProperty) {
Add-Issue "JSON missing operation: $($Method.ToUpper()) $Path"
return $null
}
return $operationProperty.Value
}
function Get-JsonResponseSchemaRef {
param([object]$Operation, [string]$Label)
if (-not $Operation) { return '' }
$responseProperty = $Operation.responses.PSObject.Properties['200']
if (-not $responseProperty) {
Add-Issue "JSON $Label missing 200 response"
return ''
}
$response = $responseProperty.Value
if ($response.'$ref') {
$responseName = ([string]$response.'$ref').Split('/')[-1]
$responseOwner = $document.components.responses.PSObject.Properties[$responseName]
if (-not $responseOwner) {
Add-Issue "JSON $Label references missing response owner: $responseName"
return ''
}
$response = $responseOwner.Value
}
$media = @($response.content.PSObject.Properties)
if ($media.Count -eq 0) {
Add-Issue "JSON $Label 200 response has no content schema"
return ''
}
return [string]$media[0].Value.schema.'$ref'
}
function Get-JsonSchema {
param([string]$Name)
$property = $document.components.schemas.PSObject.Properties[$Name]
if (-not $property) {
Add-Issue "JSON missing schema owner: $Name"
return $null
}
return $property.Value
}
function Assert-JsonRequired {
param([object]$Schema, [string]$SchemaName, [string[]]$Fields)
if (-not $Schema) { return }
$required = @($Schema.required)
foreach ($field in $Fields) {
if ($field -notin $required) {
Add-Issue "JSON $SchemaName.required missing: $field"
}
}
}
function Assert-JsonProperty {
param(
[object]$Schema,
[string]$SchemaName,
[string]$Field,
[string]$Type,
[switch]$NonEmpty
)
if (-not $Schema) { return $null }
$property = $Schema.properties.PSObject.Properties[$Field]
if (-not $property) {
Add-Issue "JSON $SchemaName missing property: $Field"
return $null
}
if ($property.Value.type -ne $Type) {
Add-Issue "JSON $SchemaName.$Field must be $Type"
}
if ($NonEmpty -and [int]$property.Value.minLength -lt 1) {
Add-Issue "JSON $SchemaName.$Field must declare minLength >= 1"
}
return $property.Value
}
function Get-YamlBlock {
param([string]$Header, [string]$NextHeaderPattern)
$lines = @($yaml -split "`r?`n")
$start = [Array]::IndexOf($lines, $Header)
if ($start -lt 0) { return '' }
$end = $lines.Count
for ($index = $start + 1; $index -lt $lines.Count; $index += 1) {
if ($lines[$index] -match $NextHeaderPattern) {
$end = $index
break
}
}
return ($lines[$start..($end - 1)] -join "`n")
}
function Get-YamlRequiredFields {
param([string]$Block)
$match = [regex]::Match(
$Block,
'(?ms)^ required:\s*\n(?<rows>(?: - [^\n]+\n?)+)'
)
if (-not $match.Success) { return @() }
return @([regex]::Matches($match.Groups['rows'].Value, '(?m)^ - (?<name>[^\s]+)\s*$') |
ForEach-Object { $_.Groups['name'].Value })
}
function Get-YamlFieldBlock {
param([string]$SchemaBlock, [string]$Field)
$pattern = '(?ms)^ ' + [regex]::Escape($Field) + ':\s*\n(?<body>.*?)(?=^ [A-Za-z0-9_]+:\s*$|\z)'
$match = [regex]::Match($SchemaBlock, $pattern)
if (-not $match.Success) { return '' }
return $match.Value
}
function Assert-YamlRequired {
param([string]$Block, [string]$SchemaName, [string[]]$Fields)
if (-not $Block) { return }
$required = @(Get-YamlRequiredFields $Block)
foreach ($field in $Fields) {
if ($field -notin $required) {
Add-Issue "YAML $SchemaName.required missing: $field"
}
}
}
function Assert-YamlField {
param(
[string]$Block,
[string]$SchemaName,
[string]$Field,
[string]$Type,
[switch]$NonEmpty
)
if (-not $Block) { return '' }
$fieldBlock = Get-YamlFieldBlock $Block $Field
if (-not $fieldBlock) {
Add-Issue "YAML $SchemaName missing property: $Field"
return ''
}
if ($fieldBlock -notmatch "(?m)^ type: $([regex]::Escape($Type))\s*$") {
Add-Issue "YAML $SchemaName.$Field must be $Type"
}
if ($NonEmpty -and $fieldBlock -notmatch '(?m)^ minLength: [1-9][0-9]*\s*$') {
Add-Issue "YAML $SchemaName.$Field must declare minLength >= 1"
}
return $fieldBlock
}
$minePath = '/genealogy/app/genealogies/mine'
$overviewPath = '/genealogy/app/genealogies/{genealogyId}/overview'
$mine = Get-JsonOperation $minePath 'get'
$overview = Get-JsonOperation $overviewPath 'get'
$mineResponseRef = Get-JsonResponseSchemaRef $mine 'GET /mine'
if ($mineResponseRef -ne '#/components/schemas/RListAppGenealogyVo') {
Add-Issue "JSON GET /mine must return RListAppGenealogyVo; actual: $mineResponseRef"
}
$overviewResponseRef = Get-JsonResponseSchemaRef $overview 'GET /overview'
if ($overviewResponseRef -ne '#/components/schemas/RAppGenealogyVo') {
Add-Issue "JSON GET /overview must return RAppGenealogyVo; actual: $overviewResponseRef"
}
$listEnvelope = Get-JsonSchema 'RListAppGenealogyVo'
$objectEnvelope = Get-JsonSchema 'RAppGenealogyVo'
$genealogy = Get-JsonSchema 'AppGenealogyVo'
Assert-JsonRequired $listEnvelope 'RListAppGenealogyVo' @('code', 'data')
Assert-JsonRequired $objectEnvelope 'RAppGenealogyVo' @('code', 'data')
Assert-JsonProperty $listEnvelope 'RListAppGenealogyVo' 'code' 'integer' | Out-Null
Assert-JsonProperty $objectEnvelope 'RAppGenealogyVo' 'code' 'integer' | Out-Null
if ($listEnvelope) {
$listData = $listEnvelope.properties.PSObject.Properties['data'].Value
if (-not $listData -or $listData.type -ne 'array' -or $listData.items.'$ref' -ne '#/components/schemas/AppGenealogyVo') {
Add-Issue 'JSON RListAppGenealogyVo.data must be AppGenealogyVo[]'
}
}
if ($objectEnvelope) {
$objectData = $objectEnvelope.properties.PSObject.Properties['data'].Value
if (-not $objectData -or $objectData.'$ref' -ne '#/components/schemas/AppGenealogyVo') {
Add-Issue 'JSON RAppGenealogyVo.data must reference AppGenealogyVo'
}
}
$consumedFields = @('genealogyId', 'genealogyName', 'canView', 'canManage', 'canEditContent', 'roleType')
Assert-JsonRequired $genealogy 'AppGenealogyVo' $consumedFields
Assert-JsonProperty $genealogy 'AppGenealogyVo' 'genealogyId' 'string' -NonEmpty | Out-Null
Assert-JsonProperty $genealogy 'AppGenealogyVo' 'genealogyName' 'string' -NonEmpty | Out-Null
foreach ($field in @('canView', 'canManage', 'canEditContent')) {
Assert-JsonProperty $genealogy 'AppGenealogyVo' $field 'boolean' | Out-Null
}
$roleType = Assert-JsonProperty $genealogy 'AppGenealogyVo' 'roleType' 'string'
if ($roleType) {
$roleValues = @($roleType.enum | Where-Object { $_ -is [string] -and $_.Trim() }) | Select-Object -Unique
if ($roleValues.Count -lt 2) {
Add-Issue 'JSON AppGenealogyVo.roleType must declare at least two non-empty enum values'
}
}
$yamlMine = Get-YamlBlock " $minePath`:" '^ /.*:$'
$yamlOverview = Get-YamlBlock " $overviewPath`:" '^ /.*:$'
if (-not $yamlMine) {
Add-Issue "YAML missing path: $minePath"
} elseif ($yamlMine -notmatch [regex]::Escape('#/components/schemas/RListAppGenealogyVo')) {
Add-Issue 'YAML GET /mine must return RListAppGenealogyVo'
}
if (-not $yamlOverview) {
Add-Issue "YAML missing path: $overviewPath"
} elseif ($yamlOverview -notmatch [regex]::Escape('#/components/schemas/RAppGenealogyVo')) {
Add-Issue 'YAML GET /overview must return RAppGenealogyVo'
}
$yamlListEnvelope = Get-YamlBlock ' RListAppGenealogyVo:' '^ [A-Za-z0-9_]+:$'
$yamlObjectEnvelope = Get-YamlBlock ' RAppGenealogyVo:' '^ [A-Za-z0-9_]+:$'
$yamlGenealogy = Get-YamlBlock ' AppGenealogyVo:' '^ [A-Za-z0-9_]+:$'
foreach ($schema in @(
@{ Name = 'RListAppGenealogyVo'; Block = $yamlListEnvelope },
@{ Name = 'RAppGenealogyVo'; Block = $yamlObjectEnvelope },
@{ Name = 'AppGenealogyVo'; Block = $yamlGenealogy }
)) {
if (-not $schema.Block) { Add-Issue "YAML missing schema owner: $($schema.Name)" }
}
Assert-YamlRequired $yamlListEnvelope 'RListAppGenealogyVo' @('code', 'data')
Assert-YamlRequired $yamlObjectEnvelope 'RAppGenealogyVo' @('code', 'data')
Assert-YamlRequired $yamlGenealogy 'AppGenealogyVo' $consumedFields
Assert-YamlField $yamlListEnvelope 'RListAppGenealogyVo' 'code' 'integer' | Out-Null
Assert-YamlField $yamlObjectEnvelope 'RAppGenealogyVo' 'code' 'integer' | Out-Null
Assert-YamlField $yamlGenealogy 'AppGenealogyVo' 'genealogyId' 'string' -NonEmpty | Out-Null
Assert-YamlField $yamlGenealogy 'AppGenealogyVo' 'genealogyName' 'string' -NonEmpty | Out-Null
foreach ($field in @('canView', 'canManage', 'canEditContent')) {
Assert-YamlField $yamlGenealogy 'AppGenealogyVo' $field 'boolean' | Out-Null
}
$yamlRoleType = Assert-YamlField $yamlGenealogy 'AppGenealogyVo' 'roleType' 'string'
if ($yamlRoleType) {
$enumMatch = [regex]::Match(
$yamlRoleType,
'(?ms)^ enum:\s*\n(?<rows>(?: - [^\n]+\n?)+)'
)
$yamlRoleValues = if ($enumMatch.Success) {
@([regex]::Matches($enumMatch.Groups['rows'].Value, '(?m)^ - (?<value>\S.*?)\s*$') |
ForEach-Object { $_.Groups['value'].Value } | Select-Object -Unique)
} else { @() }
if ($yamlRoleValues.Count -lt 2) {
Add-Issue 'YAML AppGenealogyVo.roleType must declare at least two enum values'
}
}
if ($issues.Count -gt 0) {
$lines = New-Object System.Collections.Generic.List[string]
$lines.Add('GENEALOGY-WORKSPACE-OPENAPI-CONTRACT BLOCKED')
foreach ($issue in $issues) { $lines.Add("- $issue") }
$lines.Add('- Release behavior still requires authenticated tests for current-account filtering, revocation, cross-account access, 401/403/404/5xx, malformed JSON, and cancellation.')
$lines.Add('- Replace both protected exports from one backend version; do not hand-edit APP.openapi.json or APP.openapi.yaml.')
throw ($lines -join [Environment]::NewLine)
}
Write-Output 'GENEALOGY-WORKSPACE-OPENAPI-CONTRACT PASS'