Files
CM95/scripts/cm95_japan_full_rebuild_pipeline.ps1

947 lines
28 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}