feat: add legal source diff review
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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,
|
||||
count(*) as article_count,
|
||||
count(distinct text_checksum) as text_checksum_count,
|
||||
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)
|
||||
Reference in New Issue
Block a user