param( [string]$ProjectRoot = "D:\React\cm93-mysql-importer-starter", [string]$SourceRoot = "D:\Ship\CM93V2\CM93JAPAN", [int]$ExpectedCells = 1492, [int]$Workers = 4, [switch]$Force ) # CM95 Japan Full Rebuild Pipeline v2 # # 从零重建: # CM93 source -> fresh MySQL -> audit -> mapping gates # -> fresh FID manifest -> fresh production plan # -> full PBF build -> final release gate # # Windows 11 / PowerShell # # 破坏性范围: # - DROP .env 中 CM93_DB_NAME 整个数据库 # - 删除旧 build\production # - 删除旧 build\releases # - 删除旧 build\pbf # - 删除旧 build\pbf-reports # - 删除旧 build\cm95_fid_manifest*.sqlite* # # 不会删除: # - D:\Ship\CM93V2\CM93JAPAN # - src\ # - config\ # - web\ # - logs\ 历史文件 # # v2 修正: # - 禁止 python.exe -c 多行代码,避免 Windows/PowerShell 引号损坏 # - 临时 Python helper 一律落盘执行 # - audit JSON gate 对齐当前 cm93db AuditResult # - geometry_validity_issues 保持 informational # - status JSON 使用 Out-String 后再 ConvertFrom-Json # - 生成当前 Dataset 专属 semantic disposition summary # # 如果新 Dataset 出现未分类 mapping gap,会在 PBF build 前 STOP。 Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" function Write-Step { param([string]$Text) Write-Host "" Write-Host "======================================================================" Write-Host $Text Write-Host "======================================================================" } function Invoke-Checked { param( [string]$Label, [scriptblock]$Action ) Write-Step $Label & $Action $ExitCode = $LASTEXITCODE if ($null -ne $ExitCode -and $ExitCode -ne 0) { throw "$Label FAILED (exit code=$ExitCode)" } } function Remove-PathIfExists { param([string]$PathValue) if (Test-Path $PathValue) { Write-Host "DELETE: $PathValue" Remove-Item $PathValue -Recurse -Force } } function Get-StateCount { param( [object]$States, [string]$Name ) if ($null -eq $States) { return 0 } $Prop = $States.PSObject.Properties[$Name] if ($null -eq $Prop) { return 0 } return [int]$Prop.Value } if (-not (Test-Path $ProjectRoot -PathType Container)) { throw "ProjectRoot 不存在: $ProjectRoot" } if (-not (Test-Path $SourceRoot -PathType Container)) { throw "SourceRoot 不存在: $SourceRoot" } Set-Location $ProjectRoot $ProjectRoot = (Resolve-Path $ProjectRoot).Path $SourceRoot = (Resolve-Path $SourceRoot).Path $Python = Join-Path $ProjectRoot ".venv\Scripts\python.exe" $Cm93Db = Join-Path $ProjectRoot ".venv\Scripts\cm93db.exe" $Cm95Pbf = Join-Path $ProjectRoot ".venv\Scripts\cm95pbf.exe" $EnvPath = Join-Path $ProjectRoot ".env" foreach ($RequiredExe in @($Python, $Cm93Db, $Cm95Pbf)) { if (-not (Test-Path $RequiredExe -PathType Leaf)) { throw "缺少运行文件: $RequiredExe" } } if (-not (Test-Path $EnvPath -PathType Leaf)) { throw "缺少项目 .env: $EnvPath" } # 临时 helper 放系统 TEMP,不受 build 清理影响。 $TempRoot = Join-Path $env:TEMP ("cm95_japan_pipeline_v2_" + $PID) Remove-Item $TempRoot -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $TempRoot -Force | Out-Null try { # ---------------------------------------------------------------------- # STEP 0 - Source hard gate # ---------------------------------------------------------------------- Write-Step "STEP 0 - Japan CM93 源目录检查" $RequiredRegions = @( "03300360", "03300420", "03900360", "03900420" ) $ActualRegions = @( Get-ChildItem $SourceRoot -Directory -ErrorAction Stop | Where-Object { $_.Name -match '^\d{8}$' } | ForEach-Object { $_.Name } | Sort-Object ) $MissingRegions = @( $RequiredRegions | Where-Object { $_ -notin $ActualRegions } ) $ExtraRegions = @( $ActualRegions | Where-Object { $_ -notin $RequiredRegions } ) Write-Host "SourceRoot:" Write-Host " $SourceRoot" Write-Host "" Write-Host "Numeric regions:" $ActualRegions | ForEach-Object { Write-Host " $_" } if ($MissingRegions.Count -gt 0) { throw "缺少 Japan 必需区域: $($MissingRegions -join ', ')" } if ($ExtraRegions.Count -gt 0) { throw "发现 Japan 工作集之外的数字区域,停止: $($ExtraRegions -join ', ')" } if (-not (Test-Path (Join-Path $SourceRoot "CM93OBJ.DIC"))) { throw "缺少 CM93OBJ.DIC" } $HasAttrDic = (Test-Path (Join-Path $SourceRoot "CM93ATTR.DIC")) -or (Test-Path (Join-Path $SourceRoot "ATTRLUT.DIC")) if (-not $HasAttrDic) { throw "缺少 CM93ATTR.DIC / ATTRLUT.DIC" } Write-Host "" Write-Host "Japan source region gate: PASS" # ---------------------------------------------------------------------- # STEP 1 - Current code baseline + read-only checks # ---------------------------------------------------------------------- Invoke-Checked "STEP 1A - pip install -e ." { & $Python -m pip install -e . } if (-not (Test-Path ".\tools\cm95_v1_4_5_preflight.py")) { throw "缺少 tools\cm95_v1_4_5_preflight.py;当前项目不是 v1.4.5 baseline" } Invoke-Checked "STEP 1B - CM95 v1.4.5 preflight" { & $Python ".\tools\cm95_v1_4_5_preflight.py" } Invoke-Checked "STEP 1C - CM93 scan" { & $Cm93Db scan $SourceRoot } Invoke-Checked "STEP 1D - CM93 dictionaries" { & $Cm93Db inspect-dicts $SourceRoot } # ---------------------------------------------------------------------- # STEP 2 - Read DB config with a real helper .py file # ---------------------------------------------------------------------- $ReadDbInfoPy = Join-Path $TempRoot "read_db_info.py" @' from __future__ import annotations import json import os import sys from pathlib import Path from dotenv import load_dotenv env_path = Path(sys.argv[1]).resolve() load_dotenv(dotenv_path=env_path) result = { "host": os.getenv("CM93_DB_HOST", "127.0.0.1"), "port": int(os.getenv("CM93_DB_PORT", "3306")), "database": (os.getenv("CM93_DB_NAME", "cm93_chart") or "").strip(), "user": os.getenv("CM93_DB_USER", "root"), "charset": os.getenv("CM93_DB_CHARSET", "utf8mb4"), } if not result["database"]: raise SystemExit("CM93_DB_NAME is empty") print(json.dumps(result, ensure_ascii=False)) '@ | Set-Content $ReadDbInfoPy -Encoding UTF8 $DbInfoText = (& $Python $ReadDbInfoPy $EnvPath | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($DbInfoText)) { throw "无法读取数据库配置" } $DbInfo = $DbInfoText | ConvertFrom-Json $DbName = [string]$DbInfo.database $ForbiddenDbNames = @( "mysql", "information_schema", "performance_schema", "sys" ) if ($DbName.ToLowerInvariant() -in $ForbiddenDbNames) { throw "拒绝删除系统数据库: $DbName" } Write-Step "STEP 2 - DESTRUCTIVE RESET CONFIRMATION" Write-Host "将永久删除整个 MySQL database:" Write-Host " $DbName" Write-Host "" Write-Host "数据库服务器:" Write-Host " $($DbInfo.host):$($DbInfo.port)" Write-Host "" Write-Host "也会删除旧 CM95 build/release 产物。" Write-Host "" Write-Host "不会删除 CM93 source:" Write-Host " $SourceRoot" if (-not $Force) { $Expected = "DELETE $DbName" $Answer = Read-Host "请输入 [$Expected] 继续" if ($Answer -ne $Expected) { throw "用户取消。" } } else { Write-Host "-Force 已指定,跳过交互确认。" } # ---------------------------------------------------------------------- # STEP 3 - Delete old generated artifacts # ---------------------------------------------------------------------- Write-Step "STEP 3 - 删除旧 CM95 build/release 产物" Remove-PathIfExists (Join-Path $ProjectRoot "build\production") Remove-PathIfExists (Join-Path $ProjectRoot "build\releases") Remove-PathIfExists (Join-Path $ProjectRoot "build\pbf") Remove-PathIfExists (Join-Path $ProjectRoot "build\pbf-reports") $BuildRoot = Join-Path $ProjectRoot "build" if (Test-Path $BuildRoot) { Get-ChildItem $BuildRoot -File -Filter "cm95_fid_manifest*.sqlite*" -ErrorAction SilentlyContinue | ForEach-Object { Write-Host "DELETE: $($_.FullName)" Remove-Item $_.FullName -Force } } New-Item -ItemType Directory -Force -Path ".\build\production" | Out-Null New-Item -ItemType Directory -Force -Path ".\logs" | Out-Null # ---------------------------------------------------------------------- # STEP 4 - DROP database using helper file, then let cm93db recreate schema # ---------------------------------------------------------------------- $DropDbPy = Join-Path $TempRoot "drop_cm93_database.py" @' from __future__ import annotations import os import re import sys from pathlib import Path import pymysql from dotenv import load_dotenv env_path = Path(sys.argv[1]).resolve() load_dotenv(dotenv_path=env_path) host = os.getenv("CM93_DB_HOST", "127.0.0.1") port = int(os.getenv("CM93_DB_PORT", "3306")) user = os.getenv("CM93_DB_USER", "root") password = os.getenv("CM93_DB_PASSWORD", "") database = (os.getenv("CM93_DB_NAME", "cm93_chart") or "").strip() charset = os.getenv("CM93_DB_CHARSET", "utf8mb4") if not database: raise RuntimeError("CM93_DB_NAME is empty") if database.lower() in { "mysql", "information_schema", "performance_schema", "sys", }: raise RuntimeError(f"refuse to drop system database: {database}") conn = pymysql.connect( host=host, port=port, user=user, password=password, charset=charset, autocommit=False, cursorclass=pymysql.cursors.DictCursor, ) try: with conn.cursor() as cur: cur.execute("SELECT VERSION() AS v") version = str(cur.fetchone()["v"]) match = re.match(r"^(\d+)\.(\d+)\.(\d+)", version) if not match or tuple(map(int, match.groups()[:2])) != (5, 7): raise RuntimeError( f"expected MySQL 5.7.x, got {version}; refusing destructive reset" ) safe = database.replace("`", "``") cur.execute(f"DROP DATABASE IF EXISTS `{safe}`") conn.commit() print(f"DROP_DATABASE_OK database={database} mysql={version}") finally: conn.close() '@ | Set-Content $DropDbPy -Encoding UTF8 Invoke-Checked "STEP 4A - DROP old database" { & $Python $DropDbPy $EnvPath } Invoke-Checked "STEP 4B - CREATE database + initialize schema" { & $Cm93Db db-init --create-database } Invoke-Checked "STEP 4C - DB contract check" { & $Cm93Db db-check } # ---------------------------------------------------------------------- # STEP 5 - Full import on fresh DB # ---------------------------------------------------------------------- Invoke-Checked "STEP 5 - 全量 CM93 Japan import(fresh DB)" { & $Cm93Db import $SourceRoot --fail-fast } # ---------------------------------------------------------------------- # STEP 6 - Full audit # ---------------------------------------------------------------------- $Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $AuditReport = ".\logs\cm93_audit_japan_full_rebuild_$Timestamp.json" Invoke-Checked "STEP 6 - CM93 full audit + geometry validation" { & $Cm93Db audit ` $SourceRoot ` --validate-geometry ` --report $AuditReport } $Audit = Get-Content $AuditReport -Raw | ConvertFrom-Json if (-not [bool]$Audit.passed) { throw "CM93 audit report passed=false;检查 $AuditReport" } $AuditHardFailures = @( [int64]$Audit.source_parse_failed, [int64]$Audit.db_missing_cells, [int64]$Audit.db_not_done_cells, [int64]$Audit.count_mismatches, [int64]$Audit.hash_mismatches, [int64]$Audit.raw_preservation_mismatches, [int64]$Audit.unknown_objects, [int64]$Audit.unknown_attributes, [int64]$Audit.precision_failed ) if (($AuditHardFailures | Measure-Object -Sum).Sum -ne 0) { throw "CM93 audit hard gate failed;检查 $AuditReport" } if ($ExpectedCells -gt 0 -and [int]$Audit.source_cells -ne $ExpectedCells) { throw "Source cell count 不符合当前 Japan baseline: $($Audit.source_cells) != $ExpectedCells" } Write-Host "" Write-Host "CM93 AUDIT HARD GATE: PASS" Write-Host "Source cells : $($Audit.source_cells)" Write-Host "Geometry validity issues : $($Audit.geometry_validity_issues) (informational)" # ---------------------------------------------------------------------- # STEP 7 - Resolve fresh Dataset # ---------------------------------------------------------------------- $DatasetInfoPy = Join-Path $TempRoot "dataset_info.py" @' from __future__ import annotations import json import os import sys from pathlib import Path import pymysql from dotenv import load_dotenv env_path = Path(sys.argv[1]).resolve() source_root = str(Path(sys.argv[2]).resolve()) load_dotenv(dotenv_path=env_path) conn = pymysql.connect( host=os.getenv("CM93_DB_HOST", "127.0.0.1"), port=int(os.getenv("CM93_DB_PORT", "3306")), user=os.getenv("CM93_DB_USER", "root"), password=os.getenv("CM93_DB_PASSWORD", ""), database=os.getenv("CM93_DB_NAME", "cm93_chart"), charset=os.getenv("CM93_DB_CHARSET", "utf8mb4"), cursorclass=pymysql.cursors.DictCursor, ) try: with conn.cursor() as cur: cur.execute("SELECT COUNT(*) AS n FROM chart_dataset") dataset_count = int(cur.fetchone()["n"]) cur.execute( """ SELECT d.id, d.name, d.source_root, COUNT(c.id) AS cells, SUM(c.import_status='DONE') AS done_cells, SUM(c.import_status='FAILED') AS failed_cells FROM chart_dataset d LEFT JOIN chart_cell c ON c.dataset_id=d.id WHERE d.source_type='CM93' AND d.source_root=%s GROUP BY d.id,d.name,d.source_root """, (source_root,), ) rows = cur.fetchall() if dataset_count != 1: raise RuntimeError( f"fresh DB expected exactly 1 dataset, got {dataset_count}" ) if len(rows) != 1: raise RuntimeError( f"expected one dataset for source_root={source_root}, got {len(rows)}" ) row = rows[0] print( json.dumps( { "id": int(row["id"]), "name": row["name"], "source_root": row["source_root"], "cells": int(row["cells"] or 0), "done_cells": int(row["done_cells"] or 0), "failed_cells": int(row["failed_cells"] or 0), }, ensure_ascii=False, ) ) finally: conn.close() '@ | Set-Content $DatasetInfoPy -Encoding UTF8 $DatasetText = (& $Python $DatasetInfoPy $EnvPath $SourceRoot | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($DatasetText)) { throw "无法读取 fresh Dataset 信息" } $Dataset = $DatasetText | ConvertFrom-Json $DatasetId = [int]$Dataset.id if ([int]$Dataset.cells -ne [int]$Audit.source_cells) { throw "DB cell count != audit source_cells ($($Dataset.cells) != $($Audit.source_cells))" } if ([int]$Dataset.done_cells -ne [int]$Audit.source_cells) { throw "不是所有 Cell 都 DONE" } if ([int]$Dataset.failed_cells -ne 0) { throw "存在 FAILED Cell" } Write-Step "STEP 7 - Fresh Dataset confirmed" Write-Host "Dataset ID : $DatasetId" Write-Host "Name : $($Dataset.name)" Write-Host "SourceRoot : $($Dataset.source_root)" Write-Host "Cells : $($Dataset.cells)" Write-Host "DONE : $($Dataset.done_cells)" Write-Host "FAILED : $($Dataset.failed_cells)" # ---------------------------------------------------------------------- # STEP 8 - Dataset-aware mapping gates # ---------------------------------------------------------------------- $GapAudit = ".\build\production\cm95_global_mapping_gap_audit_dataset_${DatasetId}.json" $GapClosure = ".\build\production\cm95_global_mapping_gap_closure_dataset_${DatasetId}.json" $MappingContract = ".\build\production\cm95_mapping_contract_check_dataset_${DatasetId}.json" Invoke-Checked "STEP 8A - Global mapping gap audit (fresh Dataset)" { & $Python ".\tools\cm95_global_mapping_gap_audit_v1.py" ` --dataset-id $DatasetId ` --mapping ".\config\mappings\cm95_to_navsea_v1.yaml" ` --output $GapAudit } Invoke-Checked "STEP 8B - Mapping gap classification closure" { & $Python ".\tools\cm95_global_mapping_gap_closure_audit_v2.py" ` --gap-audit $GapAudit ` --mapping ".\config\mappings\cm95_to_navsea_v1.yaml" ` --disposition ".\config\mappings\cm95_gap_disposition_v1.yaml" ` --output $GapClosure } $Closure = Get-Content $GapClosure -Raw | ConvertFrom-Json if ([string]$Closure.overall -ne "PASS") { Write-Host "" Write-Host "NEW DATASET MAPPING GATE STOPPED PIPELINE." Write-Host "新 Japan Dataset 有当前 disposition 尚未收口的 gap variant。" Write-Host "" Write-Host "检查:" Write-Host " $GapAudit" Write-Host " $GapClosure" throw "Mapping gap closure FAIL;PBF generation intentionally stopped." } Invoke-Checked "STEP 8C - Mapping / Style / Sprite static contract" { & $Python ".\tools\cm95_mapping_contract_check_v1.py" ` --mapping ".\config\mappings\cm95_to_navsea_v1.yaml" ` --style ".\config\style\style.navsea-delivery-full-semantic-unified-20260808-cm95-v1.json" ` --sprite-json ".\config\sprite\semantic-sprite-20260808.json" ` --output $MappingContract } $MapContract = Get-Content $MappingContract -Raw | ConvertFrom-Json if ([string]$MapContract.overall -ne "PASS") { throw "Mapping / Style / Sprite static contract FAIL" } # ---------------------------------------------------------------------- # STEP 8D - Build Dataset-specific effective gap disposition # ---------------------------------------------------------------------- $EffectiveDisposition = ".\build\production\cm95_gap_disposition_dataset_${DatasetId}_effective.yaml" $EffectiveDispositionPy = Join-Path $TempRoot "build_effective_disposition.py" @' from __future__ import annotations import copy import sys from pathlib import Path import yaml template_path = Path(sys.argv[1]) gap_path = Path(sys.argv[2]) closure_path = Path(sys.argv[3]) dataset_id = int(sys.argv[4]) out_path = Path(sys.argv[5]) template = yaml.safe_load(template_path.read_text(encoding="utf-8")) or {} gap = yaml.safe_load(gap_path.read_text(encoding="utf-8")) or {} closure = yaml.safe_load(closure_path.read_text(encoding="utf-8")) or {} template_by_key = { ( str(v.get("object_class") or ""), str(v.get("geometry_type") or "").upper(), ): v for v in template.get("variants") or [] if isinstance(v, dict) } effective_variants = [] for item in gap.get("gap_variants") or []: key = ( str(item.get("object_class") or ""), str(item.get("geometry_type") or "").upper(), ) src = template_by_key.get(key) if src is None: raise RuntimeError( f"unclassified gap variant while building effective disposition: {key}" ) row = copy.deepcopy(src) row["object_class"] = key[0] row["geometry_type"] = key[1] row["feature_count"] = int(item.get("feature_count") or 0) if item.get("object_class_id") is not None: row["object_class_id"] = item.get("object_class_id") if item.get("dictionary_description") is not None: row["dictionary_description"] = item.get("dictionary_description") if item.get("gap_reason") is not None: row["gap_reason_v1"] = item.get("gap_reason") effective_variants.append(row) pending = [ v for v in effective_variants if str(v.get("disposition") or "") == "ENGINEERING_ONLY_PENDING_SEMANTIC_REVIEW" ] delivery = [ v for v in effective_variants if str(v.get("disposition") or "") == "DELIVERY_MAPPED_V1_4_3" ] existing = [ v for v in effective_variants if str(v.get("disposition") or "").startswith("EXISTING_") ] result = { "schema": "navsea.cm95.gap-disposition/v1", "version": "v1.4.5-dataset-effective", "dataset_id": dataset_id, "source_audit": str(gap_path), "policy": ( "Dataset-specific effective disposition generated from " "cm95_gap_disposition_v1.yaml after current-dataset closure PASS." ), "summary": { "observed_gap_variants": len(effective_variants), "delivery_mapped_v1_4_3": len(delivery), "existing_explicit_non_delivery": len(existing), "pending_semantic_review": len(pending), "delivery_mapped_v1_4_3_feature_rows": sum( int(v.get("feature_count") or 0) for v in delivery ), "existing_explicit_non_delivery_feature_rows": sum( int(v.get("feature_count") or 0) for v in existing ), "pending_semantic_review_feature_rows": sum( int(v.get("feature_count") or 0) for v in pending ), }, "variants": effective_variants, } out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text( yaml.safe_dump( result, allow_unicode=True, sort_keys=False, width=120, ), encoding="utf-8", ) print(f"Effective disposition: {out_path}") print(f"Observed gap variants : {len(effective_variants)}") print(f"Pending review : {len(pending)}") print( "Pending rows : " f"{result['summary']['pending_semantic_review_feature_rows']}" ) '@ | Set-Content $EffectiveDispositionPy -Encoding UTF8 Invoke-Checked "STEP 8D - Dataset-specific semantic disposition summary" { & $Python $EffectiveDispositionPy ` ".\config\mappings\cm95_gap_disposition_v1.yaml" ` $GapAudit ` $GapClosure ` $DatasetId ` $EffectiveDisposition } # ---------------------------------------------------------------------- # STEP 9 - Fresh deterministic FID manifest # ---------------------------------------------------------------------- $Manifest = ".\build\cm95_fid_manifest_dataset_${DatasetId}_v1.sqlite" Invoke-Checked "STEP 9 - Rebuild FID manifest from zero" { & $Cm95Pbf build-fid-manifest ` --dataset-id $DatasetId ` --manifest $Manifest ` --rebuild-fid-manifest } # ---------------------------------------------------------------------- # STEP 10 - Fresh immutable production plan # ---------------------------------------------------------------------- $Plan = ".\build\production\cm95_production_plan_dataset_${DatasetId}_v1_4_5.json" $PlanAudit = ".\build\production\cm95_production_plan_audit_dataset_${DatasetId}_v1_4_5.json" Invoke-Checked "STEP 10A - plan-production z0-z12" { & $Cm95Pbf plan-production ` --dataset-id $DatasetId ` --zooms 0-12 ` --profile delivery ` --manifest $Manifest ` --output $Plan } Invoke-Checked "STEP 10B - production plan audit" { & $Python ".\tools\cm95_production_plan_audit_v1.py" ` --plan $Plan ` --expected-zooms 0-12 ` --output $PlanAudit } $PlanObj = Get-Content $Plan -Raw | ConvertFrom-Json $PlanAuditObj = Get-Content $PlanAudit -Raw | ConvertFrom-Json if ([string]$PlanAuditObj.overall -ne "PASS") { throw "Production plan audit FAIL" } $ReleaseId = [string]$PlanObj.release_id if ([string]::IsNullOrWhiteSpace($ReleaseId)) { throw "Plan 没有 release_id" } Write-Host "" Write-Host "Plan ID : $($PlanObj.plan_id)" Write-Host "Release ID : $ReleaseId" Write-Host "Jobs : $($PlanObj.job_count)" # ---------------------------------------------------------------------- # STEP 11 - Full PBF build # ---------------------------------------------------------------------- Invoke-Checked "STEP 11 - build-production FULL" { & $Cm95Pbf build-production ` --plan $Plan ` --release-root ".\build\releases" ` --workers $Workers ` --max-attempts 3 } # ---------------------------------------------------------------------- # STEP 12 - Production status # ---------------------------------------------------------------------- Write-Step "STEP 12 - Production status" $StatusText = ( & $Cm95Pbf status-production ` --plan $Plan ` --release-root ".\build\releases" | Out-String ).Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($StatusText)) { throw "status-production FAILED" } Write-Host $StatusText $Status = $StatusText | ConvertFrom-Json $States = $Status.states $PassCount = Get-StateCount $States "PASS" $FailedCount = Get-StateCount $States "FAILED" $PendingCount = Get-StateCount $States "PENDING" $RunningCount = Get-StateCount $States "RUNNING" if ($PassCount -ne [int]$Status.jobs -or $FailedCount -ne 0 -or $PendingCount -ne 0 -or $RunningCount -ne 0) { throw "Production state gate FAIL" } # ---------------------------------------------------------------------- # STEP 13 - Final release gate # ---------------------------------------------------------------------- Invoke-Checked "STEP 13 - finalize-production" { & $Cm95Pbf finalize-production ` --plan $Plan ` --release-root ".\build\releases" ` --style ".\config\style\style.navsea-delivery-full-semantic-unified-20260808-cm95-v1.json" ` --sprite-json ".\config\sprite\semantic-sprite-20260808.json" ` --gap-disposition $EffectiveDisposition } $ReleaseDir = Join-Path $ProjectRoot ("build\releases\" + $ReleaseId) $ReleaseManifest = Join-Path $ReleaseDir "release_manifest_v1.json" if (-not (Test-Path $ReleaseManifest -PathType Leaf)) { throw "finalize 已运行,但 release manifest 不存在: $ReleaseManifest" } $Final = Get-Content $ReleaseManifest -Raw | ConvertFrom-Json if ([string]$Final.status -ne "PASS") { throw "Final release manifest status != PASS" } # ---------------------------------------------------------------------- # STEP 14 - Save compact baseline summary # ---------------------------------------------------------------------- $SummaryPath = ".\build\production\CM95_JAPAN_FULL_REBUILD_CURRENT.json" $Summary = [ordered]@{ schema = "navsea.cm95.japan-full-rebuild-pipeline/v2" completed_at = (Get-Date).ToString("o") project_root = $ProjectRoot source_root = $SourceRoot database = $DbName dataset_id = $DatasetId source_cells = [int]$Audit.source_cells source_geometry_validity_issues = [int]$Audit.geometry_validity_issues audit_report = $AuditReport mapping_gap_audit = $GapAudit mapping_gap_closure = [string]$Closure.overall effective_gap_disposition = $EffectiveDisposition semantic_completeness = [string]$Final.semantic_completeness semantic_backlog = $Final.semantic_backlog fid_manifest = $Manifest plan = $Plan plan_id = [string]$PlanObj.plan_id release_id = $ReleaseId release_dir = $ReleaseDir release_manifest = $ReleaseManifest jobs = [int]$Status.jobs pbf_files = [int]$Status.pbf_files output_features = [int64]$Status.output_features final_status = [string]$Final.status } $Summary | ConvertTo-Json -Depth 12 | Set-Content $SummaryPath -Encoding UTF8 Write-Step "PIPELINE COMPLETE / PASS" Write-Host "Database : $DbName" Write-Host "Dataset ID : $DatasetId" Write-Host "Source cells : $($Audit.source_cells)" Write-Host "Plan ID : $($PlanObj.plan_id)" Write-Host "Release ID : $ReleaseId" Write-Host "Jobs : $($Status.jobs) PASS" Write-Host "PBF files : $($Status.pbf_files)" Write-Host "Output features: $($Status.output_features)" Write-Host "Final status : $($Final.status)" Write-Host "Semantic : $($Final.semantic_completeness)" Write-Host "" Write-Host "Summary:" Write-Host " $SummaryPath" Write-Host "" Write-Host "Release:" Write-Host " $ReleaseDir" Write-Host "" Write-Host "Web test:" Write-Host " cd `"$ProjectRoot`"" Write-Host " & `".\.venv\Scripts\python.exe`" .\web\cm95_web.py --release-id `"$ReleaseId`"" } finally { Remove-Item $TempRoot -Recurse -Force -ErrorAction SilentlyContinue }