Files
ai-contract-platform/scripts/run-legal-source-diff-report.ps1
T

250 lines
8.8 KiB
PowerShell

param(
[string]$LawsBackupDir = "E:\DB_Backups\192.168.3.90_20260519_114618\file_backups\cn-laws-download_20260519_155247",
[string]$ArtifactRoot = "",
[string]$RemoteHost = "192.168.3.90",
[string]$RemoteUser = "root",
[string]$BackendContainer = "ai-contract-backend-restore",
[string]$SshKeyPath = "$HOME\.ssh\id_rsa",
[switch]$UseSshpass,
[string]$TenantId = "tenant-demo-001",
[string]$PrepareSummaryJson = "",
[string]$ImportSummaryJson = ""
)
$ErrorActionPreference = "Stop"
function Resolve-SshpassPath {
$command = Get-Command sshpass.exe -ErrorAction SilentlyContinue
if ($null -ne $command) {
return $command.Source
}
$candidates = @(
(Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Packages\xhcoding.sshpass-win32_Microsoft.Winget.Source_8wekyb3d8bbwe\sshpass.exe")
)
foreach ($candidate in $candidates) {
if (Test-Path $candidate) {
return $candidate
}
}
return $null
}
function Invoke-LoggedCommand {
param(
[string]$Name,
[string]$FilePath,
[string[]]$Arguments,
[string]$LogDir
)
$logPath = Join-Path $LogDir "$Name.log"
$previousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$output = & $FilePath @Arguments 2>&1
$exitCode = if ($LASTEXITCODE -is [int]) { $LASTEXITCODE } else { 0 }
} finally {
$ErrorActionPreference = $previousErrorActionPreference
}
$outputText = ($output -join [Environment]::NewLine)
$outputText | Set-Content -Encoding UTF8 -Path $logPath
if ($exitCode -ne 0) {
throw "$Name failed with exit code $exitCode. See $logPath"
}
return $outputText
}
function Get-JsonFromOutput {
param([string]$Text)
$start = $Text.IndexOf("{")
if ($start -lt 0) {
throw "Remote output did not contain a JSON object."
}
return $Text.Substring($start)
}
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
throw "GSTACK_MISSING: gstack is required for AI-assisted work in this repo."
}
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\legal-source-diff-report"
}
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss"
$artifactDir = Join-Path $ArtifactRoot $timestamp
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
if ([string]::IsNullOrWhiteSpace($PrepareSummaryJson)) {
$PrepareSummaryJson = Join-Path $repoRoot "project-docs\qa-artifacts\cn-laws-download-import\2026-05-20T09-53-00\cn_laws_prepare_summary_20260520.json"
}
if ([string]::IsNullOrWhiteSpace($ImportSummaryJson)) {
$ImportSummaryJson = Join-Path $repoRoot "project-docs\qa-artifacts\cn-laws-download-import\2026-05-20T09-53-00\cn_laws_import_summary_20260520.json"
}
foreach ($requiredPath in @($LawsBackupDir, $PrepareSummaryJson, $ImportSummaryJson)) {
if (-not (Test-Path $requiredPath)) {
throw "Required input path does not exist: $requiredPath"
}
}
$authMode = ""
$sshpassPath = $null
if ($UseSshpass -or -not [string]::IsNullOrWhiteSpace($env:SSHPASS)) {
$sshpassPath = Resolve-SshpassPath
if ([string]::IsNullOrWhiteSpace($sshpassPath)) {
throw "sshpass.exe not found. Install sshpass or use SSH key auth."
}
if ([string]::IsNullOrWhiteSpace($env:SSHPASS)) {
throw "SSHPASS environment variable is empty."
}
$authMode = "sshpass"
} elseif (Test-Path $SshKeyPath) {
$authMode = "key"
} else {
throw "SSH key not found and SSHPASS is not configured."
}
$remoteScript = @'
from __future__ import annotations
import json
import os
from datetime import datetime, timezone
from sqlalchemy import create_engine, text
tenant_id = "__TENANT_ID__"
database_url = os.environ.get("DATABASE_URL")
if not database_url:
raise RuntimeError("DATABASE_URL is required in backend container")
engine = create_engine(database_url)
with engine.connect() as conn:
summary = dict(
conn.execute(
text(
"""
select
count(*) as legal_article_count,
count(distinct source_id) as remote_source_count,
count(distinct source_title) as remote_title_count
from legal_articles
where tenant_id = :tenant_id
"""
),
{"tenant_id": tenant_id},
).first()._mapping
)
sources = [
dict(row._mapping)
for row in conn.execute(
text(
"""
select
source_id,
min(source_title) as source_title,
min(source_type) as source_type,
min(authority) as authority,
min(effective_date) as effective_date,
min(validity_status) as validity_status,
min(region) as region,
min(official_url) as official_url,
min(source_checksum) as source_checksum,
count(distinct source_checksum) as source_checksum_count,
count(*) as article_count,
count(distinct text_checksum) as text_checksum_count,
count(*) filter (where authority is null or authority = '') as missing_authority_rows,
count(*) filter (where effective_date is null or effective_date = '') as missing_effective_date_rows,
count(*) filter (where region is null or region = '') as missing_region_rows,
count(*) filter (where tags_json is null or json_array_length(tags_json) = 0) as missing_tags_rows,
min(created_at) as first_created_at,
max(updated_at) as last_updated_at
from legal_articles
where tenant_id = :tenant_id
group by source_id
order by source_id
"""
),
{"tenant_id": tenant_id},
)
]
payload = {
"generated_at": datetime.now(timezone.utc).isoformat(),
"tenant_id": tenant_id,
"legal_article_count": int(summary["legal_article_count"] or 0),
"remote_source_count": int(summary["remote_source_count"] or 0),
"remote_title_count": int(summary["remote_title_count"] or 0),
"sources": sources,
}
print(json.dumps(payload, ensure_ascii=False, default=str, indent=2))
'@
$remoteScript = $remoteScript.Replace("__TENANT_ID__", $TenantId)
$localRemoteScript = Join-Path $artifactDir "remote-legal-source-snapshot.py"
$remoteSnapshotPath = Join-Path $artifactDir "remote-legal-source-snapshot.json"
$remoteScript | Set-Content -Encoding UTF8 -Path $localRemoteScript
$target = "${RemoteUser}@${RemoteHost}"
$remoteTempScript = "/tmp/legal_source_diff_remote_snapshot.py"
if ($authMode -eq "sshpass") {
Invoke-LoggedCommand -Name "scp-remote-legal-source-script" -FilePath $sshpassPath -Arguments @(
"-e",
"scp",
"-o", "StrictHostKeyChecking=accept-new",
"-o", "ConnectTimeout=10",
$localRemoteScript,
"${target}:$remoteTempScript"
) -LogDir $artifactDir | Out-Null
$remoteOutput = Invoke-LoggedCommand -Name "remote-legal-source-snapshot" -FilePath $sshpassPath -Arguments @(
"-e",
"ssh",
"-o", "StrictHostKeyChecking=accept-new",
"-o", "ConnectTimeout=10",
$target,
"docker cp $remoteTempScript ${BackendContainer}:$remoteTempScript && docker exec $BackendContainer sh -lc 'cd /app && python $remoteTempScript'"
) -LogDir $artifactDir
} else {
Invoke-LoggedCommand -Name "scp-remote-legal-source-script" -FilePath "scp" -Arguments @(
"-i", $SshKeyPath,
"-o", "BatchMode=yes",
"-o", "StrictHostKeyChecking=accept-new",
"-o", "ConnectTimeout=10",
$localRemoteScript,
"${target}:$remoteTempScript"
) -LogDir $artifactDir | Out-Null
$remoteOutput = Invoke-LoggedCommand -Name "remote-legal-source-snapshot" -FilePath "ssh" -Arguments @(
"-i", $SshKeyPath,
"-o", "BatchMode=yes",
"-o", "StrictHostKeyChecking=accept-new",
"-o", "ConnectTimeout=10",
$target,
"docker cp $remoteTempScript ${BackendContainer}:$remoteTempScript && docker exec $BackendContainer sh -lc 'cd /app && python $remoteTempScript'"
) -LogDir $artifactDir
}
Get-JsonFromOutput $remoteOutput | Set-Content -Encoding UTF8 -Path $remoteSnapshotPath
$toolPath = Join-Path $repoRoot "backend\tools\legal_source_diff_report.py"
$toolOutput = & python $toolPath `
--laws-backup-dir $LawsBackupDir `
--remote-source-json $remoteSnapshotPath `
--prepare-summary-json $PrepareSummaryJson `
--import-summary-json $ImportSummaryJson `
--tenant-id $TenantId `
--output-dir $artifactDir
$toolExitCode = if ($LASTEXITCODE -is [int]) { $LASTEXITCODE } else { 0 }
($toolOutput -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path (Join-Path $artifactDir "legal-source-diff-report-tool.log")
if ($toolExitCode -ne 0) {
throw "legal_source_diff_report.py failed with exit code $toolExitCode."
}
Write-Output ($toolOutput -join [Environment]::NewLine)