From f273af6b464aef551c7e44416af26b85d6dce1b6 Mon Sep 17 00:00:00 2001 From: monkey1sai <26239865+monkey1sai@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:10:18 +0800 Subject: [PATCH 1/3] feat(deploy): launch governance service from deploy Add host-native uvicorn startup for governance-service in deploy.ps1 -Build, wire HOST_GOVERNANCE_API_BASE into the Docker coordinator, and document the spec-to-done plan/evidence state. Validation: test-deploy-governance-static.ps1 and test-deploy-dryrun.ps1 pass. --- .../governance-service-deploy-state.md | 7 + .../governance-service-deploy/plan.md | 30 + .../governance-service-deploy/tasks.md | 14 + .../2026-06-16-governance-service-deploy.md | 527 ++++++++++++++++++ ...-06-16-governance-service-deploy-design.md | 210 +++++++ scripts/deploy.ps1 | 162 ++++-- scripts/lib/host-native-launcher.ps1 | 46 ++ scripts/stop-all.ps1 | 1 + scripts/tests/test-deploy-dryrun.ps1 | 17 + .../tests/test-deploy-governance-static.ps1 | 36 ++ 10 files changed, 1019 insertions(+), 31 deletions(-) create mode 100644 artifacts/spec-to-done/governance-service-deploy-state.md create mode 100644 artifacts/spec-to-done/governance-service-deploy/plan.md create mode 100644 artifacts/spec-to-done/governance-service-deploy/tasks.md create mode 100644 docs/superpowers/plans/2026-06-16-governance-service-deploy.md create mode 100644 docs/superpowers/specs/2026-06-16-governance-service-deploy-design.md create mode 100644 scripts/tests/test-deploy-governance-static.ps1 diff --git a/artifacts/spec-to-done/governance-service-deploy-state.md b/artifacts/spec-to-done/governance-service-deploy-state.md new file mode 100644 index 000000000..7e8906d9f --- /dev/null +++ b/artifacts/spec-to-done/governance-service-deploy-state.md @@ -0,0 +1,7 @@ +P0.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=spec approved; linked worktree active; main workspace avoided +P1.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=deploy.ps1 impact LOW; PowerShell helper functions not indexed, fallback tests required +P3.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=implemented launcher/deploy/stop-all/tests; static test and deploy dry-run passed +P4.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=deploy -Build -SkipKit -SkipConversion -StrictPostVerify passed; governance/coordinator/proxy health probes 200; A1 Playwright 2 passed; screenshots artifacts/e2e/a1-m1-closeout-*.png +P5.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=adversarial reviewer Carson approved; blocking findings none +P6.ready | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=user requested commit push PR; targeted static and dry-run checks passed; GitNexus detect_changes returned no changes despite dirty linked worktree +P7.done | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=local completion snapshot recorded before the later P6 commit/push/PR request diff --git a/artifacts/spec-to-done/governance-service-deploy/plan.md b/artifacts/spec-to-done/governance-service-deploy/plan.md new file mode 100644 index 000000000..39873331b --- /dev/null +++ b/artifacts/spec-to-done/governance-service-deploy/plan.md @@ -0,0 +1,30 @@ +# spec-to-done Plan: governance-service-deploy + +## Inputs + +- Spec: `docs/superpowers/specs/2026-06-16-governance-service-deploy-design.md` +- Implementation plan: `docs/superpowers/plans/2026-06-16-governance-service-deploy.md` +- Worktree: `C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec` +- Branch: `docs/governance-service-deploy-spec` +- User-facing: true + +## Gate Summary + +- P0 args: OK. +- Existing linked worktree: OK; not main workspace. +- Spec approval: Approved by reviewer agent Planck, blocking findings none after third review. +- GitNexus impact pre-scan: + - `scripts/deploy.ps1` file impact: LOW, no direct processes reported. + - `Start-HostNativeConversion` / `Start-HostNativeKit`: not indexed as symbols; fallback guard is focused static search and script tests. + +## Execution Strategy + +Implement sequentially in the existing worktree: + +1. Add `Start-HostNativeGovernance` to `scripts/lib/host-native-launcher.ps1`. +2. Wire `scripts/deploy.ps1` parameters, runtime signature, dry-run audit fields, Phase 4a lifecycle, Docker env injection, and Phase 5 probes. +3. Add governance to `scripts/stop-all.ps1`. +4. Extend `scripts/tests/test-deploy-dryrun.ps1` and add `scripts/tests/test-deploy-governance-static.ps1`. +5. Run focused syntax/static/dry-run validation. +6. Attempt runtime smoke and browser evidence if local runtime permits; otherwise record HELD at P4 with exact blocker. + diff --git a/artifacts/spec-to-done/governance-service-deploy/tasks.md b/artifacts/spec-to-done/governance-service-deploy/tasks.md new file mode 100644 index 000000000..0f7703403 --- /dev/null +++ b/artifacts/spec-to-done/governance-service-deploy/tasks.md @@ -0,0 +1,14 @@ +# spec-to-done Tasks: governance-service-deploy + +- [x] P0: Read skill, AGENTS, product/script contract, spec, implementation plan, and worktree status. +- [x] P1: Confirm spec approved and aligned. +- [x] P1: Run GitNexus impact pre-scan or documented fallback. +- [x] P3: Add `Start-HostNativeGovernance`. +- [x] P3: Wire governance into `deploy.ps1`. +- [x] P3: Update `stop-all.ps1`. +- [x] P3: Add/update tests. +- [x] P3: Run syntax/static/dry-run validation. +- [x] P4: Run backend stack preflight and runtime smoke. +- [x] P4: Collect A1 browser evidence, or HELD if browser/runtime evidence is unavailable. +- [x] P5: Run adversarial verification. +- [x] P7: Final report. diff --git a/docs/superpowers/plans/2026-06-16-governance-service-deploy.md b/docs/superpowers/plans/2026-06-16-governance-service-deploy.md new file mode 100644 index 000000000..3085b987a --- /dev/null +++ b/docs/superpowers/plans/2026-06-16-governance-service-deploy.md @@ -0,0 +1,527 @@ +# Governance Service Deploy Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `.\scripts\deploy.ps1 -Build` start and verify host-native `governance-service` so A1/M1 closeout can be operated from `http://127.0.0.1:8004/ui/#/a1` without a second terminal. + +**Architecture:** Keep `governance-service` host-native on `127.0.0.1:49102`. `deploy.ps1` manages it through `scripts/lib/host-native-launcher.ps1`, writes PID/log/signature files under `scripts\.run`, injects `HOST_GOVERNANCE_API_BASE=http://host.docker.internal:` for Docker coordinator, and verifies `/health` in Phase 5. No governance Docker container is added. + +**Tech Stack:** PowerShell 5.1 compatible deploy scripts, FastAPI/uvicorn host-native Python service, existing Docker compose web-plane, existing coordinator `/api/governance/*` proxy, existing custom PowerShell test scripts. + +--- + +## Files + +- Modify: `scripts/lib/host-native-launcher.ps1` +- Modify: `scripts/deploy.ps1` +- Modify: `scripts/stop-all.ps1` +- Modify: `scripts/tests/test-deploy-dryrun.ps1` +- Add: `scripts/tests/test-deploy-governance-static.ps1` +- Optional docs update: `governance-service/README.md` + +Do not modify existing untracked files under `artifacts/`, `test-results/`, or unrelated `docs/superpowers/*conv-coverage*` files. + +## Constraints + +- Do not work on `main`; use a feature branch. +- Before changing existing functions/classes/methods that are indexed by GitNexus, run impact analysis and report risk. For these PowerShell script edits, first try `gitnexus_impact` on `Start-HostNativeConversion`, `Start-HostNativeKit`, and deploy helper names if available; if the index is stale, run `npx gitnexus analyze` before continuing. +- Do not add a new production dependency. +- Do not Dockerize `governance-service`. +- Do not commit unless the user explicitly asks for commit/push/PR. If commit is requested later, run `gitnexus_detect_changes` before committing. +- Preserve PowerShell 5.1 compatibility. + +## Task 1: Add host-native governance launcher + +- [ ] **Step 1: Inspect current launcher patterns** + +Read: + +```powershell +Get-Content -Raw scripts\lib\host-native-launcher.ps1 +Get-Content -Raw governance-service\README.md +``` + +Confirm existing reusable functions: + +- `Start-HostNativeService` +- `Wait-HostNativeHealth` +- `Start-HostNativeConversion` +- `Start-HostNativeKit` + +- [ ] **Step 2: Add `Start-HostNativeGovernance`** + +Modify `scripts/lib/host-native-launcher.ps1`. + +Add a function after `Start-HostNativeConversion` and before `Start-HostNativeKit`: + +```powershell +function Start-HostNativeGovernance { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)][string] $RepoRoot, + [int] $Port = 49102, + [string] $DbPath = '', + [string] $FileLibraryRoot = '' + ) + $runDir = Join-Path $RepoRoot 'scripts\.run' + if (-not (Test-Path -LiteralPath $runDir)) { + New-Item -ItemType Directory -Path $runDir -Force | Out-Null + } + + $serviceRoot = Join-Path $RepoRoot 'governance-service' + $python312 = 'C:\Program Files\Python312\python.exe' + $repoVenvPython = Join-Path $RepoRoot '.venv\Scripts\python.exe' + $pythonExe = if (Test-Path -LiteralPath $python312 -PathType Leaf) { + $python312 + } elseif (Test-Path -LiteralPath $repoVenvPython -PathType Leaf) { + $repoVenvPython + } else { + 'python' + } + + Remove-Item Env:PYTHONNOUSERSITE -ErrorAction SilentlyContinue + & $pythonExe -c "import ifcopenshell, fastapi, uvicorn" *> $null + if ($LASTEXITCODE -ne 0) { + throw "governance-service Python cannot import ifcopenshell, fastapi, and uvicorn: $pythonExe" + } + + $env:GOV_PORT = "$Port" + if (-not [string]::IsNullOrWhiteSpace($DbPath)) { + $env:GOV_DB_PATH = $DbPath + } + if (-not [string]::IsNullOrWhiteSpace($FileLibraryRoot)) { + $env:BIM_FILE_LIBRARY_ROOT = $FileLibraryRoot + } + + return (Start-HostNativeService ` + -Name 'governance-service' ` + -WorkingDirectory $serviceRoot ` + -FilePath $pythonExe ` + -ArgumentList @('-m','uvicorn','app:app','--host','127.0.0.1','--port',"$Port") ` + -RunDir $runDir) +} +``` + +Expected result: the function delegates all process/log/PID behavior to `Start-HostNativeService` and does not duplicate `Start-Process`. + +- [ ] **Step 3: Syntax check** + +Run: + +```powershell +powershell -NoProfile -Command "[scriptblock]::Create((Get-Content -Raw scripts\lib\host-native-launcher.ps1)) | Out-Null" +``` + +Expected: exit code 0. + +## Task 2: Wire governance into deploy parameters and runtime state + +- [ ] **Step 1: Add deploy parameters** + +Modify `scripts/deploy.ps1` param block: + +```powershell +[switch] $SkipGovernance, +[int] $GovernancePort = 49102, +``` + +Place them near `-SkipConversion` because all three are host-native runtime controls. + +- [ ] **Step 2: Add script-scope paths** + +Near the existing runtime signature paths, add: + +```powershell +$script:governanceRuntimeSignaturePath = Join-Path $RunDir 'governance-service.params.json' +``` + +- [ ] **Step 3: Add governance runtime helpers** + +Near `New-ConversionRuntimeSignature`, add: + +```powershell +function New-GovernanceRuntimeSignature { + param( + [Parameter(Mandatory = $true)][int] $Port, + [Parameter(Mandatory = $true)][string] $DbPath, + [Parameter(Mandatory = $true)][string] $FileLibraryRoot + ) + return ([pscustomobject]@{ + host = '127.0.0.1' + port = $Port + dbPath = $DbPath + fileLibraryRoot = $FileLibraryRoot + } | ConvertTo-Json -Compress) +} +``` + +Reuse `Test-KitRuntimeSignatureMatches` and `Set-KitRuntimeSignature` for this signature file unless you choose to rename those generic helpers in a separate, GitNexus-checked refactor. Do not do that rename in this task. + +- [ ] **Step 4: Resolve governance settings after env/volume resolution** + +After `$volume` is resolved and before Phase 3 port audit, define: + +```powershell +$resolvedGovernancePort = Resolve-DeployIntValue ` + -Name 'GOV_PORT' ` + -EnvFile $resolvedEnvFile ` + -Default 49102 ` + -ExplicitValue $GovernancePort ` + -HasExplicitValue ` + -Min 1 ` + -Max 65535 + +$resolvedGovernanceDbPath = Join-Path $RepoRoot 'storage\governance.db' +$resolvedGovernanceFileLibraryRoot = if ($volume -and $volume.runtimeStorageRoot) { + $volume.runtimeStorageRoot +} else { + Join-Path $RepoRoot 'storage' +} +$governanceRuntimeSignature = New-GovernanceRuntimeSignature ` + -Port $resolvedGovernancePort ` + -DbPath $resolvedGovernanceDbPath ` + -FileLibraryRoot $resolvedGovernanceFileLibraryRoot +$resolvedGovernanceApiBaseForDocker = if ($SkipGovernance) { '' } else { "http://host.docker.internal:$resolvedGovernancePort" } +if (-not $SkipGovernance) { + [Environment]::SetEnvironmentVariable('HOST_GOVERNANCE_API_BASE', $resolvedGovernanceApiBaseForDocker, 'Process') + if ($PSBoundParameters.ContainsKey('GovernancePort') -or $resolvedGovernancePort -ne 49102) { + $shouldRefreshWebPlane = $true + } +} +``` + +- [ ] **Step 5: Add governance fields to dry-run audit** + +In the `$auditObj.runtime` object, add: + +```powershell +governanceSkipped = [bool]$SkipGovernance +governancePort = $resolvedGovernancePort +governanceApiBaseForDocker = $resolvedGovernanceApiBaseForDocker +governanceDbPath = $resolvedGovernanceDbPath +governanceFileLibraryRoot = $resolvedGovernanceFileLibraryRoot +``` + +Expected: `deploy.ps1 -DryRun` still exits before Phase 4, but `scripts\.run\deploy-audit.json` records whether governance would be started and which endpoint Docker coordinator would use. + +- [ ] **Step 6: Include governance in port audit** + +Find the Phase 3 call: + +```powershell +$ports = Test-PortAvailability -RepoRoot $RepoRoot -KitSignalPort $resolvedKitSignalPort -KitMediaPort $resolvedKitMediaPort -ExtraHostNativePorts $resolvedSpectatorSignalPorts -ExtraHostNativeUdpPorts $resolvedSpectatorMediaPorts +``` + +Change it to include governance port unless skipped: + +```powershell +$extraHostNativePorts = @($resolvedSpectatorSignalPorts) +if (-not $SkipGovernance) { $extraHostNativePorts += $resolvedGovernancePort } +$ports = Test-PortAvailability -RepoRoot $RepoRoot -KitSignalPort $resolvedKitSignalPort -KitMediaPort $resolvedKitMediaPort -ExtraHostNativePorts $extraHostNativePorts -ExtraHostNativeUdpPorts $resolvedSpectatorMediaPorts +``` + +Expected: a stranger process on 49102 triggers the existing Phase 3 guard. + +## Task 3: Add Phase 4 governance lifecycle + +- [ ] **Step 1: Insert governance before conversion** + +Change Phase 4 comment to: + +```powershell +# Phase 4: Start (依賴順序 4a → 4b → 4c → 4d) +``` + +Insert a new block before current conversion block: + +```powershell +# 4a: host-native governance-service +if ($SkipGovernance) { + Write-DeployTag -Tag 'skip' -Message 'Phase 4a host-native governance (--SkipGovernance)' -LogPath $LogPath | Out-Null +} else { + $governanceHealthUrl = "http://127.0.0.1:$resolvedGovernancePort/health" + $governanceAlreadyRunning = Test-AlreadyRunning -Name 'governance-service' -RunDir $RunDir + if ($governanceAlreadyRunning -and -not (Test-KitRuntimeSignatureMatches -Path $script:governanceRuntimeSignaturePath -Expected $governanceRuntimeSignature)) { + Write-DeployTag -Tag 'fix' -Message 'Phase 4a restarting host-native governance because runtime parameters changed' -LogPath $LogPath | Out-Null + Stop-HostNativeService -Name 'governance-service' -RunDir $RunDir | Out-Null + $governanceAlreadyRunning = $false + } + if ($governanceAlreadyRunning) { + if (Wait-HostNativeHealth -Name 'governance-service' -Url $governanceHealthUrl -TimeoutSec 5) { + Write-DeployTag -Tag 'skip' -Message "Phase 4a host-native governance already running ($governanceHealthUrl 200)" -LogPath $LogPath | Out-Null + } else { + Write-DeployTag -Tag 'fix' -Message "Phase 4a restarting host-native governance because wrapper is alive but $governanceHealthUrl is unhealthy" -LogPath $LogPath | Out-Null + Stop-HostNativeService -Name 'governance-service' -RunDir $RunDir | Out-Null + $governanceAlreadyRunning = $false + } + } + if (-not $governanceAlreadyRunning) { + Write-DeployTag -Tag 'ok' -Message 'Phase 4a starting host-native governance-service' -LogPath $LogPath | Out-Null + $startInfo = Start-HostNativeGovernance ` + -RepoRoot $RepoRoot ` + -Port $resolvedGovernancePort ` + -DbPath $resolvedGovernanceDbPath ` + -FileLibraryRoot $resolvedGovernanceFileLibraryRoot + Write-DeployTag -Tag 'ok' -Message "governance PID=$($startInfo.Pid) log=$($startInfo.LogPath)" -LogPath $LogPath | Out-Null + $ok = Wait-HostNativeHealth -Name 'governance-service' -Url $governanceHealthUrl -TimeoutSec 30 + if (-not $ok) { + Write-DeployTag -Tag 'fail' -Message "stage=4a Phase 4a governance-service $governanceHealthUrl did not return 200 within 30s" -LogPath $LogPath | Out-Null + Print-FinalSummary -ExitCode 4 -FailedPhase 'Phase 4a (governance)' + exit 4 + } + Set-KitRuntimeSignature -Path $script:governanceRuntimeSignaturePath -Value $governanceRuntimeSignature + Write-DeployTag -Tag 'ok' -Message "Phase 4a governance-service ready ($governanceHealthUrl 200)" -LogPath $LogPath | Out-Null + } +} +``` + +- [ ] **Step 2: Renumber existing Phase 4 labels** + +Change: + +- conversion `4a` -> `4b` +- Kit `4b` -> `4c` +- docker compose `4c` -> `4d` + +This includes comments, log messages, `stage=...`, and `FailedPhase` labels. + +- [ ] **Step 3: Inject Docker coordinator governance base URL** + +Before starting `scripts\start-web-plane-docker.ps1`, add: + +```powershell +if (-not $SkipGovernance) { + [Environment]::SetEnvironmentVariable( + 'HOST_GOVERNANCE_API_BASE', + "http://host.docker.internal:$resolvedGovernancePort", + 'Process' + ) +} +``` + +Expected: `compose.host-kit.yml` passes `GOVERNANCE_API_BASE` into the coordinator container. + +## Task 4: Add Phase 5 governance verification + +- [ ] **Step 1: Probe direct governance health** + +In Phase 5, after `Probe-Url` helper definitions and before conversion probe, add: + +```powershell +if (-not $SkipGovernance) { + if (-not (Probe-Url -Name 'governance' -Url "http://127.0.0.1:$resolvedGovernancePort/health")) { $verifyFails += 'governance' } +} +``` + +- [ ] **Step 2: Probe coordinator governance proxy when Docker is running** + +If `governanceProxy.ts` has no `/api/governance/health` route, do not add one in this deploy task. Instead, use the existing browser-facing file-library health proxy: + +```powershell +if (-not $SkipDocker -and -not $SkipGovernance) { + if (-not (Probe-Url -Name 'coordinator-governance-files-tree' -Url 'http://127.0.0.1:8004/api/governance/files/tree')) { $verifyFails += 'coordinator-governance-files-tree' } +} +``` + +Expected: this verifies Docker coordinator can reach host-native governance through `host.docker.internal`. + +## Task 5: Update stop-all + +- [ ] **Step 1: Add governance expected service** + +Modify `scripts/stop-all.ps1`. + +Add to `$ExpectedServices`: + +```powershell +@{ Name = "governance-service"; Ports = @(49102) }, +``` + +Place it near the other host-native services. + +- [ ] **Step 2: Verify stop-all syntax** + +Run: + +```powershell +powershell -NoProfile -Command "[scriptblock]::Create((Get-Content -Raw scripts\stop-all.ps1)) | Out-Null" +``` + +Expected: exit code 0. + +## Task 6: Update dry-run and static tests + +- [ ] **Step 1: Extend `test-deploy-dryrun.ps1`** + +Modify `scripts/tests/test-deploy-dryrun.ps1` so it asserts dry-run audit contains governance runtime intent without entering Phase 4. + +After the existing `deploy-audit.json` check, add: + +```powershell +$audit = Get-Content -LiteralPath $auditJson -Raw | ConvertFrom-Json +Assert-Equal 49102 $audit.runtime.governancePort 'default governance port is 49102' +Assert-Equal $false $audit.runtime.governanceSkipped 'default governance is not skipped' +Assert-Equal 'http://host.docker.internal:49102' $audit.runtime.governanceApiBaseForDocker 'default Docker governance base URL' +Write-TestPass 'governance dry-run audit defaults' +``` + +Also add a skip test invocation: + +```powershell +$skipGovernanceOutput = & powershell -NoProfile -ExecutionPolicy Bypass -File $deploy -DryRun -SkipGovernance *>&1 | Out-String +Assert-True (-not ($skipGovernanceOutput -match 'Phase 4:')) 'Phase 4 not entered under -DryRun -SkipGovernance' +$skipGovernanceAudit = Get-Content -LiteralPath $auditJson -Raw | ConvertFrom-Json +Assert-Equal $true $skipGovernanceAudit.runtime.governanceSkipped 'governance skip state recorded in dry-run audit' +Assert-Equal '' $skipGovernanceAudit.runtime.governanceApiBaseForDocker 'skipped governance has no Docker base URL' +Write-TestPass 'governance skip dry-run audit' +``` + +- [ ] **Step 2: Add static deploy governance test** + +Create `scripts/tests/test-deploy-governance-static.ps1`: + +```powershell +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$RepoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..')).Path +$deploy = Get-Content -Raw (Join-Path $RepoRoot 'scripts\deploy.ps1') +$launcher = Get-Content -Raw (Join-Path $RepoRoot 'scripts\lib\host-native-launcher.ps1') +$stopAll = Get-Content -Raw (Join-Path $RepoRoot 'scripts\stop-all.ps1') + +function Assert-Contains { + param([string] $Text, [string] $Pattern, [string] $Message) + if ($Text -notmatch [regex]::Escape($Pattern)) { + throw $Message + } +} + +Assert-Contains $deploy '[switch] $SkipGovernance' 'deploy.ps1 must expose -SkipGovernance' +Assert-Contains $deploy '[int] $GovernancePort = 49102' 'deploy.ps1 must expose -GovernancePort 49102' +Assert-Contains $deploy 'Start-HostNativeGovernance' 'deploy.ps1 must start governance through host-native launcher' +Assert-Contains $deploy 'HOST_GOVERNANCE_API_BASE' 'deploy.ps1 must inject coordinator governance base URL' +Assert-Contains $deploy 'coordinator-governance-files-tree' 'deploy.ps1 must verify coordinator to governance proxy' +Assert-Contains $launcher 'function Start-HostNativeGovernance' 'launcher must define Start-HostNativeGovernance' +Assert-Contains $launcher "-Name 'governance-service'" 'launcher must use governance-service PID/log name' +Assert-Contains $stopAll 'governance-service' 'stop-all.ps1 must know governance-service' + +[scriptblock]::Create($deploy) | Out-Null +[scriptblock]::Create($launcher) | Out-Null +[scriptblock]::Create($stopAll) | Out-Null +Write-Host 'PASS deploy governance static checks' +``` + +- [ ] **Step 3: Run tests** + +Run: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\tests\test-deploy-governance-static.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\tests\test-deploy-dryrun.ps1 +``` + +Expected: + +```txt +PASS deploy governance static checks +PASS ... +``` + +## Task 7: Runtime smoke + +- [ ] **Step 1: Clean current runtime** + +Run: + +```powershell +.\scripts\stop-all.ps1 +``` + +Expected: exits 0 or reports nothing relevant running. + +- [ ] **Step 2: Start A1-only deploy smoke** + +Run: + +```powershell +.\scripts\deploy.ps1 -Build -SkipKit -SkipConversion -StrictPostVerify +``` + +Expected: + +- `Phase 4a governance-service ready` +- `Phase 4d docker compose up complete` +- `verify governance http=200` +- `verify coordinator-governance-files-tree http=200` +- summary points to `http://127.0.0.1:8004/ui` + +- [ ] **Step 3: Manual HTTP probes** + +Run: + +```powershell +Invoke-WebRequest http://127.0.0.1:49102/health -UseBasicParsing +Invoke-WebRequest http://127.0.0.1:8004/health -UseBasicParsing +Invoke-WebRequest http://127.0.0.1:8004/api/governance/files/tree -UseBasicParsing +Invoke-WebRequest http://127.0.0.1:8004/ui -UseBasicParsing +``` + +Expected: all return 2xx. + +## Task 8: A1 browser evidence + +- [ ] **Step 1: Run Playwright A1 smoke** + +Use existing A1 E2E spec if available: + +```powershell +cd web-viewer-sample +npm run e2e -- --project=chromium --grep "A1" +``` + +If the grep does not select the intended test, inspect `web-viewer-sample/e2e` and run the A1 closeout spec directly. + +- [ ] **Step 2: Capture evidence** + +Evidence must include: + +- Frontend URL: `http://127.0.0.1:8004/ui/#/a1` +- Buttons tested: file picker, run rules, expand failure drawer, create issue, export Excel/BCF if present in the scenario +- Fixture: `storage/fixture-bytes.ifc` or exact fixture path used +- Visible result: five-step state reaches scored/issued/delivered; failure drawer shows GUID/name/type/storey +- Trace/screenshot path under `test-results/` or `artifacts/e2e/` + +## Task 9: Final change review + +- [ ] **Step 1: Check diff** + +Run: + +```powershell +git diff -- scripts\lib\host-native-launcher.ps1 scripts\deploy.ps1 scripts\stop-all.ps1 scripts\tests\test-deploy-dryrun.ps1 scripts\tests\test-deploy-governance-static.ps1 governance-service\README.md +git diff --check +``` + +Expected: no whitespace errors. + +- [ ] **Step 2: GitNexus detect changes** + +If implementation changed code/scripts and user asks to commit: + +```powershell +npx gitnexus detect-changes +``` + +Expected: affected scope is limited to deploy scripts, host-native launcher, stop-all, tests, and optional README. + +- [ ] **Step 3: Report status** + +Final response must include: + +- Changed files +- Validation commands and pass/fail status +- Runtime URL +- PID/log files +- Evidence path +- Known risks +- Whether commit was intentionally skipped because user did not ask for it diff --git a/docs/superpowers/specs/2026-06-16-governance-service-deploy-design.md b/docs/superpowers/specs/2026-06-16-governance-service-deploy-design.md new file mode 100644 index 000000000..6845f7341 --- /dev/null +++ b/docs/superpowers/specs/2026-06-16-governance-service-deploy-design.md @@ -0,0 +1,210 @@ +# deploy.ps1 -Build 啟動 governance-service 設計規格 + +## 文件性質 + +- 文件性質:spec design(設計文件)。 +- 需求來源:使用者要求「`deploy.ps1 -Build` 新增 `governance-service`」。 +- 權威序:code > contracts > `AGENTS.md` > 本 spec > generated wiki。 +- 對應背景:PR #213 已把 A1/M1 closeout 的五步 stepper、失敗構件抽屜、`GET /api/rule-runs/{id}/failures` 與 `/api/governance/*` proxy 做完;目前阻塞點不是 A1 功能本身,而是 canonical deploy 沒有啟動 `governance-service :49102`。 + +## 問題 + +`scripts/deploy.ps1 -Build` 目前會建置 coordinator + viewer 的 Docker web-plane,也會啟動 host-native conversion-service 與 Kit runtime,但不會啟動 `governance-service`。結果是: + +1. `/ui/#/a1` 已被打包進 coordinator `/ui`,但按 A1 rule-run 時,coordinator 的 `/api/governance/*` proxy 會連不到 `http://host.docker.internal:49102` 或 `127.0.0.1:49102`。 +2. 使用者必須另開一個 terminal 手動跑 `governance-service\app.py`,這讓 `deploy.ps1 -Build` 不是完整的 A1/M1 closeout 啟動入口。 +3. `stop-all.ps1` 不知道 `governance-service`,即使手動啟動也沒有一致的 PID/log/cleanup lifecycle。 + +## 目標 + +讓 `.\scripts\deploy.ps1 -Build` 在 canonical hybrid deploy 中一併啟動 host-native `governance-service`,使 A1/M1 closeout 可從單一 deploy 命令啟動到可操作狀態。 + +成功標準: + +1. `deploy.ps1` 預設啟動 `governance-service` 於 `127.0.0.1:49102`。 +2. Docker coordinator 仍透過 `HOST_GOVERNANCE_API_BASE=http://host.docker.internal:49102` 連到 host-native service。 +3. `scripts\.run\governance-service.pid`、`governance-service.log`、`governance-service.log.err` 由共用 host-native launcher 管理。 +4. `deploy.ps1 -DryRun` 不進 Phase 4,但會在 `scripts\.run\deploy-audit.json` 記錄 governance runtime 意圖(port、skip state、base URL),不實際啟 process。 +5. `deploy.ps1 -SkipGovernance` 可跳過 governance,且 Phase 5 不把 governance health 視為失敗。 +6. `stop-all.ps1` 會停止 `governance-service` 並清掉 PID file。 +7. `StrictPostVerify` 開啟時,governance health 失敗會讓 deploy exit 5。 +8. A1 UI 可在 `http://127.0.0.1:8004/ui/#/a1` 使用預設 fixture 走到 rule-run success/failure state,不需要第二個 terminal。 + +## 非目標 + +- 不把 `governance-service` Docker 化。此服務維持 host-native,原因是 ifcopenshell/CPU IFC 掃描與本機 IFC 檔案視角要跟 host storage 對齊。 +- 不修改 A1 stepper、失敗構件抽屜、rule engine、BCF/export 行為。 +- 不新增 production dependency。 +- 不把 `governance-service` 暴露給瀏覽器直連;瀏覽器仍只打 coordinator `/api/governance/*`。 +- 不用 `-Force` 殺無關 PID。port 被陌生 process 佔住時沿用 Phase 3 guard。 +- 不把 local `storage/`、DB、IFC、USDC 或 deploy logs commit 進 repo。 + +## 架構 + +```txt +PowerShell deploy.ps1 -Build + -> Phase 1/2 preflight + docker build + -> Phase 3 dangerous action guard + -> Phase 4a host-native governance-service :49102 + -> Phase 4b host-native conversion-service :49101 + -> Phase 4c host-native Kit :49100 + -> Phase 4d Docker web-plane coordinator/viewer + -> Phase 5 health verify + +Browser + -> http://127.0.0.1:8004/ui/#/a1 + -> /api/governance/* + -> coordinator container + -> http://host.docker.internal:49102 + -> host-native governance-service +``` + +`governance-service` 應透過共用 launcher 啟動: + +```powershell +python -m uvicorn app:app --host 127.0.0.1 --port 49102 +``` + +啟動工作目錄: + +```txt +C:\Repos\active\iot\AI-BIM-governance\governance-service +``` + +建議環境變數: + +| 變數 | 值 | 說明 | +|---|---|---| +| `GOV_PORT` | `49102` | 與 deploy 參數一致 | +| `GOV_DB_PATH` | `\storage\governance.db` | 預設 DB,沿用 service 現有預設語意 | +| `BIM_FILE_LIBRARY_ROOT` | `` 或 `\storage` | A1 file-library tree 的 host 視角 | +| `PYTHONNOUSERSITE` | unset | `governance-service` 指定 host Python312;實機依賴可能位於 user-site,launcher 需清掉此變數,避免 `python -m uvicorn` 找不到 uvicorn | + +## deploy.ps1 行為變更 + +新增參數: + +```powershell +[switch] $SkipGovernance, +[int] $GovernancePort = 49102 +``` + +新增 script-scope signature: + +```powershell +$script:governanceRuntimeSignaturePath = Join-Path $RunDir 'governance-service.params.json' +``` + +新增 runtime signature 欄位: + +```json +{ + "host": "127.0.0.1", + "port": 49102, + "dbPath": "\\storage\\governance.db", + "fileLibraryRoot": "" +} +``` + +若 PID file 已存在: + +1. signature 相符且 `/health` 回 200:skip,不重啟。 +2. signature 不相符:只停止 PID file 指向的 `governance-service` process tree,再重啟。 +3. wrapper process 活著但 `/health` 不健康:停止 PID file 指向 process tree,再重啟。 + +若 port 49102 被陌生 process 佔用: + +1. Phase 3 依既有 dangerous action guard 詢問或在 `-Force` 下處理。 +2. 不為 governance 新增繞過 guard 的 kill 行為。 + +## launcher 行為變更 + +在 `scripts/lib/host-native-launcher.ps1` 新增 `Start-HostNativeGovernance`,薄包裝既有 `Start-HostNativeService`。 + +必須使用既有 PID/log 標準: + +```txt +scripts\.run\governance-service.pid +scripts\.run\governance-service.log +scripts\.run\governance-service.log.err +``` + +Python 解析順序: + +1. 優先使用 `C:\Program Files\Python312\python.exe`,因為 `governance-service` contract 指定這個 host Python 具備 `ifcopenshell`。 +2. 若 `C:\Program Files\Python312\python.exe` 不存在,才允許 fallback 到 `\.venv\Scripts\python.exe` 或 `python`。 +3. 選定 interpreter 後,啟動前必須驗證可 import `ifcopenshell`、`fastapi`、`uvicorn`。驗證失敗要在 Phase 4a 直接 fail,不能等 `/health` timeout 才模糊失敗。 + +## compose/env 行為 + +`compose.host-kit.yml` 已有: + +```yaml +GOVERNANCE_API_BASE: ${HOST_GOVERNANCE_API_BASE:-http://host.docker.internal:49102} +``` + +deploy 預設不需改 compose。若 `-GovernancePort` 不是 `49102`,`deploy.ps1` 必須在啟 docker compose 子程序前設定 process env: + +```powershell +[Environment]::SetEnvironmentVariable( + 'HOST_GOVERNANCE_API_BASE', + "http://host.docker.internal:$resolvedGovernancePort", + 'Process' +) +``` + +當 `-GovernancePort` 明確傳入或解析後不是 `49102`,`deploy.ps1` 必須強制 refresh Docker web-plane;不得因 coordinator/viewer 已 running 就 skip compose,否則 running coordinator container 可能保留舊的 `GOVERNANCE_API_BASE`。 + +## stop-all 行為變更 + +`scripts/stop-all.ps1` 的 expected services 必須加入: + +```powershell +@{ Name = "governance-service"; Ports = @(49102) } +``` + +若後續支援非 49102 port,`stop-all.ps1` 可先以 PID file 為主,port audit 仍列 49102 預設值。本輪不要求 stop-all 解析 deploy env。 + +## 測試與驗收 + +最小文件/腳本驗證: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\tests\test-deploy-dryrun.ps1 +powershell -NoProfile -Command "[scriptblock]::Create((Get-Content -Raw scripts\deploy.ps1)) | Out-Null; [scriptblock]::Create((Get-Content -Raw scripts\stop-all.ps1)) | Out-Null; [scriptblock]::Create((Get-Content -Raw scripts\lib\host-native-launcher.ps1)) | Out-Null" +``` + +最小 runtime smoke: + +```powershell +.\scripts\stop-all.ps1 +.\scripts\deploy.ps1 -Build -SkipKit -SkipConversion -StrictPostVerify +Invoke-WebRequest http://127.0.0.1:49102/health -UseBasicParsing +Invoke-WebRequest http://127.0.0.1:8004/health -UseBasicParsing +Invoke-WebRequest http://127.0.0.1:8004/ui -UseBasicParsing +``` + +完整 A1 驗收: + +```powershell +.\scripts\deploy.ps1 -Build -StrictPostVerify +cd web-viewer-sample +npm run e2e -- --project=chromium --grep "A1" +``` + +完成回報必須列出: + +- Frontend URL:`http://127.0.0.1:8004/ui/#/a1` +- Buttons tested:檔案庫選取、執行規則檢核、展開失敗構件、建立 issue、匯出 Excel/BCF +- Test fixture used:repo local `storage/fixture-bytes.ifc` 或明確替代 fixture +- Expected visible result:五步 stepper 到 scored/issued/delivered,失敗構件抽屜可見 GUID/name/type/storey +- E2E command +- Screenshot/trace path +- Known limitations + +## 風險 + +- host-native Python 環境若缺 `ifcopenshell`,`governance-service` 可能在 `app.py` import 階段就啟動失敗;因此 Phase 4a 需要 interpreter import sanity check,不能只依賴 `/health` timeout。 +- Docker coordinator 看到的是 `host.docker.internal`;PowerShell host health 看到的是 `127.0.0.1`。兩者都要在驗收中覆蓋。 +- `-SkipGovernance` 是 escape hatch,只能用於 debug;不能用來宣告 A1/M1 deploy complete。 +- 若 `GovernancePort` 改成非 49102,所有文件與測試必須確認 `HOST_GOVERNANCE_API_BASE` 有跟著傳入 docker compose 子程序。 diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index 0205d6862..e49bd7a96 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -23,9 +23,11 @@ param( [string] $EnvFile = '', [switch] $SkipKit, [switch] $SkipConversion, + [switch] $SkipGovernance, [switch] $SkipDocker, [string] $PublicHost = '', [string] $ConversionBindHost = '', + [int] $GovernancePort = 49102, [int] $KitSignalPort = 49100, [int] $KitMediaPort = 47998, [int] $SpectatorCount = 5, @@ -70,6 +72,7 @@ $script:volume = $null $script:coordinatorPublicUrl = '' $script:viewerPublicUrl = '' $script:conversionRuntimeSignaturePath = Join-Path $RunDir 'bim-streaming-conversion-service.params.json' +$script:governanceRuntimeSignaturePath = Join-Path $RunDir 'governance-service.params.json' $script:kitRuntimeSignaturePath = Join-Path $RunDir 'bim-streaming-server.params.json' # ============================================================ @@ -363,7 +366,8 @@ function Test-WebPlaneRefreshRequired { 'WEB_VIEWER_COORDINATOR_API_BASE', 'WEB_VIEWER_COORDINATOR_SOCKET_URL', 'VIEWER_PUBLIC_BASE_URL', - 'COORDINATOR_PUBLIC_BASE_URL' + 'COORDINATOR_PUBLIC_BASE_URL', + 'HOST_GOVERNANCE_API_BASE' ) foreach ($name in $topologyEnvNames) { if (Test-DeployValueConfigured -Name $name -EnvFile $EnvFile) { return $true } @@ -405,6 +409,20 @@ function New-ConversionRuntimeSignature { } | ConvertTo-Json -Compress) } +function New-GovernanceRuntimeSignature { + param( + [Parameter(Mandatory = $true)][int] $Port, + [Parameter(Mandatory = $true)][string] $DbPath, + [Parameter(Mandatory = $true)][string] $FileLibraryRoot + ) + return ([pscustomobject]@{ + host = '127.0.0.1' + port = $Port + dbPath = $DbPath + fileLibraryRoot = $FileLibraryRoot + } | ConvertTo-Json -Compress) +} + function Test-KitRuntimeSignatureMatches { param( [Parameter(Mandatory = $true)][string] $Path, @@ -582,9 +600,36 @@ $conversionRuntimeSignature = New-ConversionRuntimeSignature ` -HealthHost $resolvedConversionHealthHost ` -PublicArtifactsUrl $resolvedConversionPublicArtifactsUrl -$ports = Test-PortAvailability -RepoRoot $RepoRoot -KitSignalPort $resolvedKitSignalPort -KitMediaPort $resolvedKitMediaPort -ExtraHostNativePorts $resolvedSpectatorSignalPorts -ExtraHostNativeUdpPorts $resolvedSpectatorMediaPorts $volume = Test-VolumeAlignment -RepoRoot $RepoRoot -EnvFile $resolvedEnvFile $script:volume = $volume +$resolvedGovernancePort = Resolve-DeployIntValue ` + -Name 'GOV_PORT' ` + -EnvFile $resolvedEnvFile ` + -Default 49102 ` + -ExplicitValue $GovernancePort ` + -HasExplicitValue:($PSBoundParameters.ContainsKey('GovernancePort')) ` + -Min 1 ` + -Max 65535 +$resolvedGovernanceDbPath = Join-Path $RepoRoot 'storage\governance.db' +$resolvedGovernanceFileLibraryRoot = if ($volume -and $volume.runtimeStorageRoot) { + $volume.runtimeStorageRoot +} else { + Join-Path $RepoRoot 'storage' +} +$governanceRuntimeSignature = New-GovernanceRuntimeSignature ` + -Port $resolvedGovernancePort ` + -DbPath $resolvedGovernanceDbPath ` + -FileLibraryRoot $resolvedGovernanceFileLibraryRoot +$resolvedGovernanceApiBaseForDocker = if ($SkipGovernance) { '' } else { "http://host.docker.internal:$resolvedGovernancePort" } +if (-not $SkipGovernance) { + [Environment]::SetEnvironmentVariable('HOST_GOVERNANCE_API_BASE', $resolvedGovernanceApiBaseForDocker, 'Process') + if ($PSBoundParameters.ContainsKey('GovernancePort') -or $resolvedGovernancePort -ne 49102) { + $shouldRefreshWebPlane = $true + } +} +$extraHostNativePorts = @($resolvedSpectatorSignalPorts) +if (-not $SkipGovernance) { $extraHostNativePorts += $resolvedGovernancePort } +$ports = Test-PortAvailability -RepoRoot $RepoRoot -KitSignalPort $resolvedKitSignalPort -KitMediaPort $resolvedKitMediaPort -ExtraHostNativePorts $extraHostNativePorts -ExtraHostNativeUdpPorts $resolvedSpectatorMediaPorts # Audit summary 印出 function Report-Audit { @@ -668,6 +713,11 @@ $auditObj = [pscustomobject]@{ conversionBindHost = $resolvedConversionBindHost conversionHealthHost = $resolvedConversionHealthHost conversionPublicArtifactsUrl = $resolvedConversionPublicArtifactsUrl + governanceSkipped = [bool]$SkipGovernance + governancePort = $resolvedGovernancePort + governanceApiBaseForDocker = $resolvedGovernanceApiBaseForDocker + governanceDbPath = $resolvedGovernanceDbPath + governanceFileLibraryRoot = $resolvedGovernanceFileLibraryRoot corsOrigins = $resolvedCorsOrigins allowedStageHosts = $resolvedAllowedStageHosts kitSignalPort = $resolvedKitSignalPort @@ -934,7 +984,9 @@ Write-DeployHeader -Title 'Phase 3: Interactive guard (dangerous actions)' # Phase 2 跑了 docker compose rm / build,docker container 與 wslrelay 等 port forwarder # 狀態可能變動。Re-audit ports 避免用 Phase 1 的 stale 資料問互動。 -$ports = Test-PortAvailability -RepoRoot $RepoRoot -KitSignalPort $resolvedKitSignalPort -KitMediaPort $resolvedKitMediaPort -ExtraHostNativePorts $resolvedSpectatorSignalPorts -ExtraHostNativeUdpPorts $resolvedSpectatorMediaPorts +$extraHostNativePorts = @($resolvedSpectatorSignalPorts) +if (-not $SkipGovernance) { $extraHostNativePorts += $resolvedGovernancePort } +$ports = Test-PortAvailability -RepoRoot $RepoRoot -KitSignalPort $resolvedKitSignalPort -KitMediaPort $resolvedKitMediaPort -ExtraHostNativePorts $extraHostNativePorts -ExtraHostNativeUdpPorts $resolvedSpectatorMediaPorts # Docker Desktop 在 Windows 用以下 process 做 container port forward,不是「陌生 PID」: $dockerForwarderNames = @('wslrelay.exe','com.docker.backend.exe','docker.exe','vpnkit.exe','vpnkit-bridge.exe') @@ -1012,40 +1064,79 @@ if ($hostNative.venv -eq 'WRONG_VERSION') { } # ============================================================ -# Phase 4: Start (依賴順序 4a → 4b → 4c) +# Phase 4: Start (依賴順序 4a → 4b → 4c → 4d) # ============================================================ Write-DeployHeader -Title 'Phase 4: Start services' -# 4a: host-native conversion-service +# 4a: host-native governance-service +if ($SkipGovernance) { + Write-DeployTag -Tag 'skip' -Message 'Phase 4a host-native governance (--SkipGovernance)' -LogPath $LogPath | Out-Null +} else { + $governanceHealthUrl = "http://127.0.0.1:$resolvedGovernancePort/health" + $governanceAlreadyRunning = Test-AlreadyRunning -Name 'governance-service' -RunDir $RunDir + if ($governanceAlreadyRunning -and -not (Test-KitRuntimeSignatureMatches -Path $script:governanceRuntimeSignaturePath -Expected $governanceRuntimeSignature)) { + Write-DeployTag -Tag 'fix' -Message 'Phase 4a restarting host-native governance because runtime parameters changed' -LogPath $LogPath | Out-Null + Stop-HostNativeService -Name 'governance-service' -RunDir $RunDir | Out-Null + $governanceAlreadyRunning = $false + } + if ($governanceAlreadyRunning) { + if (Wait-HostNativeHealth -Name 'governance-service' -Url $governanceHealthUrl -TimeoutSec 5) { + Write-DeployTag -Tag 'skip' -Message "Phase 4a host-native governance already running ($governanceHealthUrl 200)" -LogPath $LogPath | Out-Null + } else { + Write-DeployTag -Tag 'fix' -Message "Phase 4a restarting host-native governance because wrapper is alive but $governanceHealthUrl is unhealthy" -LogPath $LogPath | Out-Null + Stop-HostNativeService -Name 'governance-service' -RunDir $RunDir | Out-Null + $governanceAlreadyRunning = $false + } + } + if (-not $governanceAlreadyRunning) { + Write-DeployTag -Tag 'ok' -Message 'Phase 4a starting host-native governance-service' -LogPath $LogPath | Out-Null + $startInfo = Start-HostNativeGovernance ` + -RepoRoot $RepoRoot ` + -Port $resolvedGovernancePort ` + -DbPath $resolvedGovernanceDbPath ` + -FileLibraryRoot $resolvedGovernanceFileLibraryRoot + Write-DeployTag -Tag 'ok' -Message "governance PID=$($startInfo.Pid) log=$($startInfo.LogPath)" -LogPath $LogPath | Out-Null + $ok = Wait-HostNativeHealth -Name 'governance-service' -Url $governanceHealthUrl -TimeoutSec 30 + if (-not $ok) { + Write-DeployTag -Tag 'fail' -Message "stage=4a Phase 4a governance-service $governanceHealthUrl did not return 200 within 30s" -LogPath $LogPath | Out-Null + Print-FinalSummary -ExitCode 4 -FailedPhase 'Phase 4a (governance)' + exit 4 + } + Set-KitRuntimeSignature -Path $script:governanceRuntimeSignaturePath -Value $governanceRuntimeSignature + Write-DeployTag -Tag 'ok' -Message "Phase 4a governance-service ready ($governanceHealthUrl 200)" -LogPath $LogPath | Out-Null + } +} + +# 4b: host-native conversion-service if ($SkipConversion) { - Write-DeployTag -Tag 'skip' -Message 'Phase 4a host-native conversion (--SkipConversion)' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'skip' -Message 'Phase 4b host-native conversion (--SkipConversion)' -LogPath $LogPath | Out-Null } else { $conversionHealthUrl = "http://${resolvedConversionHealthHost}:49101/health" $conversionPublicHealthUrl = "http://${resolvedPublicHost}:49101/health" $conversionPublicHealthRequired = -not (Test-LoopbackHost -HostName $resolvedPublicHost) $conversionAlreadyRunning = Test-AlreadyRunning -Name 'bim-streaming-conversion-service' -RunDir $RunDir if ($conversionAlreadyRunning -and -not (Test-KitRuntimeSignatureMatches -Path $script:conversionRuntimeSignaturePath -Expected $conversionRuntimeSignature)) { - Write-DeployTag -Tag 'fix' -Message 'Phase 4a restarting host-native conversion because runtime parameters changed' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'fix' -Message 'Phase 4b restarting host-native conversion because runtime parameters changed' -LogPath $LogPath | Out-Null Stop-HostNativeService -Name 'bim-streaming-conversion-service' -RunDir $RunDir | Out-Null $conversionAlreadyRunning = $false } if ($conversionAlreadyRunning) { if (Wait-HostNativeHealth -Name 'conversion-service' -Url $conversionHealthUrl -TimeoutSec 5) { if ($conversionPublicHealthRequired -and -not (Wait-HostNativeHealth -Name 'conversion-service-public' -Url $conversionPublicHealthUrl -TimeoutSec 5)) { - Write-DeployTag -Tag 'fix' -Message "Phase 4a restarting host-native conversion because public health is unreachable at $conversionPublicHealthUrl" -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'fix' -Message "Phase 4b restarting host-native conversion because public health is unreachable at $conversionPublicHealthUrl" -LogPath $LogPath | Out-Null Stop-HostNativeService -Name 'bim-streaming-conversion-service' -RunDir $RunDir | Out-Null $conversionAlreadyRunning = $false } else { - Write-DeployTag -Tag 'skip' -Message "Phase 4a host-native conversion already running ($conversionHealthUrl 200)" -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'skip' -Message "Phase 4b host-native conversion already running ($conversionHealthUrl 200)" -LogPath $LogPath | Out-Null } } else { - Write-DeployTag -Tag 'fix' -Message "Phase 4a restarting host-native conversion because wrapper is alive but $conversionHealthUrl is unhealthy" -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'fix' -Message "Phase 4b restarting host-native conversion because wrapper is alive but $conversionHealthUrl is unhealthy" -LogPath $LogPath | Out-Null Stop-HostNativeService -Name 'bim-streaming-conversion-service' -RunDir $RunDir | Out-Null $conversionAlreadyRunning = $false } } if (-not $conversionAlreadyRunning) { - Write-DeployTag -Tag 'ok' -Message 'Phase 4a starting host-native conversion-service' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'ok' -Message 'Phase 4b starting host-native conversion-service' -LogPath $LogPath | Out-Null $startInfo = Start-HostNativeConversion ` -RepoRoot $RepoRoot ` -RuntimeStorageRoot $volume.runtimeStorageRoot ` @@ -1054,37 +1145,37 @@ if ($SkipConversion) { Write-DeployTag -Tag 'ok' -Message "conversion PID=$($startInfo.Pid) log=$($startInfo.LogPath)" -LogPath $LogPath | Out-Null $ok = Wait-HostNativeHealth -Name 'conversion-service' -Url $conversionHealthUrl -TimeoutSec 30 if (-not $ok) { - Write-DeployTag -Tag 'fail' -Message "stage=4a Phase 4a conversion-service $conversionHealthUrl did not return 200 within 30s" -LogPath $LogPath | Out-Null - Print-FinalSummary -ExitCode 4 -FailedPhase 'Phase 4a (conversion)' + Write-DeployTag -Tag 'fail' -Message "stage=4b Phase 4b conversion-service $conversionHealthUrl did not return 200 within 30s" -LogPath $LogPath | Out-Null + Print-FinalSummary -ExitCode 4 -FailedPhase 'Phase 4b (conversion)' exit 4 } if ($conversionPublicHealthRequired) { $publicOk = Wait-HostNativeHealth -Name 'conversion-service-public' -Url $conversionPublicHealthUrl -TimeoutSec 30 if (-not $publicOk) { - Write-DeployTag -Tag 'fail' -Message "stage=4a Phase 4a conversion-service public URL $conversionPublicHealthUrl did not return 200 within 30s" -LogPath $LogPath | Out-Null - Print-FinalSummary -ExitCode 4 -FailedPhase 'Phase 4a (conversion public reachability)' + Write-DeployTag -Tag 'fail' -Message "stage=4b Phase 4b conversion-service public URL $conversionPublicHealthUrl did not return 200 within 30s" -LogPath $LogPath | Out-Null + Print-FinalSummary -ExitCode 4 -FailedPhase 'Phase 4b (conversion public reachability)' exit 4 } } Set-KitRuntimeSignature -Path $script:conversionRuntimeSignaturePath -Value $conversionRuntimeSignature - Write-DeployTag -Tag 'ok' -Message "Phase 4a conversion-service ready ($conversionHealthUrl 200)" -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'ok' -Message "Phase 4b conversion-service ready ($conversionHealthUrl 200)" -LogPath $LogPath | Out-Null } } -# 4b: host-native Kit +# 4c: host-native Kit if ($SkipKit) { - Write-DeployTag -Tag 'skip' -Message 'Phase 4b host-native Kit (--SkipKit)' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'skip' -Message 'Phase 4c host-native Kit (--SkipKit)' -LogPath $LogPath | Out-Null } else { $kitAlreadyRunning = Test-AlreadyRunning -Name 'bim-streaming-server' -RunDir $RunDir if ($kitAlreadyRunning -and -not (Test-KitRuntimeSignatureMatches -Path $script:kitRuntimeSignaturePath -Expected $kitRuntimeSignature)) { - Write-DeployTag -Tag 'fix' -Message 'Phase 4b restarting host-native Kit because runtime parameters changed' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'fix' -Message 'Phase 4c restarting host-native Kit because runtime parameters changed' -LogPath $LogPath | Out-Null Stop-HostNativeService -Name 'bim-streaming-server' -RunDir $RunDir | Out-Null $kitAlreadyRunning = $false } if ($kitAlreadyRunning) { - Write-DeployTag -Tag 'skip' -Message 'Phase 4b host-native Kit already running with matching runtime parameters' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'skip' -Message 'Phase 4c host-native Kit already running with matching runtime parameters' -LogPath $LogPath | Out-Null } else { - Write-DeployTag -Tag 'ok' -Message 'Phase 4b starting host-native Kit streaming' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'ok' -Message 'Phase 4c starting host-native Kit streaming' -LogPath $LogPath | Out-Null $startInfo = Start-HostNativeKit ` -RepoRoot $RepoRoot ` -SignalPort $resolvedKitSignalPort ` @@ -1095,22 +1186,25 @@ if ($SkipKit) { Write-DeployTag -Tag 'ok' -Message "Kit PID=$($startInfo.Pid) log=$($startInfo.LogPath)" -LogPath $LogPath | Out-Null $kitRes = Wait-KitReady -LogPath $startInfo.LogPath -SignalPort $resolvedKitSignalPort -TimeoutSec 90 if (-not $kitRes.ready) { - Write-DeployTag -Tag 'fail' -Message "stage=4b Phase 4b Kit not ready in 90s (listen=$($null -ne $kitRes.listenPort) keyword=$($kitRes.matchedKeyword))" -LogPath $LogPath | Out-Null - Print-FinalSummary -ExitCode 4 -FailedPhase 'Phase 4b (Kit)' + Write-DeployTag -Tag 'fail' -Message "stage=4c Phase 4c Kit not ready in 90s (listen=$($null -ne $kitRes.listenPort) keyword=$($kitRes.matchedKeyword))" -LogPath $LogPath | Out-Null + Print-FinalSummary -ExitCode 4 -FailedPhase 'Phase 4c (Kit)' exit 4 } Set-KitRuntimeSignature -Path $script:kitRuntimeSignaturePath -Value $kitRuntimeSignature - Write-DeployTag -Tag 'ok' -Message "Phase 4b Kit ready (:$resolvedKitSignalPort LISTEN + '$($kitRes.matchedKeyword)')" -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'ok' -Message "Phase 4c Kit ready (:$resolvedKitSignalPort LISTEN + '$($kitRes.matchedKeyword)')" -LogPath $LogPath | Out-Null } } -# 4c: docker compose +# 4d: docker compose if ($SkipDocker) { - Write-DeployTag -Tag 'skip' -Message 'Phase 4c docker compose (--SkipDocker)' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'skip' -Message 'Phase 4d docker compose (--SkipDocker)' -LogPath $LogPath | Out-Null } elseif ($webPlaneRunning -and -not $shouldRefreshWebPlane) { - Write-DeployTag -Tag 'skip' -Message 'Phase 4c docker compose: coordinator + viewer already running' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'skip' -Message 'Phase 4d docker compose: coordinator + viewer already running' -LogPath $LogPath | Out-Null } else { - Write-DeployTag -Tag 'ok' -Message 'Phase 4c running scripts\start-web-plane-docker.ps1' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'ok' -Message 'Phase 4d running scripts\start-web-plane-docker.ps1' -LogPath $LogPath | Out-Null + if (-not $SkipGovernance) { + [Environment]::SetEnvironmentVariable('HOST_GOVERNANCE_API_BASE', $resolvedGovernanceApiBaseForDocker, 'Process') + } # 用 Start-Process 隔離子 script:start-web-plane-docker.ps1 內 $ErrorActionPreference='Stop', # 而 docker compose up 的進度訊息('Container ... Creating')會被 PowerShell 5.1 promote # 成 NativeCommandError。隔離成 new process 把它的 stderr 寫進 .err.log,不污染父流程。 @@ -1132,11 +1226,11 @@ if ($SkipDocker) { -Wait -PassThru -WindowStyle Hidden $dockerExit = $proc.ExitCode if ($dockerExit -ne 0) { - Write-DeployTag -Tag 'fail' -Message "stage=4c Phase 4c docker compose up failed (exit=$dockerExit; see scripts\.run\docker-compose-up.log + .err.log)" -LogPath $LogPath | Out-Null - Print-FinalSummary -ExitCode 4 -FailedPhase 'Phase 4c (docker)' + Write-DeployTag -Tag 'fail' -Message "stage=4d Phase 4d docker compose up failed (exit=$dockerExit; see scripts\.run\docker-compose-up.log + .err.log)" -LogPath $LogPath | Out-Null + Print-FinalSummary -ExitCode 4 -FailedPhase 'Phase 4d (docker)' exit 4 } - Write-DeployTag -Tag 'ok' -Message 'Phase 4c docker compose up complete' -LogPath $LogPath | Out-Null + Write-DeployTag -Tag 'ok' -Message 'Phase 4d docker compose up complete' -LogPath $LogPath | Out-Null } # ============================================================ @@ -1203,6 +1297,12 @@ if (-not $SkipDocker) { } elseif (-not (Probe-UiAsset -Name 'coordinator-ui-edge-console-asset' -UiUrl $coordinatorUiUrl -Html $uiProbe.Content)) { $verifyFails += 'coordinator-ui-edge-console-asset' } + if (-not $SkipGovernance) { + if (-not (Probe-Url -Name 'coordinator-governance-files-tree' -Url 'http://127.0.0.1:8004/api/governance/files/tree')) { $verifyFails += 'coordinator-governance-files-tree' } + } +} +if (-not $SkipGovernance) { + if (-not (Probe-Url -Name 'governance' -Url "http://127.0.0.1:$resolvedGovernancePort/health")) { $verifyFails += 'governance' } } if (-not $SkipConversion) { if (-not (Probe-Url -Name 'conversion' -Url 'http://127.0.0.1:49101/health')) { $verifyFails += 'conversion' } diff --git a/scripts/lib/host-native-launcher.ps1 b/scripts/lib/host-native-launcher.ps1 index 0291160fe..eec12ea2e 100644 --- a/scripts/lib/host-native-launcher.ps1 +++ b/scripts/lib/host-native-launcher.ps1 @@ -198,6 +198,52 @@ function Start-HostNativeConversion { -RunDir $runDir) } +function Start-HostNativeGovernance { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)][string] $RepoRoot, + [int] $Port = 49102, + [string] $DbPath = '', + [string] $FileLibraryRoot = '' + ) + $runDir = Join-Path $RepoRoot 'scripts\.run' + if (-not (Test-Path -LiteralPath $runDir)) { + New-Item -ItemType Directory -Path $runDir -Force | Out-Null + } + + $serviceRoot = Join-Path $RepoRoot 'governance-service' + $python312 = 'C:\Program Files\Python312\python.exe' + $repoVenvPython = Join-Path $RepoRoot '.venv\Scripts\python.exe' + $pythonExe = if (Test-Path -LiteralPath $python312 -PathType Leaf) { + $python312 + } elseif (Test-Path -LiteralPath $repoVenvPython -PathType Leaf) { + $repoVenvPython + } else { + 'python' + } + + Remove-Item Env:PYTHONNOUSERSITE -ErrorAction SilentlyContinue + & $pythonExe -c "import ifcopenshell, fastapi, uvicorn" *> $null + if ($LASTEXITCODE -ne 0) { + throw "governance-service Python cannot import ifcopenshell, fastapi, and uvicorn: $pythonExe" + } + + $env:GOV_PORT = "$Port" + if (-not [string]::IsNullOrWhiteSpace($DbPath)) { + $env:GOV_DB_PATH = $DbPath + } + if (-not [string]::IsNullOrWhiteSpace($FileLibraryRoot)) { + $env:BIM_FILE_LIBRARY_ROOT = $FileLibraryRoot + } + + return (Start-HostNativeService ` + -Name 'governance-service' ` + -WorkingDirectory $serviceRoot ` + -FilePath $pythonExe ` + -ArgumentList @('-m','uvicorn','app:app','--host','127.0.0.1','--port',"$Port") ` + -RunDir $runDir) +} + function Start-HostNativeKit { [CmdletBinding()] param( diff --git a/scripts/stop-all.ps1 b/scripts/stop-all.ps1 index 9f4d9e135..45150ff0f 100644 --- a/scripts/stop-all.ps1 +++ b/scripts/stop-all.ps1 @@ -46,6 +46,7 @@ $ResolvedStreamingPorts = @($ResolvedKitSignalPorts + $ResolvedKitStreamPorts + $ExpectedServices = @( # B-scheme T2:_bim-control(:8001) / _worker(:8005) 已自 repo 刪除 @{ Name = "bim-review-coordinator"; Ports = @(8004) }, + @{ Name = "governance-service"; Ports = @(49102) }, @{ Name = "bim-streaming-conversion-service"; Ports = @(49101) }, @{ Name = "web-viewer-sample"; Ports = @(5173) }, @{ Name = "bim-streaming-server"; Ports = $ResolvedStreamingPorts } diff --git a/scripts/tests/test-deploy-dryrun.ps1 b/scripts/tests/test-deploy-dryrun.ps1 index 7658a65a6..80f6150f9 100644 --- a/scripts/tests/test-deploy-dryrun.ps1 +++ b/scripts/tests/test-deploy-dryrun.ps1 @@ -29,10 +29,27 @@ Write-TestPass 'DRY-RUN marker' $auditJson = Join-Path $repoRoot 'scripts\.run\deploy-audit.json' Assert-True (Test-Path $auditJson) 'deploy-audit.json written' Write-TestPass 'deploy-audit.json present' +$audit = Get-Content -LiteralPath $auditJson -Raw | ConvertFrom-Json +Assert-Equal 49102 $audit.runtime.governancePort 'default governance port is 49102' +Assert-Equal $false $audit.runtime.governanceSkipped 'default governance is not skipped' +Assert-Equal 'http://host.docker.internal:49102' $audit.runtime.governanceApiBaseForDocker 'default Docker governance base URL' +Write-TestPass 'governance dry-run audit defaults' # Test 6: Phase 4 / 5 應不出現 Assert-True (-not ($output -match 'Phase 4:')) 'Phase 4 not entered under -DryRun' Write-TestPass 'Phase 4 skipped' +$skipGovernanceOutput = & powershell -NoProfile -ExecutionPolicy Bypass -File $deploy -DryRun -SkipGovernance *>&1 | Out-String +Assert-True (-not ($skipGovernanceOutput -match 'Phase 4:')) 'Phase 4 not entered under -DryRun -SkipGovernance' +$skipGovernanceAudit = Get-Content -LiteralPath $auditJson -Raw | ConvertFrom-Json +Assert-Equal $true $skipGovernanceAudit.runtime.governanceSkipped 'governance skip state recorded in dry-run audit' +Assert-Equal '' $skipGovernanceAudit.runtime.governanceApiBaseForDocker 'skipped governance has no Docker base URL' +Write-TestPass 'governance skip dry-run audit' +$customGovernanceOutput = & powershell -NoProfile -ExecutionPolicy Bypass -File $deploy -DryRun -GovernancePort 49103 *>&1 | Out-String +Assert-True (-not ($customGovernanceOutput -match 'Phase 4:')) 'Phase 4 not entered under -DryRun -GovernancePort' +$customGovernanceAudit = Get-Content -LiteralPath $auditJson -Raw | ConvertFrom-Json +Assert-Equal 49103 $customGovernanceAudit.runtime.governancePort 'custom governance port recorded in dry-run audit' +Assert-Equal 'http://host.docker.internal:49103' $customGovernanceAudit.runtime.governanceApiBaseForDocker 'custom Docker governance base URL' +Write-TestPass 'custom governance port dry-run audit' # Test 7: spectator ports must not collide with primary Kit ports $collisionOut = Join-Path $repoRoot 'scripts\.run\deploy-collision-test.out.log' diff --git a/scripts/tests/test-deploy-governance-static.ps1 b/scripts/tests/test-deploy-governance-static.ps1 new file mode 100644 index 000000000..f4eb3f1c5 --- /dev/null +++ b/scripts/tests/test-deploy-governance-static.ps1 @@ -0,0 +1,36 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$RepoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..')).Path +$deploy = Get-Content -Raw (Join-Path $RepoRoot 'scripts\deploy.ps1') +$launcher = Get-Content -Raw (Join-Path $RepoRoot 'scripts\lib\host-native-launcher.ps1') +$stopAll = Get-Content -Raw (Join-Path $RepoRoot 'scripts\stop-all.ps1') + +function Assert-Contains { + param([string] $Text, [string] $Pattern, [string] $Message) + if ($Text -notmatch [regex]::Escape($Pattern)) { + throw $Message + } +} + +Assert-Contains $deploy '[switch] $SkipGovernance' 'deploy.ps1 must expose -SkipGovernance' +Assert-Contains $deploy '[int] $GovernancePort = 49102' 'deploy.ps1 must expose -GovernancePort 49102' +Assert-Contains $deploy 'Start-HostNativeGovernance' 'deploy.ps1 must start governance through host-native launcher' +Assert-Contains $deploy 'HOST_GOVERNANCE_API_BASE' 'deploy.ps1 must inject coordinator governance base URL' +Assert-Contains $deploy "'HOST_GOVERNANCE_API_BASE'" 'deploy.ps1 must consider governance base URL in web-plane refresh inputs' +Assert-Contains $deploy '$shouldRefreshWebPlane = $true' 'deploy.ps1 must force web-plane refresh for custom governance port' +Assert-Contains $deploy 'coordinator-governance-files-tree' 'deploy.ps1 must verify coordinator to governance proxy' +Assert-Contains $launcher 'function Start-HostNativeGovernance' 'launcher must define Start-HostNativeGovernance' +Assert-Contains $launcher "-Name 'governance-service'" 'launcher must use governance-service PID/log name' +Assert-Contains $stopAll 'governance-service' 'stop-all.ps1 must know governance-service' + +$clearUserSite = $launcher.IndexOf('Remove-Item Env:PYTHONNOUSERSITE') +$importCheck = $launcher.IndexOf('import ifcopenshell, fastapi, uvicorn') +if ($clearUserSite -lt 0 -or $importCheck -lt 0 -or $clearUserSite -gt $importCheck) { + throw 'launcher must clear PYTHONNOUSERSITE before governance import sanity check' +} + +[scriptblock]::Create($deploy) | Out-Null +[scriptblock]::Create($launcher) | Out-Null +[scriptblock]::Create($stopAll) | Out-Null +Write-Host 'PASS deploy governance static checks' From 89548eb4099f3aa419ac5d0b964cadb1a60313cc Mon Sep 17 00:00:00 2001 From: monkey1sai <26239865+monkey1sai@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:26:24 +0800 Subject: [PATCH 2/3] docs(openspec): cover governance service deploy Add active OpenSpec evidence for the deploy.ps1 governance-service startup behavior so PR review governance can validate the workflow change. Validation: npx openspec validate governance-service-deploy --strict; local pr-review-agent reports warning with no blockers. --- .../governance-service-deploy-state.md | 1 + .../governance-service-deploy/design.md | 46 +++++++++++++++++++ .../governance-service-deploy/proposal.md | 34 ++++++++++++++ .../specs/one-click-deploy-hybrid/spec.md | 30 ++++++++++++ .../governance-service-deploy/tasks.md | 26 +++++++++++ 5 files changed, 137 insertions(+) create mode 100644 openspec/changes/governance-service-deploy/design.md create mode 100644 openspec/changes/governance-service-deploy/proposal.md create mode 100644 openspec/changes/governance-service-deploy/specs/one-click-deploy-hybrid/spec.md create mode 100644 openspec/changes/governance-service-deploy/tasks.md diff --git a/artifacts/spec-to-done/governance-service-deploy-state.md b/artifacts/spec-to-done/governance-service-deploy-state.md index 7e8906d9f..7afffc52e 100644 --- a/artifacts/spec-to-done/governance-service-deploy-state.md +++ b/artifacts/spec-to-done/governance-service-deploy-state.md @@ -4,4 +4,5 @@ P3.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service P4.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=deploy -Build -SkipKit -SkipConversion -StrictPostVerify passed; governance/coordinator/proxy health probes 200; A1 Playwright 2 passed; screenshots artifacts/e2e/a1-m1-closeout-*.png P5.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=adversarial reviewer Carson approved; blocking findings none P6.ready | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=user requested commit push PR; targeted static and dry-run checks passed; GitNexus detect_changes returned no changes despite dirty linked worktree +P6.fix | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=PR #215 pr-review-agent failed missing_openspec; added active OpenSpec change governance-service-deploy and validate passed P7.done | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=local completion snapshot recorded before the later P6 commit/push/PR request diff --git a/openspec/changes/governance-service-deploy/design.md b/openspec/changes/governance-service-deploy/design.md new file mode 100644 index 000000000..35683d770 --- /dev/null +++ b/openspec/changes/governance-service-deploy/design.md @@ -0,0 +1,46 @@ +# Design: governance-service-deploy + +## Context + +`governance-service` is already the loopback authority for A1/A2/A3 governance work. Before this change, a developer had to start it separately from the canonical `deploy.ps1 -Build` flow. That made `deploy.ps1` incomplete for A1/M1 closeout even though the web UI and coordinator proxy expected governance endpoints to be available. + +The design keeps governance host-native for the same reason as conversion and Kit: it uses local Python packages and local IFC/file-library state, and it is not a browser-facing service. Docker coordinator accesses it through `host.docker.internal`. + +## Goals + +- Make `scripts/deploy.ps1 -Build` start a healthy `governance-service` by default. +- Keep `governance-service` loopback-only on the host. +- Make coordinator containers use `HOST_GOVERNANCE_API_BASE` to proxy browser requests. +- Preserve an explicit opt-out for operators who do not need governance in a local run. +- Preserve dry-run auditability. + +## Non-Goals + +- Do not containerize `governance-service`. +- Do not make browsers call `127.0.0.1:49102` directly. +- Do not change A1/A2/A3 rule-run APIs. +- Do not merge governance into conversion or Kit runtime processes. + +## Decisions + +### Decision 1: Phase 4 starts governance before the existing runtime chain + +Governance becomes Phase 4a. The previous conversion / Kit / Docker stages move to 4b / 4c / 4d. This keeps coordinator startup last, after all host-native services it may proxy are ready. + +### Decision 2: Docker coordinator receives a host bridge URL + +The host process sets `HOST_GOVERNANCE_API_BASE=http://host.docker.internal:` unless `-SkipGovernance` is used. If the operator passes a non-default governance port, deploy forces a web-plane refresh so the container environment cannot stay stale. + +### Decision 3: Host Python sanity check must match real runtime imports + +The launcher clears `PYTHONNOUSERSITE` before checking imports because the supported host Python can resolve `uvicorn` through user-site packages. This mirrors the runtime used by `python -m uvicorn app:app`. + +## Verification + +- PowerShell parse check for deploy / launcher / stop scripts. +- `scripts/tests/test-deploy-governance-static.ps1` +- `scripts/tests/test-deploy-dryrun.ps1` +- `scripts/deploy.ps1 -Build -SkipKit -SkipConversion -StrictPostVerify` +- governance direct health probe: `http://127.0.0.1:49102/health` +- coordinator proxy probe: `http://127.0.0.1:8004/api/governance/files/tree` +- A1/M1 Playwright E2E evidence remains valid through the coordinator proxy. diff --git a/openspec/changes/governance-service-deploy/proposal.md b/openspec/changes/governance-service-deploy/proposal.md new file mode 100644 index 000000000..47c95e9eb --- /dev/null +++ b/openspec/changes/governance-service-deploy/proposal.md @@ -0,0 +1,34 @@ +# Proposal: governance-service-deploy + +## Why + +PR #215 changes the canonical deploy path: `scripts/deploy.ps1 -Build` no longer starts only conversion, Kit, and the Docker web plane. It now also starts the loopback `governance-service` so A1/M1 closeout can use the real governance file tree and rule-run authority through the coordinator proxy without a second terminal. + +This is a behavior / workflow change to the one-click deployment contract, so it needs explicit OpenSpec evidence for the PR review gate. + +## What Changes + +- `deploy.ps1` gains a governance stage before conversion / Kit / Docker compose startup: + - default port `49102` + - opt-out flag `-SkipGovernance` + - custom port flag `-GovernancePort` + - direct health probe `http://127.0.0.1:/health` +- `deploy.ps1` exports `HOST_GOVERNANCE_API_BASE` for the Docker coordinator so browser-facing `/api/governance/*` routes continue through `:8004`. +- `deploy.ps1` refreshes the Docker web plane when the governance API base changes, preventing a running coordinator container from keeping stale governance configuration. +- `stop-all.ps1` knows about `governance-service`. +- Dry-run and static tests cover the new deploy contract. + +## Impact + +- Affected capability: `one-click-deploy-hybrid` +- Affected files: + - `scripts/deploy.ps1` + - `scripts/lib/host-native-launcher.ps1` + - `scripts/stop-all.ps1` + - `scripts/tests/test-deploy-dryrun.ps1` + - `scripts/tests/test-deploy-governance-static.ps1` +- Non-goals: + - no change to `governance-service` rule-run semantics + - no change to browser direct-access boundary; browser still uses coordinator proxy + - no public / internet exposure of `governance-service` + - no change to Kit or conversion authority behavior diff --git a/openspec/changes/governance-service-deploy/specs/one-click-deploy-hybrid/spec.md b/openspec/changes/governance-service-deploy/specs/one-click-deploy-hybrid/spec.md new file mode 100644 index 000000000..729bcbe13 --- /dev/null +++ b/openspec/changes/governance-service-deploy/specs/one-click-deploy-hybrid/spec.md @@ -0,0 +1,30 @@ +# one-click-deploy-hybrid — Spec Delta (governance-service-deploy) + +## MODIFIED Requirements + +### Requirement: Phase 4 嚴格依賴順序 + +deploy.ps1 SHALL 在 Phase 4 嚴格按 host-native governance-service → host-native conversion-service → host-native Kit → Docker web plane 順序啟動。Coordinator container 啟動時會代理 governance 與 conversion endpoints,因此 governance-service 與 conversion-service MUST 先於 Docker coordinator ready。 + +#### Scenario: Phase 4a 啟動 host-native governance-service + +- **WHEN** deploy.ps1 未帶 `-SkipGovernance` +- **THEN** deploy.ps1 MUST 啟動 host-native `governance-service`,並以 `uvicorn app:app` 綁定 `127.0.0.1:` +- **AND** default `GovernancePort` MUST be `49102` +- **AND** deploy.ps1 MUST 等 `http://127.0.0.1:/health` 回 200;timeout 視為 stage=4a fail,退 4 +- **AND** deploy.ps1 MUST set `HOST_GOVERNANCE_API_BASE=http://host.docker.internal:` for the Docker coordinator unless governance is skipped +- **AND** browser access MUST still go through coordinator `/api/governance/*`, not directly to `governance-service` + +#### Scenario: -SkipGovernance explicitly opts out + +- **WHEN** deploy.ps1 帶 `-SkipGovernance` +- **THEN** Phase 4a governance startup MUST be skipped +- **AND** deploy audit MUST record `governanceSkipped=true` +- **AND** deploy.ps1 MUST NOT set a Docker governance API base + +#### Scenario: custom governance port refreshes Docker coordinator configuration + +- **WHEN** deploy.ps1 receives `-GovernancePort ` or resolves a non-default governance port +- **THEN** dry-run audit MUST record the resolved governance port +- **AND** `HOST_GOVERNANCE_API_BASE` MUST use that port +- **AND** an already-running Docker web plane MUST be refreshed before post-verify so coordinator cannot keep stale governance proxy configuration diff --git a/openspec/changes/governance-service-deploy/tasks.md b/openspec/changes/governance-service-deploy/tasks.md new file mode 100644 index 000000000..8a96aaadb --- /dev/null +++ b/openspec/changes/governance-service-deploy/tasks.md @@ -0,0 +1,26 @@ +# Tasks: governance-service-deploy + +## 1. Deploy behavior + +- [x] 1.1 Add `-SkipGovernance` and `-GovernancePort` to `scripts/deploy.ps1`. +- [x] 1.2 Add host-native `governance-service` launcher using `python -m uvicorn app:app`. +- [x] 1.3 Start governance before conversion / Kit / Docker compose. +- [x] 1.4 Export `HOST_GOVERNANCE_API_BASE` to Docker coordinator. +- [x] 1.5 Refresh web plane when the governance API base changes. +- [x] 1.6 Add direct and coordinator-proxy health checks. +- [x] 1.7 Add `governance-service` to `scripts/stop-all.ps1`. + +## 2. Governance evidence + +- [x] 2.1 Add OpenSpec delta for `one-click-deploy-hybrid`. +- [x] 2.2 Keep the Superpowers spec / plan as implementation governance artifacts. +- [x] 2.3 Record spec-to-done state through PR stage. + +## 3. Validation + +- [x] 3.1 `scripts/tests/test-deploy-governance-static.ps1`. +- [x] 3.2 `scripts/tests/test-deploy-dryrun.ps1`. +- [x] 3.3 `scripts/deploy.ps1 -Build -SkipKit -SkipConversion -StrictPostVerify`. +- [x] 3.4 A1/M1 Playwright E2E: `npm run test:e2e -- --project=chromium e2e/a1-m1-closeout.spec.ts`. +- [x] 3.5 `npx openspec validate governance-service-deploy --strict`. +- [ ] 3.6 PR review agent check passes. From 205a38c4c1fbaff0a64255983141ec1210db3601 Mon Sep 17 00:00:00 2001 From: monkey1sai <26239865+monkey1sai@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:34:27 +0800 Subject: [PATCH 3/3] chore(spec): record governance deploy PR checks Mark the OpenSpec review task and spec-to-done PR gate as complete after CodeRabbit and pr-review-agent passed. Validation: npx openspec validate governance-service-deploy --strict. --- artifacts/spec-to-done/governance-service-deploy-state.md | 3 ++- openspec/changes/governance-service-deploy/tasks.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/artifacts/spec-to-done/governance-service-deploy-state.md b/artifacts/spec-to-done/governance-service-deploy-state.md index 7afffc52e..e25da23ff 100644 --- a/artifacts/spec-to-done/governance-service-deploy-state.md +++ b/artifacts/spec-to-done/governance-service-deploy-state.md @@ -5,4 +5,5 @@ P4.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service P5.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=adversarial reviewer Carson approved; blocking findings none P6.ready | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=user requested commit push PR; targeted static and dry-run checks passed; GitNexus detect_changes returned no changes despite dirty linked worktree P6.fix | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=PR #215 pr-review-agent failed missing_openspec; added active OpenSpec change governance-service-deploy and validate passed -P7.done | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=local completion snapshot recorded before the later P6 commit/push/PR request +P6.ok | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=PR #215 CodeRabbit pass and pr-review-agent pass after OpenSpec blocker fix +P7.done | spec=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\docs\superpowers\specs\2026-06-16-governance-service-deploy-design.md | slug=governance-service-deploy | userFacing=true | dateStamp=2026-06-16 | branch=docs/governance-service-deploy-spec | worktree=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec | planPath=C:\Repos\active\iot\AI-BIM-governance\.worktrees\governance-service-deploy-spec\artifacts\spec-to-done\governance-service-deploy\plan.md | diagnosis=authorized scope complete through PR checks; merge/closeout not performed because merge was not explicitly requested diff --git a/openspec/changes/governance-service-deploy/tasks.md b/openspec/changes/governance-service-deploy/tasks.md index 8a96aaadb..d70be3b79 100644 --- a/openspec/changes/governance-service-deploy/tasks.md +++ b/openspec/changes/governance-service-deploy/tasks.md @@ -23,4 +23,4 @@ - [x] 3.3 `scripts/deploy.ps1 -Build -SkipKit -SkipConversion -StrictPostVerify`. - [x] 3.4 A1/M1 Playwright E2E: `npm run test:e2e -- --project=chromium e2e/a1-m1-closeout.spec.ts`. - [x] 3.5 `npx openspec validate governance-service-deploy --strict`. -- [ ] 3.6 PR review agent check passes. +- [x] 3.6 PR review agent check passes.