diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d608707..eb7534f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ permissions: jobs: windows-build-test-package: runs-on: windows-latest + timeout-minutes: 30 steps: - name: Checkout @@ -287,7 +288,7 @@ jobs: - name: Pipeline smoke shell: pwsh - run: .\legacy/scripts/tests/test-code-intel-pipeline.ps1 -RepoPath . -SkipRepowise -AllowGraphMissing -SkipSentruxGate -Mode normal + run: .\legacy/scripts/tests/test-code-intel-pipeline.ps1 -RepoPath . -SkipRepowise -AllowGraphMissing -Mode normal - name: Model channel and automation regression suites shell: pwsh @@ -357,6 +358,7 @@ jobs: - ubuntu-latest runs-on: ${{ matrix.os }} + timeout-minutes: 30 steps: - name: Checkout @@ -391,8 +393,13 @@ jobs: brew install ripgrep } else { - sudo apt-get update - sudo apt-get install -y ripgrep + if (Get-Command rg -ErrorAction SilentlyContinue) { + rg --version + } + else { + sudo apt-get update + sudo apt-get install -y ripgrep + } } - name: Install ast-grep (pinned) @@ -672,8 +679,81 @@ jobs: if ($result.status -notin @("installed", "repaired", "already_installed")) { throw "Unexpected bootstrap status '$($result.status)':`n$raw" } + $result | ConvertTo-Json -Depth 20 | Set-Content -Encoding utf8 (Join-Path $env:RUNNER_TEMP "code-intel-smoketest-bootstrap.json") Write-Host "bootstrap.py installed $($result.tag) -> $($result.release_root)" + - name: Packaged Sentrux capability closure smoke + shell: pwsh + run: | + $bootstrap = Get-Content -Raw -LiteralPath (Join-Path $env:RUNNER_TEMP "code-intel-smoketest-bootstrap.json") | ConvertFrom-Json + $payload = [IO.Path]::GetFullPath([string]$bootstrap.release_root) + $binaryName = if ($IsWindows) { "code-intel.exe" } else { "code-intel" } + $binary = Join-Path $payload (Join-Path "bin" $binaryName) + $staging = Join-Path $env:RUNNER_TEMP "code-intel-packaged-sentrux-staging" + $authority = Join-Path $env:RUNNER_TEMP "code-intel-packaged-sentrux-authority" + $finalName = "packaged-sentrux-$env:GITHUB_RUN_ID" + $manifestPath = Join-Path $payload (Join-Path "orchestration" "integrations.json") + New-Item -ItemType Directory -Force -Path $authority | Out-Null + & $binary run execute --repo $payload --out $staging --authority-root $authority --final-name $finalName --manifest $manifestPath --doctor-require-repowise false + if ($LASTEXITCODE -ne 0) { throw "packaged install run execute failed with exit ${LASTEXITCODE}" } + + $repoName = (Get-Item -LiteralPath $payload).Name + $runRoot = Join-Path (Join-Path $authority $repoName) $finalName + $index = Join-Path $authority "index.json" + & $binary artifact index --artifact-root $authority --output $index --operation rebuild + if ($LASTEXITCODE -ne 0) { throw "packaged Sentrux artifact index validation failed" } + $marker = Get-Content -Raw -LiteralPath (Join-Path $runRoot "run-complete.json") | ConvertFrom-Json + if ($marker.schema -ne "code-intel-run-commit.v1") { throw "packaged run completion marker is invalid" } + $manifest = Get-Content -Raw -LiteralPath (Join-Path $runRoot ($marker.manifest.path -replace '/', [IO.Path]::DirectorySeparatorChar)) | ConvertFrom-Json + if ($manifest.schema -ne "code-intel-run-manifest.v1" -or $manifest.outcome -ne "completed") { throw "packaged run manifest is not completed" } + if ($manifest.snapshotIdentity -ne $marker.snapshotIdentity) { throw "packaged marker and manifest snapshots differ" } + if ($manifest.nodes.'evidence.sentrux'.status -ne "succeeded" -or $manifest.nodes.'evidence.sentrux'.verdict -ne "pass") { throw "packaged Sentrux evidence node is not a passing verified result" } + + $indexValue = Get-Content -Raw -LiteralPath $index | ConvertFrom-Json + $entry = @($indexValue.entries | Where-Object { $_.repo -eq $repoName -and $_.run -eq $finalName }) + if ($entry.Count -ne 1) { throw "packaged run was not admitted exactly once by artifact index" } + if ($entry[0].outcome -ne "completed" -or $entry[0].snapshotIdentity -ne $marker.snapshotIdentity) { throw "packaged artifact index identity does not match completion marker" } + $refs = @($entry[0].artifactRefs) + if (@($refs | Where-Object { $_.type -eq "diagnosis.hospital" }).Count -ne 1) { throw "packaged run is missing its verified hospital report ref" } + $capabilityRefs = @($refs | Where-Object { $_.artifactSchema -eq "code-intel-sentrux-capability-artifact.v1" -and $_.type -eq "provider.sentrux.capability-artifact" }) + if ($capabilityRefs.Count -eq 0) { throw "packaged run has no verified Sentrux capability refs" } + $matrix = Get-Content -Raw -LiteralPath (Join-Path $payload (Join-Path "orchestration" "sentrux-capability-matrix.v1.json")) | ConvertFrom-Json + $matrixById = @{} + foreach ($capability in @($matrix.capabilities)) { $matrixById[$capability.id] = $capability } + $observed = @{} + foreach ($ref in $capabilityRefs) { + if ($ref.path -ne "objects/sha256/$($ref.sha256)") { throw "packaged capability ref is not content-addressed: $($ref.path)" } + $payloadPath = Join-Path $runRoot ($ref.path -replace '/', [IO.Path]::DirectorySeparatorChar) + if (-not (Test-Path -LiteralPath $payloadPath -PathType Leaf)) { throw "packaged capability object is missing: $($ref.path)" } + if ((Get-FileHash -LiteralPath $payloadPath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $ref.sha256) { throw "packaged capability object digest mismatch: $($ref.path)" } + $capabilityPayload = Get-Content -Raw -LiteralPath $payloadPath | ConvertFrom-Json + if ($capabilityPayload.schema -ne "code-intel-sentrux-capability-artifact.v1" -or $capabilityPayload.snapshotIdentity -ne $marker.snapshotIdentity) { throw "packaged capability artifact is not snapshot-bound: $($ref.path)" } + if (-not $matrixById.ContainsKey($capabilityPayload.capabilityId)) { throw "packaged capability is absent from the matrix: $($capabilityPayload.capabilityId)" } + if ($matrixById[$capabilityPayload.capabilityId].operation -ne $capabilityPayload.operation) { throw "packaged capability operation disagrees with the matrix: $($capabilityPayload.capabilityId)" } + if ($observed.ContainsKey($capabilityPayload.capabilityId)) { throw "duplicate packaged capability artifact: $($capabilityPayload.capabilityId)" } + $observed[$capabilityPayload.capabilityId] = $capabilityPayload + if ($capabilityPayload.status -ne "succeeded" -and $null -eq $capabilityPayload.failure) { throw "packaged non-success capability has no explicit failure: $($capabilityPayload.capabilityId)" } + } + $required = @($matrix.capabilities | Where-Object { $_.requiredForRelease }) + if ($required.Count -eq 0) { throw "capability matrix exposes no required capabilities" } + foreach ($capability in $required) { + $id = [string]$capability.id + if (-not $observed.ContainsKey($id)) { throw "packaged run is missing required Sentrux capability: $id" } + $payload = $observed[$id] + $mode = [string]$capability.executionMode + if ($mode -eq "automatic") { + if ($capability.currentState -eq "authoritative_automatic" -and $payload.status -ne "succeeded") { throw "packaged authoritative automatic capability did not succeed: $id ($($payload.status))" } + if ($capability.currentState -eq "automatic_degraded" -and $payload.status -notin @("succeeded", "degraded")) { throw "packaged degraded automatic capability did not produce an accepted result: $id ($($payload.status))" } + if ($capability.currentState -notin @("authoritative_automatic", "automatic_degraded")) { throw "packaged automatic capability has an unsupported matrix state: $id ($($capability.currentState))" } + } + elseif ($mode -in @("explicit_authority", "lifecycle_external")) { + if ($payload.status -ne "not_applicable") { throw "packaged explicit/lifecycle capability must be explicitly not_applicable: $id ($($payload.status))" } + if ($null -eq $payload.failure -or [string]::IsNullOrWhiteSpace([string]$payload.failure.kind)) { throw "packaged explicit/lifecycle capability has no failure reason: $id" } + } + else { throw "packaged capability has no recognized execution mode: $id ($mode)" } + } + Write-Host "Packaged Sentrux closure verified from committed refs: $($capabilityRefs.Count) capability refs; required=$($required.Count); matrix coverage=$($matrix.coverageStatus)" + - name: Assert PATH and CODE_INTEL_HOME persisted (Windows) if: runner.os == 'Windows' shell: pwsh @@ -738,7 +818,7 @@ jobs: - name: Pipeline smoke shell: pwsh - run: ./legacy/scripts/tests/test-code-intel-pipeline.ps1 -RepoPath . -SkipRepowise -AllowGraphMissing -SkipSentruxCheck -SkipSentruxGate -Mode normal + run: ./legacy/scripts/tests/test-code-intel-pipeline.ps1 -RepoPath . -SkipRepowise -AllowGraphMissing -Mode normal - name: Hardcoded path scan shell: pwsh diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index cdedb14a..e2180b3e 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -227,3 +227,115 @@ jobs: exit 1 fi echo "agent-approved label present — gate green" + + sentrux-capability-gate: + # The PR gate consumes only the committed Run Commit boundary. It never + # treats Sentrux stdout as authority: `artifact index` revalidates the + # completion marker, content-addressed manifest, and every Artifact Ref. + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Rust + shell: pwsh + run: | + rustup toolchain install + rustup show active-toolchain + + - name: Install ripgrep + shell: pwsh + run: | + if (Get-Command rg -ErrorAction SilentlyContinue) { + rg --version + } + else { + sudo apt-get update + sudo apt-get install -y ripgrep + } + + - name: Build Rust CLI + shell: pwsh + run: cargo build -p code-intel --release --locked + + - name: Run authoritative Sentrux capability pipeline + shell: pwsh + run: | + $binary = "./target/release/code-intel" + $staging = Join-Path $env:RUNNER_TEMP "code-intel-pr-sentrux-staging" + $authority = Join-Path $env:RUNNER_TEMP "code-intel-pr-sentrux-authority" + $finalName = "pr-sentrux-$env:GITHUB_RUN_ID" + New-Item -ItemType Directory -Force -Path $authority | Out-Null + & $binary run execute --repo . --out $staging --authority-root $authority --final-name $finalName --manifest orchestration/integrations.json --doctor-require-repowise false + if ($LASTEXITCODE -ne 0) { + throw "authoritative Sentrux pipeline failed with exit ${LASTEXITCODE}; inspect the uploaded run closure evidence" + } + + $repoName = (Get-Item .).Name + $runRoot = Join-Path (Join-Path $authority $repoName) $finalName + $index = Join-Path $authority "index.json" + & $binary artifact index --artifact-root $authority --output $index --operation rebuild + if ($LASTEXITCODE -ne 0) { throw "committed artifact index validation failed" } + + $markerPath = Join-Path $runRoot "run-complete.json" + if (-not (Test-Path -LiteralPath $markerPath -PathType Leaf)) { throw "Sentrux run has no completion marker: $markerPath" } + $marker = Get-Content -Raw -LiteralPath $markerPath | ConvertFrom-Json + if ($marker.schema -ne "code-intel-run-commit.v1") { throw "unexpected completion marker schema: $($marker.schema)" } + $manifestPath = Join-Path $runRoot ($marker.manifest.path -replace '/', [IO.Path]::DirectorySeparatorChar) + $manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json + if ($manifest.schema -ne "code-intel-run-manifest.v1" -or $manifest.outcome -ne "completed") { throw "Sentrux run manifest is not completed and authoritative" } + if ($manifest.snapshotIdentity -ne $marker.snapshotIdentity) { throw "completion marker and run manifest snapshot identities differ" } + + $indexValue = Get-Content -Raw -LiteralPath $index | ConvertFrom-Json + $entry = @($indexValue.entries | Where-Object { $_.repo -eq $repoName -and $_.run -eq $finalName }) + if ($entry.Count -ne 1) { throw "artifact index did not admit exactly one completed PR run" } + $refs = @($entry[0].artifactRefs) + $hospitalRefs = @($refs | Where-Object { $_.type -eq "diagnosis.hospital" }) + if ($hospitalRefs.Count -ne 1) { throw "completed run is missing its verified final hospital report Artifact Ref" } + $capabilityRefs = @($refs | Where-Object { $_.artifactSchema -eq "code-intel-sentrux-capability-artifact.v1" -and $_.type -eq "provider.sentrux.capability-artifact" }) + if ($capabilityRefs.Count -eq 0) { throw "completed run has no verified Sentrux capability Artifact Refs" } + + $matrix = Get-Content -Raw -LiteralPath "orchestration/sentrux-capability-matrix.v1.json" | ConvertFrom-Json + $matrixById = @{} + foreach ($capability in @($matrix.capabilities)) { $matrixById[$capability.id] = $capability } + $observed = @{} + foreach ($ref in $capabilityRefs) { + $payloadPath = Join-Path $runRoot ($ref.path -replace '/', [IO.Path]::DirectorySeparatorChar) + $payload = Get-Content -Raw -LiteralPath $payloadPath | ConvertFrom-Json + if ($payload.schema -ne "code-intel-sentrux-capability-artifact.v1" -or $payload.snapshotIdentity -ne $marker.snapshotIdentity) { throw "Sentrux capability artifact is not schema-valid and snapshot-bound: $($ref.path)" } + if (-not $matrixById.ContainsKey($payload.capabilityId)) { throw "Sentrux capability is absent from the capability matrix: $($payload.capabilityId)" } + if ($matrixById[$payload.capabilityId].operation -ne $payload.operation) { throw "Sentrux capability operation disagrees with the matrix: $($payload.capabilityId)" } + if ($observed.ContainsKey($payload.capabilityId)) { throw "duplicate Sentrux capability artifact: $($payload.capabilityId)" } + $observed[$payload.capabilityId] = $payload + if ($payload.status -ne "succeeded" -and $null -eq $payload.failure) { throw "non-success Sentrux capability has no explicit failure: $($payload.capabilityId)" } + } + $required = @($matrix.capabilities | Where-Object { $_.requiredForRelease }) + foreach ($capability in $required) { + $id = [string]$capability.id + if (-not $observed.ContainsKey($id)) { throw "required Sentrux capability is missing from the committed run: $id" } + $payload = $observed[$id] + $mode = [string]$capability.executionMode + if ($mode -eq "automatic") { + if ($capability.currentState -eq "authoritative_automatic" -and $payload.status -ne "succeeded") { throw "authoritative automatic Sentrux capability did not succeed: $id ($($payload.status))" } + if ($capability.currentState -eq "automatic_degraded" -and $payload.status -notin @("succeeded", "degraded")) { throw "degraded automatic Sentrux capability did not produce an accepted result: $id ($($payload.status))" } + if ($capability.currentState -notin @("authoritative_automatic", "automatic_degraded")) { throw "automatic Sentrux capability has an unsupported matrix state: $id ($($capability.currentState))" } + } + elseif ($mode -in @("explicit_authority", "lifecycle_external")) { + if ($payload.status -ne "not_applicable") { throw "explicit/lifecycle Sentrux capability must be explicitly not_applicable in a DAG run: $id ($($payload.status))" } + if ($null -eq $payload.failure -or [string]::IsNullOrWhiteSpace([string]$payload.failure.kind)) { throw "explicit/lifecycle Sentrux capability has no failure reason: $id" } + } + else { throw "Sentrux capability has no recognized execution mode: $id ($mode)" } + } + Write-Host "Sentrux capability closure verified: $($capabilityRefs.Count) capability refs; required=$($required.Count); matrix coverage=$($matrix.coverageStatus)" + + - name: Upload Sentrux closure evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: sentrux-pr-closure + path: | + ${{ runner.temp }}/code-intel-pr-sentrux-authority + if-no-files-found: ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c188e7da..b4a709fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,7 +170,7 @@ jobs: - name: Pipeline smoke shell: pwsh - run: .\legacy/scripts/tests/test-code-intel-pipeline.ps1 -RepoPath . -SkipRepowise -AllowGraphMissing -SkipSentruxGate -Mode normal + run: .\legacy/scripts/tests/test-code-intel-pipeline.ps1 -RepoPath . -SkipRepowise -AllowGraphMissing -Mode normal - name: Package shell: pwsh @@ -220,24 +220,74 @@ jobs: } } python (Join-Path $payload "skills\code-intel-pipeline\scripts\bootstrap.py") --help - # The packaged binary must pass its own structural gates against the - # packaged payload: release green, self-scan green, and published - # artifact green must refer to the same snapshot content. + # The packaged binary must publish a committed, snapshot-bound + # Sentrux closure. The manifest and content-addressed artifacts are + # authoritative; command stdout is intentionally not inspected. $packagedBinary = Join-Path $payload "bin\code-intel.exe" - & $packagedBinary sentrux --operation check --repo $payload - if ($LASTEXITCODE -ne 0) { throw "packaged binary failed its own structural check" } - & $packagedBinary sentrux --operation gate --repo $payload - if ($LASTEXITCODE -ne 0) { throw "packaged binary failed its own no-degradation gate" } - # scan/health/dsm are not gated (no baseline semantics), but they - # must at least run clean against the real packaged payload -- a - # regression here previously only showed up against synthetic unit - # fixtures, never against a real packaged tree. - & $packagedBinary sentrux --operation scan --repo $payload | Out-Null - if ($LASTEXITCODE -ne 0) { throw "packaged binary failed its own structural scan" } - & $packagedBinary sentrux --operation health --repo $payload | Out-Null - if ($LASTEXITCODE -ne 0) { throw "packaged binary failed its own structural health summary" } - & $packagedBinary sentrux --operation dsm --repo $payload | Out-Null - if ($LASTEXITCODE -ne 0) { throw "packaged binary failed its own dependency-structure analysis" } + $staging = Join-Path $env:RUNNER_TEMP "code-intel-release-packaged-sentrux-staging-$env:GITHUB_RUN_ID" + $authority = Join-Path $env:RUNNER_TEMP "code-intel-release-packaged-sentrux-authority-$env:GITHUB_RUN_ID" + $finalName = "release-packaged-sentrux-$env:GITHUB_RUN_ID" + $manifestPath = Join-Path $payload (Join-Path "orchestration" "integrations.json") + New-Item -ItemType Directory -Force -Path $authority | Out-Null + & $packagedBinary run execute --repo $payload --out $staging --authority-root $authority --final-name $finalName --manifest $manifestPath --doctor-require-repowise false + if ($LASTEXITCODE -ne 0) { throw "packaged release run execute failed with exit ${LASTEXITCODE}" } + + $repoName = (Get-Item -LiteralPath $payload).Name + $runRoot = Join-Path (Join-Path $authority $repoName) $finalName + $index = Join-Path $authority "index.json" + & $packagedBinary artifact index --artifact-root $authority --output $index --operation rebuild + if ($LASTEXITCODE -ne 0) { throw "packaged release artifact index validation failed" } + $marker = Get-Content -Raw -LiteralPath (Join-Path $runRoot "run-complete.json") | ConvertFrom-Json + if ($marker.schema -ne "code-intel-run-commit.v1") { throw "packaged release completion marker is invalid" } + $manifest = Get-Content -Raw -LiteralPath (Join-Path $runRoot ($marker.manifest.path -replace '/', [IO.Path]::DirectorySeparatorChar)) | ConvertFrom-Json + if ($manifest.schema -ne "code-intel-run-manifest.v1" -or $manifest.outcome -ne "completed") { throw "packaged release run manifest is not completed" } + if ($manifest.snapshotIdentity -ne $marker.snapshotIdentity) { throw "packaged release marker and manifest snapshots differ" } + if ($manifest.nodes.'evidence.sentrux'.status -ne "succeeded" -or $manifest.nodes.'evidence.sentrux'.verdict -ne "pass") { throw "packaged release Sentrux evidence node is not a passing verified result" } + + $indexValue = Get-Content -Raw -LiteralPath $index | ConvertFrom-Json + $entry = @($indexValue.entries | Where-Object { $_.repo -eq $repoName -and $_.run -eq $finalName }) + if ($entry.Count -ne 1) { throw "packaged release run was not admitted exactly once by artifact index" } + if ($entry[0].outcome -ne "completed" -or $entry[0].snapshotIdentity -ne $marker.snapshotIdentity) { throw "packaged release artifact index identity does not match completion marker" } + $refs = @($entry[0].artifactRefs) + if (@($refs | Where-Object { $_.type -eq "diagnosis.hospital" }).Count -ne 1) { throw "packaged release run is missing its verified hospital report ref" } + $capabilityRefs = @($refs | Where-Object { $_.artifactSchema -eq "code-intel-sentrux-capability-artifact.v1" -and $_.type -eq "provider.sentrux.capability-artifact" }) + if ($capabilityRefs.Count -eq 0) { throw "packaged release run has no verified Sentrux capability refs" } + $matrix = Get-Content -Raw -LiteralPath (Join-Path $payload (Join-Path "orchestration" "sentrux-capability-matrix.v1.json")) | ConvertFrom-Json + $matrixById = @{} + foreach ($capability in @($matrix.capabilities)) { $matrixById[$capability.id] = $capability } + $observed = @{} + foreach ($ref in $capabilityRefs) { + if ($ref.path -ne "objects/sha256/$($ref.sha256)") { throw "packaged release capability ref is not content-addressed: $($ref.path)" } + $payloadPath = Join-Path $runRoot ($ref.path -replace '/', [IO.Path]::DirectorySeparatorChar) + if (-not (Test-Path -LiteralPath $payloadPath -PathType Leaf)) { throw "packaged release capability object is missing: $($ref.path)" } + if ((Get-FileHash -LiteralPath $payloadPath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $ref.sha256) { throw "packaged release capability object digest mismatch: $($ref.path)" } + $capabilityPayload = Get-Content -Raw -LiteralPath $payloadPath | ConvertFrom-Json + if ($capabilityPayload.schema -ne "code-intel-sentrux-capability-artifact.v1" -or $capabilityPayload.snapshotIdentity -ne $marker.snapshotIdentity) { throw "packaged release capability artifact is not snapshot-bound: $($ref.path)" } + if (-not $matrixById.ContainsKey($capabilityPayload.capabilityId)) { throw "packaged release capability is absent from the matrix: $($capabilityPayload.capabilityId)" } + if ($matrixById[$capabilityPayload.capabilityId].operation -ne $capabilityPayload.operation) { throw "packaged release capability operation disagrees with the matrix: $($capabilityPayload.capabilityId)" } + if ($observed.ContainsKey($capabilityPayload.capabilityId)) { throw "duplicate packaged release capability artifact: $($capabilityPayload.capabilityId)" } + $observed[$capabilityPayload.capabilityId] = $capabilityPayload + if ($capabilityPayload.status -ne "succeeded" -and $null -eq $capabilityPayload.failure) { throw "packaged release non-success capability has no explicit failure: $($capabilityPayload.capabilityId)" } + } + $required = @($matrix.capabilities | Where-Object { $_.requiredForRelease }) + if ($required.Count -eq 0) { throw "capability matrix exposes no required capabilities" } + foreach ($capability in $required) { + $id = [string]$capability.id + if (-not $observed.ContainsKey($id)) { throw "packaged release run is missing required Sentrux capability: $id" } + $payload = $observed[$id] + $mode = [string]$capability.executionMode + if ($mode -eq "automatic") { + if ($capability.currentState -eq "authoritative_automatic" -and $payload.status -ne "succeeded") { throw "packaged release authoritative automatic capability did not succeed: $id ($($payload.status))" } + if ($capability.currentState -eq "automatic_degraded" -and $payload.status -notin @("succeeded", "degraded")) { throw "packaged release degraded automatic capability did not produce an accepted result: $id ($($payload.status))" } + if ($capability.currentState -notin @("authoritative_automatic", "automatic_degraded")) { throw "packaged release automatic capability has an unsupported matrix state: $id ($($capability.currentState))" } + } + elseif ($mode -in @("explicit_authority", "lifecycle_external")) { + if ($payload.status -ne "not_applicable") { throw "packaged release explicit/lifecycle capability must be explicitly not_applicable: $id ($($payload.status))" } + if ($null -eq $payload.failure -or [string]::IsNullOrWhiteSpace([string]$payload.failure.kind)) { throw "packaged release explicit/lifecycle capability has no failure reason: $id" } + } + else { throw "packaged release capability has no recognized execution mode: $id ($mode)" } + } + Write-Host "Packaged release Sentrux closure verified from committed refs: $($capabilityRefs.Count) capability refs; required=$($required.Count); matrix coverage=$($matrix.coverageStatus)" # ai-safety-003 (issue #34): if the packaged snapshot carries an # audit report, it must pass the same fail-closed validate @@ -307,8 +357,13 @@ jobs: brew install ripgrep } else { - sudo apt-get update - sudo apt-get install -y ripgrep + if (Get-Command rg -ErrorAction SilentlyContinue) { + rg --version + } + else { + sudo apt-get update + sudo apt-get install -y ripgrep + } } - name: Build Rust CLI @@ -396,23 +451,73 @@ jobs: $packagedBinary = Join-Path $payload "bin/code-intel" bash -c "test -x '$packagedBinary'" if ($LASTEXITCODE -ne 0) { throw "packaged binary lost its executable bit in the zip round trip: $packagedBinary" } - # The packaged binary must pass its own structural gates against the - # packaged payload: release green, self-scan green, and published - # artifact green must refer to the same snapshot content. - & $packagedBinary sentrux --operation check --repo $payload - if ($LASTEXITCODE -ne 0) { throw "packaged binary failed its own structural check" } - & $packagedBinary sentrux --operation gate --repo $payload - if ($LASTEXITCODE -ne 0) { throw "packaged binary failed its own no-degradation gate" } - # scan/health/dsm are not gated (no baseline semantics), but they - # must at least run clean against the real packaged payload -- a - # regression here previously only showed up against synthetic unit - # fixtures, never against a real packaged tree. - & $packagedBinary sentrux --operation scan --repo $payload | Out-Null - if ($LASTEXITCODE -ne 0) { throw "packaged binary failed its own structural scan" } - & $packagedBinary sentrux --operation health --repo $payload | Out-Null - if ($LASTEXITCODE -ne 0) { throw "packaged binary failed its own structural health summary" } - & $packagedBinary sentrux --operation dsm --repo $payload | Out-Null - if ($LASTEXITCODE -ne 0) { throw "packaged binary failed its own dependency-structure analysis" } + # The packaged binary must publish a committed, snapshot-bound + # Sentrux closure. The manifest and content-addressed artifacts are + # authoritative; command stdout is intentionally not inspected. + $staging = Join-Path $env:RUNNER_TEMP "code-intel-release-packaged-sentrux-staging-$env:GITHUB_RUN_ID" + $authority = Join-Path $env:RUNNER_TEMP "code-intel-release-packaged-sentrux-authority-$env:GITHUB_RUN_ID" + $finalName = "release-packaged-sentrux-$env:GITHUB_RUN_ID" + $manifestPath = Join-Path $payload (Join-Path "orchestration" "integrations.json") + New-Item -ItemType Directory -Force -Path $authority | Out-Null + & $packagedBinary run execute --repo $payload --out $staging --authority-root $authority --final-name $finalName --manifest $manifestPath --doctor-require-repowise false + if ($LASTEXITCODE -ne 0) { throw "packaged release run execute failed with exit ${LASTEXITCODE}" } + + $repoName = (Get-Item -LiteralPath $payload).Name + $runRoot = Join-Path (Join-Path $authority $repoName) $finalName + $index = Join-Path $authority "index.json" + & $packagedBinary artifact index --artifact-root $authority --output $index --operation rebuild + if ($LASTEXITCODE -ne 0) { throw "packaged release artifact index validation failed" } + $marker = Get-Content -Raw -LiteralPath (Join-Path $runRoot "run-complete.json") | ConvertFrom-Json + if ($marker.schema -ne "code-intel-run-commit.v1") { throw "packaged release completion marker is invalid" } + $manifest = Get-Content -Raw -LiteralPath (Join-Path $runRoot ($marker.manifest.path -replace '/', [IO.Path]::DirectorySeparatorChar)) | ConvertFrom-Json + if ($manifest.schema -ne "code-intel-run-manifest.v1" -or $manifest.outcome -ne "completed") { throw "packaged release run manifest is not completed" } + if ($manifest.snapshotIdentity -ne $marker.snapshotIdentity) { throw "packaged release marker and manifest snapshots differ" } + if ($manifest.nodes.'evidence.sentrux'.status -ne "succeeded" -or $manifest.nodes.'evidence.sentrux'.verdict -ne "pass") { throw "packaged release Sentrux evidence node is not a passing verified result" } + + $indexValue = Get-Content -Raw -LiteralPath $index | ConvertFrom-Json + $entry = @($indexValue.entries | Where-Object { $_.repo -eq $repoName -and $_.run -eq $finalName }) + if ($entry.Count -ne 1) { throw "packaged release run was not admitted exactly once by artifact index" } + if ($entry[0].outcome -ne "completed" -or $entry[0].snapshotIdentity -ne $marker.snapshotIdentity) { throw "packaged release artifact index identity does not match completion marker" } + $refs = @($entry[0].artifactRefs) + if (@($refs | Where-Object { $_.type -eq "diagnosis.hospital" }).Count -ne 1) { throw "packaged release run is missing its verified hospital report ref" } + $capabilityRefs = @($refs | Where-Object { $_.artifactSchema -eq "code-intel-sentrux-capability-artifact.v1" -and $_.type -eq "provider.sentrux.capability-artifact" }) + if ($capabilityRefs.Count -eq 0) { throw "packaged release run has no verified Sentrux capability refs" } + $matrix = Get-Content -Raw -LiteralPath (Join-Path $payload (Join-Path "orchestration" "sentrux-capability-matrix.v1.json")) | ConvertFrom-Json + $matrixById = @{} + foreach ($capability in @($matrix.capabilities)) { $matrixById[$capability.id] = $capability } + $observed = @{} + foreach ($ref in $capabilityRefs) { + if ($ref.path -ne "objects/sha256/$($ref.sha256)") { throw "packaged release capability ref is not content-addressed: $($ref.path)" } + $payloadPath = Join-Path $runRoot ($ref.path -replace '/', [IO.Path]::DirectorySeparatorChar) + if (-not (Test-Path -LiteralPath $payloadPath -PathType Leaf)) { throw "packaged release capability object is missing: $($ref.path)" } + if ((Get-FileHash -LiteralPath $payloadPath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $ref.sha256) { throw "packaged release capability object digest mismatch: $($ref.path)" } + $capabilityPayload = Get-Content -Raw -LiteralPath $payloadPath | ConvertFrom-Json + if ($capabilityPayload.schema -ne "code-intel-sentrux-capability-artifact.v1" -or $capabilityPayload.snapshotIdentity -ne $marker.snapshotIdentity) { throw "packaged release capability artifact is not snapshot-bound: $($ref.path)" } + if (-not $matrixById.ContainsKey($capabilityPayload.capabilityId)) { throw "packaged release capability is absent from the matrix: $($capabilityPayload.capabilityId)" } + if ($matrixById[$capabilityPayload.capabilityId].operation -ne $capabilityPayload.operation) { throw "packaged release capability operation disagrees with the matrix: $($capabilityPayload.capabilityId)" } + if ($observed.ContainsKey($capabilityPayload.capabilityId)) { throw "duplicate packaged release capability artifact: $($capabilityPayload.capabilityId)" } + $observed[$capabilityPayload.capabilityId] = $capabilityPayload + if ($capabilityPayload.status -ne "succeeded" -and $null -eq $capabilityPayload.failure) { throw "packaged release non-success capability has no explicit failure: $($capabilityPayload.capabilityId)" } + } + $required = @($matrix.capabilities | Where-Object { $_.requiredForRelease }) + if ($required.Count -eq 0) { throw "capability matrix exposes no required capabilities" } + foreach ($capability in $required) { + $id = [string]$capability.id + if (-not $observed.ContainsKey($id)) { throw "packaged release run is missing required Sentrux capability: $id" } + $payload = $observed[$id] + $mode = [string]$capability.executionMode + if ($mode -eq "automatic") { + if ($capability.currentState -eq "authoritative_automatic" -and $payload.status -ne "succeeded") { throw "packaged release authoritative automatic capability did not succeed: $id ($($payload.status))" } + if ($capability.currentState -eq "automatic_degraded" -and $payload.status -notin @("succeeded", "degraded")) { throw "packaged release degraded automatic capability did not produce an accepted result: $id ($($payload.status))" } + if ($capability.currentState -notin @("authoritative_automatic", "automatic_degraded")) { throw "packaged release automatic capability has an unsupported matrix state: $id ($($capability.currentState))" } + } + elseif ($mode -in @("explicit_authority", "lifecycle_external")) { + if ($payload.status -ne "not_applicable") { throw "packaged release explicit/lifecycle capability must be explicitly not_applicable: $id ($($payload.status))" } + if ($null -eq $payload.failure -or [string]::IsNullOrWhiteSpace([string]$payload.failure.kind)) { throw "packaged release explicit/lifecycle capability has no failure reason: $id" } + } + else { throw "packaged release capability has no recognized execution mode: $id ($mode)" } + } + Write-Host "Packaged release Sentrux closure verified from committed refs: $($capabilityRefs.Count) capability refs; required=$($required.Count); matrix coverage=$($matrix.coverageStatus)" # ai-safety-003 (issue #34): if the packaged snapshot carries an # audit report, it must pass the same fail-closed validate diff --git a/crates/code-intel-cli/src/artifact_ref.rs b/crates/code-intel-cli/src/artifact_ref.rs index 1fb891a5..d023413b 100644 --- a/crates/code-intel-cli/src/artifact_ref.rs +++ b/crates/code-intel-cli/src/artifact_ref.rs @@ -289,6 +289,14 @@ fn diagnosis_family_contract(schema: &str, artifact_type: &str) -> Option { + Some(ArtifactContract { + artifact_schema: "code-intel-sentrux-capability-artifact.v1", + artifact_type: "provider.sentrux.capability-artifact", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_sentrux_capability_artifact, + }) + } ("code-intel-hospital.v1", "diagnosis.hospital") => Some(ArtifactContract { artifact_schema: "code-intel-hospital.v1", artifact_type: "diagnosis.hospital", @@ -561,6 +569,69 @@ fn sentrux_command_result_is_valid(command: &Value, id: &str) -> bool { && command["stderr"].is_string() } +fn validate_sentrux_capability_artifact(bytes: &[u8]) -> Result<(), String> { + let value = parse_contract_json(bytes, "Sentrux capability artifact")?; + exact_object_keys( + &value, + &[ + "schema", + "contractVersion", + "capabilityId", + "operation", + "runId", + "snapshotIdentity", + "provider", + "status", + "authority", + "inputs", + "outputs", + "failure", + "freshness", + "decisionConsumers", + ], + "Sentrux capability artifact", + )?; + if value["schema"] != "code-intel-sentrux-capability-artifact.v1" + || value["contractVersion"] != 1 + || !value["capabilityId"] + .as_str() + .is_some_and(|id| !id.is_empty() && id.starts_with("sentrux.")) + || !value["operation"] + .as_str() + .is_some_and(|operation| !operation.is_empty()) + || !value["runId"] + .as_str() + .is_some_and(|run_id| !run_id.is_empty()) + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + || !value["provider"].is_object() + || !matches!( + value["status"].as_str(), + Some( + "succeeded" | "degraded" | "unavailable" | "skipped" | "not_applicable" | "failed" + ) + ) + || !matches!( + value["authority"].as_str(), + Some("authoritative" | "fallback" | "compatibility" | "declared_only") + ) + || !value["inputs"].is_object() + || !value["outputs"].is_object() + || if value["status"] == "succeeded" { + !value["failure"].is_null() + } else { + !value["failure"].is_object() + } + || !value["freshness"].is_object() + || !value["decisionConsumers"].is_array() + { + return Err("Sentrux capability artifact header or envelope fields are invalid".into()); + } + if value["status"] == "succeeded" && value["outputs"].as_object().is_none_or(|v| v.is_empty()) { + return Err("successful Sentrux capability artifact must contain outputs".into()); + } + Ok(()) +} + fn validate_retirement_manifest(bytes: &[u8]) -> Result<(), String> { let value = parse_contract_json(bytes, "retirement manifest")?; exact_object_keys( @@ -3827,6 +3898,58 @@ mod tests { } } + #[test] + fn sentrux_capability_artifact_contract_accepts_success_and_requires_failure_details() { + let reference = json!({ + "artifactSchema":"code-intel-sentrux-capability-artifact.v1", + "type":"provider.sentrux.capability-artifact" + }); + let contract = registered_contract(&reference).expect("Sentrux artifact is registered"); + let base = json!({ + "schema":"code-intel-sentrux-capability-artifact.v1", + "contractVersion":1, + "capabilityId":"sentrux.scan", + "operation":"scan", + "runId":"run-1", + "snapshotIdentity":"a".repeat(64), + "provider":{ + "mode":"builtin", + "id":"sentrux.builtin", + "version":"1.0.0", + "digest":"b".repeat(64) + }, + "status":"succeeded", + "authority":"authoritative", + "inputs":{}, + "outputs":{"artifacts":[]}, + "failure":null, + "freshness":{ + "status":"current", + "evaluatedAt":"2026-08-18T00:00:00Z", + "consumedSnapshotIdentity":"a".repeat(64) + }, + "decisionConsumers":["release_gate"] + }); + (contract.validate_payload)(&serde_json::to_vec(&base).unwrap()) + .expect("successful Sentrux artifact with null failure must pass"); + + let mut failed = base.clone(); + failed["status"] = json!("failed"); + failed["failure"] = json!({ + "kind":"provider_error", + "message":"command failed", + "retryable":true + }); + (contract.validate_payload)(&serde_json::to_vec(&failed).unwrap()) + .expect("failed Sentrux artifact with failure details must pass"); + + failed["failure"] = Value::Null; + assert!( + (contract.validate_payload)(&serde_json::to_vec(&failed).unwrap()).is_err(), + "failed Sentrux artifact must not silently omit failure details" + ); + } + fn deletion_file(path: &str, base: &str, result: &str, added: Vec<&str>) -> Value { json!({ "path":path, diff --git a/crates/code-intel-cli/src/artifacts_report.rs b/crates/code-intel-cli/src/artifacts_report.rs index 18397d92..a526e9fd 100644 --- a/crates/code-intel-cli/src/artifacts_report.rs +++ b/crates/code-intel-cli/src/artifacts_report.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::fs; use std::path::{Path, PathBuf}; @@ -36,9 +37,11 @@ pub(super) fn report(repo: &Path, artifact_root: Option<&Path>, json: bool) -> R let hospital = required_report_artifact(&run_root, &manifest, "diagnosis.hospital")?; let hospital_markdown = required_report_artifact(&run_root, &manifest, "diagnosis.hospital-view")?; + let hospital_value = read_json_artifact(&hospital.path, "diagnosis.hospital")?; let hospital_text = fs::read_to_string(&hospital_markdown.path)?; let agent_code_slice_ranking = optional_report_artifact(&run_root, &manifest, "code_evidence.agent_slice")?; + let sentrux_evidence = project_sentrux_evidence(&run_root, &manifest, &hospital_value); let out = serde_json::json!({ "schema": "code-intel-report.v1", @@ -47,6 +50,7 @@ pub(super) fn report(repo: &Path, artifact_root: Option<&Path>, json: bool) -> R "hospital": hospital.to_json(), "hospitalMarkdown": hospital_markdown.to_json(), "agentCodeSliceRanking": agent_code_slice_ranking.as_ref().map(ReportArtifact::to_json), + "sentruxEvidence": sentrux_evidence, }); if json { println!("{}", serde_json::to_string_pretty(&out)?); @@ -59,6 +63,12 @@ pub(super) fn report(repo: &Path, artifact_root: Option<&Path>, json: bool) -> R if let Some(ranking) = &agent_code_slice_ranking { println!("agentCodeSliceRanking: {}", ranking.path.display()); } + println!( + "sentruxEvidence: {}", + out["sentruxEvidence"]["status"] + .as_str() + .unwrap_or("unknown") + ); println!(); println!("--- hospital.md ---"); print!("{hospital_text}"); @@ -69,6 +79,214 @@ pub(super) fn report(repo: &Path, artifact_root: Option<&Path>, json: bool) -> R Ok(()) } +fn read_json_artifact(path: &Path, artifact_type: &str) -> Result { + let bytes = fs::read(path)?; + serde_json::from_slice(&bytes).map_err(|error| { + format!( + "committed `{artifact_type}` artifact {} is not valid JSON: {error}", + path.display() + ) + .into() + }) +} + +/// Project Sentrux evidence only when the Hospital's references are also +/// present in the committed run manifest and the referenced capability +/// artifact verifies against that manifest. The report must remain useful on +/// older or partial runs, but it must never turn an absent or untrusted +/// capability reference into a successful result. +fn project_sentrux_evidence(run_root: &Path, manifest: &Value, hospital: &Value) -> Value { + let manifest_refs = manifest_capability_refs(manifest); + let hospital_refs = match hospital.pointer("/tools/sentruxCapabilities") { + Some(Value::Array(refs)) => refs, + Some(_) => { + return serde_json::json!({ + "status": if manifest_refs.is_empty() { "unknown" } else { "degraded" }, + "manifestCapabilityRefs": manifest_refs.len(), + "hospitalCapabilityRefs": 0, + "capabilities": [], + "missingReferences": manifest_refs.iter().map(|reference| reference_descriptor(reference)).collect::>(), + "unverifiedReferences": [], + "reason": "Hospital tools.sentruxCapabilities is not an array" + }); + } + None => { + let status = if manifest_refs.is_empty() { + "unknown" + } else { + "degraded" + }; + return serde_json::json!({ + "status": status, + "manifestCapabilityRefs": manifest_refs.len(), + "hospitalCapabilityRefs": 0, + "capabilities": [], + "missingReferences": manifest_refs.iter().map(|reference| reference_descriptor(reference)).collect::>(), + "unverifiedReferences": [], + "reason": if manifest_refs.is_empty() { + "Hospital did not publish Sentrux capability references and the manifest contains none" + } else { + "Manifest contains Sentrux capability references but Hospital did not project them" + } + }); + } + }; + + let manifest_keys = manifest_refs + .iter() + .filter_map(|reference| reference_key(reference).map(|key| (key, *reference))) + .collect::>(); + let mut projected_keys = BTreeSet::new(); + let mut verified = Vec::new(); + let mut unverified = Vec::new(); + + for reference in hospital_refs { + let Some(key) = reference_key(reference) else { + unverified.push(serde_json::json!({ + "reference": reference, + "reason": "Hospital Sentrux capability reference is malformed" + })); + continue; + }; + if !projected_keys.insert(key.clone()) { + unverified.push(serde_json::json!({ + "reference": reference, + "reason": "Hospital Sentrux capability reference is duplicated" + })); + continue; + } + let Some((_, manifest_reference)) = manifest_keys + .iter() + .find(|(candidate, _)| *candidate == key) + else { + unverified.push(serde_json::json!({ + "reference": reference, + "reason": "Hospital Sentrux capability reference is not present in the committed manifest" + })); + continue; + }; + + let Some(snapshot) = manifest["snapshotIdentity"].as_str() else { + unverified.push(serde_json::json!({ + "reference": reference, + "reason": "Committed manifest has no snapshot identity" + })); + continue; + }; + let artifact = match crate::artifact_ref::registered_contract(manifest_reference).and_then( + |contract| { + crate::artifact_ref::verify_artifact_ref( + run_root, + snapshot, + contract, + manifest_reference, + ) + }, + ) { + Ok(artifact) => artifact, + Err(error) => { + unverified.push(serde_json::json!({ + "reference": reference, + "reason": format!("Committed Sentrux capability artifact failed verification: {error:?}") + })); + continue; + } + }; + let payload: Value = match serde_json::from_slice(artifact.bytes()) { + Ok(payload) => payload, + Err(error) => { + unverified.push(serde_json::json!({ + "reference": reference, + "reason": format!("Verified Sentrux capability artifact is not JSON: {error}") + })); + continue; + } + }; + verified.push(serde_json::json!({ + "capabilityId": payload["capabilityId"], + "operation": payload["operation"], + "status": payload["status"], + "authority": payload["authority"], + "verdict": payload.pointer("/outputs/verdict").cloned().unwrap_or(Value::String("unknown".into())), + "provider": payload["provider"], + "artifact": reference_descriptor(manifest_reference), + "verification": "committed_manifest_and_payload" + })); + } + + let missing = manifest_refs + .iter() + .filter(|reference| { + reference_key(reference).is_some_and(|key| !projected_keys.contains(&key)) + }) + .map(|reference| reference_descriptor(reference)) + .collect::>(); + let has_degraded_capability = verified.iter().any(|capability| { + !matches!(capability["status"].as_str(), Some("succeeded")) + || matches!(capability["verdict"].as_str(), Some("fail" | "unknown")) + }); + let status = if verified.is_empty() { + "unknown" + } else if !unverified.is_empty() || !missing.is_empty() || has_degraded_capability { + "degraded" + } else { + "verified" + }; + serde_json::json!({ + "status": status, + "manifestCapabilityRefs": manifest_refs.len(), + "hospitalCapabilityRefs": hospital_refs.len(), + "capabilities": verified, + "missingReferences": missing, + "unverifiedReferences": unverified, + "reason": if status == "verified" { + "All Hospital Sentrux capability references are manifest-bound and payload-verified" + } else if status == "degraded" { + "Sentrux evidence is partially verified; missing, unverified, or non-success capabilities are listed" + } else { + "No Hospital Sentrux capability reference could be verified" + } + }) +} + +fn manifest_capability_refs(manifest: &Value) -> Vec<&Value> { + manifest["nodes"] + .as_object() + .into_iter() + .flat_map(|nodes| nodes.values()) + .flat_map(|node| node["artifacts"].as_array().into_iter().flatten()) + .filter(|reference| { + reference["type"] == "provider.sentrux.capability-artifact" + && reference["artifactSchema"] == "code-intel-sentrux-capability-artifact.v1" + }) + .collect() +} + +fn reference_key(reference: &Value) -> Option { + [ + "schema", + "artifactSchema", + "type", + "sha256", + "consumedSnapshotIdentity", + ] + .into_iter() + .map(|field| reference[field].as_str()) + .collect::>>() + .map(|fields| fields.join("\u{0}")) +} + +fn reference_descriptor(reference: &Value) -> Value { + serde_json::json!({ + "schema": reference["schema"], + "artifactSchema": reference["artifactSchema"], + "type": reference["type"], + "path": reference["path"], + "sha256": reference["sha256"], + "consumedSnapshotIdentity": reference["consumedSnapshotIdentity"] + }) +} + fn required_report_artifact( run_root: &Path, manifest: &Value, @@ -151,3 +369,119 @@ fn latest_committed_run(repo_artifacts: &Path) -> Result<(PathBuf, Value)> { ) .into()) } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn unique_temp_dir() -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be after epoch") + .as_nanos(); + std::env::temp_dir().join(format!("code-intel-report-sentrux-{stamp}")) + } + + fn capability_fixture(root: &Path) -> (String, Value) { + let snapshot = "a".repeat(64); + let payload = json!({ + "schema":"code-intel-sentrux-capability-artifact.v1", + "contractVersion":1, + "capabilityId":"sentrux.scan", + "operation":"scan", + "runId":"run-1", + "snapshotIdentity":snapshot, + "provider":{ + "mode":"builtin", + "id":"sentrux.builtin", + "version":"1.0.0", + "digest":"b".repeat(64) + }, + "status":"succeeded", + "authority":"authoritative", + "inputs":{"snapshotIdentity":snapshot}, + "outputs":{"verdict":"pass"}, + "failure":null, + "freshness":{ + "status":"current", + "evaluatedAt":"2026-08-19T00:00:00Z", + "consumedSnapshotIdentity":snapshot + }, + "decisionConsumers":["diagnosis.hospital"] + }); + let bytes = serde_json::to_vec(&payload).expect("fixture should serialize"); + let relative_path = "objects/sha256/sentrux-scan"; + let path = root.join(relative_path); + fs::create_dir_all(path.parent().expect("fixture has a parent")) + .expect("fixture directory should be created"); + fs::write(path, bytes.clone()).expect("fixture artifact should be written"); + let reference = json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-sentrux-capability-artifact.v1", + "type":"provider.sentrux.capability-artifact", + "path":relative_path, + "sha256":crate::capability::sha256_hex(&bytes), + "consumedSnapshotIdentity":snapshot + }); + (snapshot, reference) + } + + #[test] + fn report_projects_verified_refs_and_marks_untrusted_refs_degraded() { + let root = unique_temp_dir(); + fs::create_dir_all(&root).expect("fixture root should be created"); + let (snapshot, reference) = capability_fixture(&root); + let mut untrusted = reference.clone(); + untrusted["sha256"] = json!("c".repeat(64)); + let manifest = json!({ + "snapshotIdentity":snapshot, + "nodes":{"evidence.sentrux":{"artifacts":[reference]}} + }); + let mut hospital_reference = reference.clone(); + hospital_reference["path"] = json!("sentrux-capability-scan.json"); + let hospital = json!({ + "tools":{"sentruxCapabilities":[hospital_reference, untrusted]} + }); + + let evidence = project_sentrux_evidence(&root, &manifest, &hospital); + + assert_eq!(evidence["status"], "degraded"); + assert_eq!(evidence["manifestCapabilityRefs"], 1); + assert_eq!(evidence["hospitalCapabilityRefs"], 2); + assert_eq!(evidence["capabilities"].as_array().unwrap().len(), 1); + assert_eq!(evidence["capabilities"][0]["capabilityId"], "sentrux.scan"); + assert_eq!( + evidence["capabilities"][0]["verification"], + "committed_manifest_and_payload" + ); + assert_eq!( + evidence["unverifiedReferences"].as_array().unwrap().len(), + 1 + ); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn report_marks_missing_hospital_projection_unknown_or_degraded() { + let root = unique_temp_dir(); + fs::create_dir_all(&root).expect("fixture root should be created"); + let (snapshot, reference) = capability_fixture(&root); + let manifest = json!({ + "snapshotIdentity":snapshot, + "nodes":{"evidence.sentrux":{"artifacts":[reference]}} + }); + + let degraded = project_sentrux_evidence(&root, &manifest, &json!({"tools":{}})); + assert_eq!(degraded["status"], "degraded"); + assert_eq!(degraded["missingReferences"].as_array().unwrap().len(), 1); + + let unknown = project_sentrux_evidence(&root, &json!({"nodes":{}}), &json!({})); + assert_eq!(unknown["status"], "unknown"); + assert!(unknown["capabilities"].as_array().unwrap().is_empty()); + + let _ = fs::remove_dir_all(root); + } +} diff --git a/crates/code-intel-cli/src/builtin_provider_evidence.rs b/crates/code-intel-cli/src/builtin_provider_evidence.rs index 654c58cc..1cb849f3 100644 --- a/crates/code-intel-cli/src/builtin_provider_evidence.rs +++ b/crates/code-intel-cli/src/builtin_provider_evidence.rs @@ -1,6 +1,6 @@ use std::fs; use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; +use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; use serde_json::{json, Value}; @@ -29,13 +29,21 @@ mod graph; mod graph_adapter; #[path = "sentrux_adapter.rs"] mod sentrux_adapter; +#[path = "sentrux_analysis.rs"] +mod sentrux_analysis; +#[path = "sentrux_capability_artifacts.rs"] +mod sentrux_capability_artifacts; +#[path = "sentrux_command.rs"] +mod sentrux_command; #[path = "sentrux_gate.rs"] mod sentrux_gate; +#[path = "sentrux_lite_capabilities.rs"] +mod sentrux_lite_capabilities; use codenexus_scratch::{create_codenexus_scratch_dir, ScratchDir}; +pub(super) use sentrux_command::{command_evidence, SentruxCommand}; use sentrux_gate::Violation; const MAX_AGE_SECONDS: u64 = 300; -const MAX_COMMAND_EVIDENCE_BYTES: usize = 1024 * 1024; // The codenexus-domain effect vocabulary validated by // `codenexus_adapter::validate_native` -- distinct from the generic // repo_read/local_write/process_spawn effects `publish_admission` reports at @@ -134,15 +142,18 @@ pub(super) fn sentrux_admission( let lease = snapshot::begin_consumption(repo, &request["snapshot"]).map_err(AdapterError::Contract)?; let collected_at = now()?; - let gate = run_sentrux(repo, tool_path_prefix, "gate")?; - let check = run_sentrux(repo, tool_path_prefix, "check")?; + let (gate, check, capability_observations) = + sentrux_capability_artifacts::collect_sentrux_capabilities(repo, tool_path_prefix)?; lease.verify_after(repo).map_err(AdapterError::Contract)?; let observed_at = now()?.max(collected_at); let identity = snapshot_identity(request)?; let command_observation = json!({ "schema":"code-intel-sentrux-command-observation.v1", "snapshotIdentity":identity, - "commands":[command_evidence("gate", &gate), command_evidence("check", &check)] + "commands":[ + command_evidence("gate", &gate), + command_evidence("check", &check) + ] }); let command_observation_bytes = serde_json::to_vec(&command_observation).map_err(|error| { AdapterError::Internal(format!("serialize Sentrux command observation: {error}")) @@ -195,17 +206,28 @@ pub(super) fn sentrux_admission( }); let first = sentrux_adapter::translate(&native, observed_at, MAX_AGE_SECONDS) .map_err(AdapterError::Contract)?; + let run_id = format!("sentrux-{identity}"); + let (capability_artifacts, capability_refs) = + sentrux_capability_artifacts::build_capability_artifacts( + &capability_observations, + identity, + &run_id, + )?; let payload = json!({ "schema":"code-intel-evidence-payload.v1", - "data":{"structuralEvidence":{ - "schema":"code-intel-structural-evidence-payload.v1", - "snapshotIdentity":identity, - "provider":first["port"]["provider"], - "provenance":payload_provenance(request), - "effects":first["port"]["effects"], - "completeness":first["port"]["completeness"], - "rules":first["port"]["rules"] - }} + "data":{ + "structuralEvidence":{ + "schema":"code-intel-structural-evidence-payload.v1", + "snapshotIdentity":identity, + "provider":first["port"]["provider"], + "provenance":payload_provenance(request), + "effects":first["port"]["effects"], + "completeness":first["port"]["completeness"], + "rules":first["port"]["rules"] + }, + "sentruxCapabilities":capability_observations, + "capabilityArtifactRefs":capability_refs + } }); fs::create_dir(out) .map_err(|error| AdapterError::Io(format!("create Sentrux provider output: {error}")))?; @@ -218,6 +240,11 @@ pub(super) fn sentrux_admission( .map_err(|error| AdapterError::Internal(format!("serialize Sentrux payload: {error}")))?; fs::write(out.join("sentrux-payload.json"), &payload_bytes) .map_err(|error| AdapterError::Io(format!("write Sentrux payload: {error}")))?; + for artifact in &capability_artifacts { + fs::write(out.join(&artifact.relative_path), &artifact.bytes).map_err(|error| { + AdapterError::Io(format!("write Sentrux capability artifact: {error}")) + })?; + } let mut native = native; native["payload"] = payload_ref("sentrux-payload.json", &payload_bytes, identity); let adapter = sentrux_adapter::translate(&native, observed_at, MAX_AGE_SECONDS) @@ -246,6 +273,7 @@ pub(super) fn sentrux_admission( bytes: command_observation_bytes, }, ]); + output.artifacts.extend(capability_artifacts); Ok(output) } @@ -431,81 +459,6 @@ fn sentrux_provider_options<'a>( Ok((repo, tool_path_prefix)) } -struct SentruxCommand { - argv: Vec, - exit_code: Option, - success: bool, - stdout: String, - stderr: String, - violations: Vec, - governed: bool, -} - -impl SentruxCommand { - fn from_native(run: sentrux_gate::EngineRun, subcommand: &str) -> Self { - Self { - argv: vec![ - "code-intel".into(), - "sentrux".into(), - subcommand.into(), - ".".into(), - ], - exit_code: Some(if run.success { 0 } else { 1 }), - success: run.success, - stdout: run.stdout, - stderr: String::new(), - violations: run.violations, - governed: run.governed, - } - } - - fn from_external(output: Output, subcommand: &str) -> Self { - let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); - // External engines only expose text; keep the per-line failure - // messages so downstream diagnosis is never target-blind. Bounded at - // the producer: an over-verbose external engine must degrade the - // details, never turn the domain verdict into a contract failure. - const MAX_EXTERNAL_VIOLATIONS: usize = 32; - const MAX_EXTERNAL_MESSAGE: usize = 1024; - let violations = if output.status.success() { - Vec::new() - } else { - stdout - .lines() - .filter_map(|line| line.strip_prefix("- ")) - .map(str::trim) - .filter(|message| !message.is_empty()) - .take(MAX_EXTERNAL_VIOLATIONS) - .map(|message| { - let mut bounded = String::new(); - for character in message.chars() { - if bounded.len() + character.len_utf8() > MAX_EXTERNAL_MESSAGE { - break; - } - bounded.push(character); - } - Violation { - rule: format!("sentrux_{subcommand}"), - message: bounded, - targets: Vec::new(), - } - }) - .collect() - }; - Self { - argv: vec!["sentrux".into(), subcommand.into(), ".".into()], - exit_code: output.status.code(), - success: output.status.success(), - stdout, - stderr: String::from_utf8_lossy(&output.stderr).into_owned(), - violations, - // External engines expose no governance signal, so their exit code - // is the only verdict available and is taken at face value. - governed: true, - } - } -} - fn run_sentrux( repo: &Path, tool_path_prefix: Option<&Path>, @@ -515,8 +468,13 @@ fn run_sentrux( Some(prefix) => { let resolved = resolve_sentrux(prefix)?; let mut command = external_command(&resolved); + let provider_subcommand = if subcommand == "provider_discovery" { + "pro_status" + } else { + subcommand + }; let output = command - .arg(subcommand) + .arg(provider_subcommand) .arg(".") .current_dir(repo) .output() @@ -527,8 +485,40 @@ fn run_sentrux( } None => { let run = match subcommand { + "dsm" => return json_command(sentrux_analysis::analyze(repo), subcommand), + "scan" => return json_command(sentrux_gate::scan_json(repo), subcommand), + "rescan" => return json_command(sentrux_gate::scan_json(repo), subcommand), + "health" => return json_command(sentrux_health_json(repo), subcommand), + "git_stats" => { + return json_command( + sentrux_lite_capabilities::git_stats_json(repo), + subcommand, + ) + } + "evolution" => { + return json_command( + sentrux_lite_capabilities::evolution_json(repo), + subcommand, + ) + } + "test_gaps" => { + return json_command( + sentrux_lite_capabilities::test_gaps_json(repo), + subcommand, + ) + } + "what_if" => { + return json_command(sentrux_lite_capabilities::what_if_json(repo), subcommand) + } + "provider_discovery" => { + return json_command( + sentrux_lite_capabilities::provider_discovery_json(), + subcommand, + ) + } + "check_rules" => sentrux_gate::run_check(repo), + "check" => sentrux_gate::run_check_aligned(repo, true), "gate" => sentrux_gate::run_gate(repo, false), - "check" => sentrux_gate::run_check(repo), other => { return Err(AdapterError::Internal(format!( "unsupported built-in Sentrux subcommand: {other}" @@ -539,16 +529,44 @@ fn run_sentrux( SentruxCommand::from_native(run, subcommand) } }; - if command.stdout.len() > MAX_COMMAND_EVIDENCE_BYTES - || command.stderr.len() > MAX_COMMAND_EVIDENCE_BYTES - { - return Err(AdapterError::Contract(format!( - "Sentrux {subcommand} output exceeds the bounded evidence limit" - ))); - } Ok(command) } +fn json_command( + value: Result, + subcommand: &str, +) -> Result { + let value = + value.map_err(|error| AdapterError::Internal(format!("Sentrux {subcommand}: {error}")))?; + let stdout = serde_json::to_string_pretty(&value).map_err(|error| { + AdapterError::Internal(format!("serialize Sentrux {subcommand}: {error}")) + })?; + Ok(SentruxCommand::from_json(stdout.into_bytes(), subcommand)) +} + +fn sentrux_health_json(repo: &Path) -> Result { + let metrics = sentrux_gate::scan_json(repo)?; + let god_files = metrics["god_file_count"].as_i64().unwrap_or(0); + let complex = metrics["complex_fn_count"].as_i64().unwrap_or(0); + let coupling = metrics["coupling_score"].as_f64().unwrap_or(0.0); + let bottleneck = if god_files > 0 { + "god_files" + } else if complex > 0 { + "complexity" + } else if coupling > 20.0 { + "coupling" + } else { + "none" + }; + Ok(json!({ + "status":"ok", + "tool":sentrux_gate::ENGINE_ID, + "quality_signal":metrics["quality_signal"], + "files":metrics["files"], + "bottleneck":bottleneck + })) +} + fn external_command(path: &Path) -> Command { #[cfg(windows)] if path @@ -564,7 +582,7 @@ fn external_command(path: &Path) -> Command { } fn resolve_sentrux(prefix: &Path) -> Result { - sentrux_names() + ["sentrux.exe", "sentrux.cmd", "sentrux.bat", "sentrux"] .iter() .map(|name| prefix.join(name)) .find(|path| path.is_file()) @@ -576,56 +594,16 @@ fn resolve_sentrux(prefix: &Path) -> Result { }) } -fn sentrux_names() -> &'static [&'static str] { - if cfg!(windows) { - &["sentrux.exe", "sentrux.cmd", "sentrux.bat", "sentrux"] - } else { - &["sentrux"] - } -} - -fn command_evidence(subcommand: &str, command: &SentruxCommand) -> Value { - json!({ - "id":subcommand, - "argv":command.argv, - "exitCode":command.exit_code, - "success":command.success, - "stdout":command.stdout, - "stderr":command.stderr - }) -} - -/// Translates one Sentrux command into an authoritative rule verdict. -/// -/// An ungoverned command carries no structural verdict: `check` without -/// `.sentrux/rules.toml` evaluated no rule, and `gate` without -/// `.sentrux/baseline.json` had no prior measurement to detect a regression -/// against. Its nonzero exit code is an operator affordance ("save a -/// baseline"), not evidence that an admitted rule failed, so reporting `fail` -/// here made every never-baselined repository read as an architecture gate -/// failure in diagnosis. The raw exit code and stdout stay verbatim in the -/// command observation artifact, which is where the ungoverned state is -/// auditable. fn command_rule(kind: &str, command: &SentruxCommand) -> Value { let verdict = if command.success || !command.governed { "pass" } else { "fail" }; - let mut rule = json!({ - "kind":kind, - "status":"evaluated", - "verdict":verdict, - "failure":{"kind":"none"} - }); + let mut rule = + json!({"kind":kind,"status":"evaluated","verdict":verdict,"failure":{"kind":"none"}}); if verdict == "fail" && !command.violations.is_empty() { - rule["details"] = json!({ - "violations":command - .violations - .iter() - .map(Violation::to_json) - .collect::>() - }); + rule["details"] = json!({"violations":command.violations.iter().map(Violation::to_json).collect::>()}); } rule } diff --git a/crates/code-intel-cli/src/change_impact.rs b/crates/code-intel-cli/src/change_impact.rs index 0c0387ec..e8c6a1a9 100644 --- a/crates/code-intel-cli/src/change_impact.rs +++ b/crates/code-intel-cli/src/change_impact.rs @@ -6,6 +6,9 @@ use serde_json::{json, Value}; use crate::committed_evidence::{self, CommittedEvidence, EvidenceError}; use crate::impact_graph::{impacted_files, reverse_import_graph, select_tests, test_commands}; +const SENTRUX_CAPABILITY_ARTIFACT_SCHEMA: &str = "code-intel-sentrux-capability-artifact.v1"; +const SENTRUX_CAPABILITY_ARTIFACT_TYPE: &str = "provider.sentrux.capability-artifact"; + pub(crate) fn run_raw(raw: &[String]) -> i32 { // Leaf adapter only — controllers wrap the execute_* paths with typed // authority receipts and must not be imported here (import cycle). @@ -294,6 +297,8 @@ fn build_result( }) }) .collect::>(); + let (sentrux_evidence_refs, sentrux_evidence) = sentrux_evidence(evidence, stale); + let sentrux_signals = sentrux_test_selection_signals(evidence, stale); let mut result = json!({ "schema":"code-intel-change-impact.v1", "repo":cli.repo, @@ -304,6 +309,8 @@ fn build_result( "freshness":freshness, "changed":changed, "evidenceRefs":[files_ref,imports_ref], + "sentruxEvidenceRefs":sentrux_evidence_refs, + "sentruxEvidence":sentrux_evidence, "impact":{ "files":impact_rows, "resolvedImportEdges":resolved_edges, @@ -315,6 +322,7 @@ fn build_result( "commands":commands, "advisoryOnly":true, "rationale":"Select impacted test files reachable through the verified snapshot's reverse import graph; use same-module test co-location only as a fallback.", + "sentruxSignals":sentrux_signals, }, "limitations":[ "Native import extraction is heuristic and does not prove runtime call paths.", @@ -344,6 +352,174 @@ fn build_result( Ok(ChangeImpactResult { value: result }) } +/// Project only manifest refs whose payloads were verified by +/// `committed_evidence::load`. This deliberately does not inspect provider +/// stdout or re-run Sentrux: change impact is a committed-snapshot consumer. +fn sentrux_evidence(evidence: &CommittedEvidence, stale: bool) -> (Vec, Value) { + let refs = evidence + .refs + .iter() + .zip(evidence.verified.iter()) + .filter(|(reference, _)| { + reference["artifactSchema"] == SENTRUX_CAPABILITY_ARTIFACT_SCHEMA + && reference["type"] == SENTRUX_CAPABILITY_ARTIFACT_TYPE + }) + .map(|(reference, _)| reference.clone()) + .collect::>(); + if refs.is_empty() { + return ( + refs, + json!({ + "status":"unknown", + "diagnostics":["No verified Sentrux capability artifact refs are present in the committed evidence; Sentrux-specific impact and test-gap signals are advisory/unknown."], + }), + ); + } + if stale { + ( + refs, + json!({ + "status":"advisory", + "diagnostics":["Sentrux capability refs are verified against the committed snapshot, but this impact result is stale-advisory."], + }), + ) + } else { + ( + refs, + json!({ + "status":"available", + "diagnostics":[], + }), + ) + } +} + +/// Consume only the JSON payloads already verified by the committed manifest. +/// +/// The capability payload contains command provenance for audit purposes, but +/// this projection intentionally never reads `outputs.command.stdout`. The +/// Capability consumers use only structured data admitted into the artifact; +/// provider stdout remains provenance/preview evidence and is never parsed by +/// this projection. +fn sentrux_test_selection_signals(evidence: &CommittedEvidence, stale: bool) -> Value { + let payloads = evidence + .refs + .iter() + .zip(evidence.verified.iter()) + .filter(|(reference, _)| { + reference["artifactSchema"] == SENTRUX_CAPABILITY_ARTIFACT_SCHEMA + && reference["type"] == SENTRUX_CAPABILITY_ARTIFACT_TYPE + }) + .filter_map(|(_, verified)| serde_json::from_slice::(verified.bytes()).ok()) + .collect::>(); + let test_gap_payload = payloads + .iter() + .find(|payload| payload["capabilityId"] == "sentrux.test_gaps"); + let dsm_payload = payloads + .iter() + .find(|payload| payload["capabilityId"] == "sentrux.dsm"); + let what_if_payload = payloads + .iter() + .find(|payload| payload["capabilityId"] == "sentrux.what_if"); + let test_gap = sentrux_signal("test_gaps", test_gap_payload); + let dsm = sentrux_signal("dsm", dsm_payload); + let what_if = sentrux_signal("what_if", what_if_payload); + let has_signal = test_gap["status"] != "unknown" + || dsm["status"] != "unknown" + || what_if["status"] != "unknown"; + let all_available = test_gap["status"] == "available" && dsm["status"] == "available"; + let what_if_risk = what_if["structuredData"]["summary"]["failingScenarioCount"] + .as_u64() + .unwrap_or(0); + let candidate_test_impact = if !has_signal { + "unknown" + } else if what_if_risk > 0 && !stale { + "retains_graph_candidates_with_what_if_risk" + } else if all_available && !stale { + "retains_graph_candidates" + } else { + "withholds_sentrux_expansion" + }; + let status = if !has_signal { + "unknown" + } else if all_available && !stale { + "available" + } else { + "advisory" + }; + let mut limitations = vec![ + "Sentrux signals are advisory and never execute tests or gate this impact result." + .to_string(), + "Only committed-manifest capability payloads verified against the snapshot are consumed." + .to_string(), + ]; + if stale { + limitations.push( + "Sentrux signals are stale-advisory because the committed snapshot differs from the current checkout." + .to_string(), + ); + } + for signal in [&test_gap, &dsm, &what_if] { + if let Some(items) = signal["limitations"].as_array() { + limitations.extend(items.iter().filter_map(Value::as_str).map(str::to_owned)); + } + } + limitations.sort(); + limitations.dedup(); + json!({ + "status":status, + "testGap":test_gap, + "dsm":dsm, + "whatIf":what_if, + "whatIfFailingScenarioCount":what_if_risk, + "candidateTestImpact":candidate_test_impact, + "limitations":limitations, + }) +} + +fn sentrux_signal(name: &str, payload: Option<&Value>) -> Value { + let Some(payload) = payload else { + return json!({ + "status":"unknown", + "capabilityStatus":"missing", + "authority":"unknown", + "candidateImpact":"unknown", + "structuredData":Value::Null, + "limitations":[format!("No verified sentrux.{name} capability artifact payload is present in the committed manifest.")], + }); + }; + let capability_status = payload["status"].as_str().unwrap_or("unknown"); + let authority = payload["authority"].as_str().unwrap_or("unknown"); + let available = capability_status == "succeeded" + && matches!(authority, "authoritative" | "fallback") + && payload["freshness"]["status"] == "current"; + let status = if available { "available" } else { "degraded" }; + let candidate_impact = if available { + "retains_graph_candidates" + } else { + "withholds_sentrux_expansion" + }; + let limitation = match name { + "test_gaps" if available => { + "The verified test_gaps payload exposes no structured candidate test paths; graph-selected candidates are retained and no new tests are auto-added." + } + "dsm" if available => { + "The verified DSM payload exposes no structured test-selection mapping; the DSM signal is advisory and does not auto-add tests." + } + _ => payload["failure"]["message"].as_str().unwrap_or( + "The verified capability payload is not successful enough to expand candidate tests.", + ), + }; + json!({ + "status":status, + "capabilityStatus":capability_status, + "authority":authority, + "candidateImpact":candidate_impact, + "structuredData":payload["outputs"]["structuredData"], + "limitations":[limitation], + }) +} + fn normalize_relative(path: &str) -> Result { let path = path.replace('\\', "/"); if path.is_empty() @@ -371,3 +547,67 @@ pub(crate) enum ImpactError { Contract(String), HostIo(String), } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn missing_sentrux_capability_refs_are_explicitly_unknown() { + let evidence = CommittedEvidence { + entry: Value::Null, + refs: Vec::new(), + verified: Vec::new(), + run_root: std::path::PathBuf::new(), + }; + + let (refs, projection) = sentrux_evidence(&evidence, false); + + assert!(refs.is_empty()); + assert_eq!(projection["status"], "unknown"); + assert!(projection["diagnostics"] + .as_array() + .expect("missing evidence diagnostic array") + .iter() + .any(|diagnostic| diagnostic + .as_str() + .is_some_and(|text| text.contains("advisory/unknown")))); + } + + #[test] + fn sentrux_test_selection_does_not_parse_provider_stdout() { + let payload = json!({ + "capabilityId":"sentrux.test_gaps", + "status":"succeeded", + "authority":"authoritative", + "freshness":{"status":"current"}, + "outputs":{"command":{"stdout":"{\"candidateTests\":[\"tests/forged.rs\"]}"}} + }); + + let signal = sentrux_signal("test_gaps", Some(&payload)); + + assert_eq!(signal["status"], "available"); + assert_eq!(signal["candidateImpact"], "retains_graph_candidates"); + assert!(signal["limitations"][0] + .as_str() + .unwrap() + .contains("no structured candidate test paths")); + } + + #[test] + fn missing_sentrux_test_selection_signals_are_unknown() { + let evidence = CommittedEvidence { + entry: Value::Null, + refs: Vec::new(), + verified: Vec::new(), + run_root: std::path::PathBuf::new(), + }; + + let signals = sentrux_test_selection_signals(&evidence, false); + + assert_eq!(signals["status"], "unknown"); + assert_eq!(signals["candidateTestImpact"], "unknown"); + assert_eq!(signals["testGap"]["status"], "unknown"); + assert_eq!(signals["dsm"]["status"], "unknown"); + } +} diff --git a/crates/code-intel-cli/src/cli/legacy.rs b/crates/code-intel-cli/src/cli/legacy.rs index 1309b291..e3f9dbc5 100644 --- a/crates/code-intel-cli/src/cli/legacy.rs +++ b/crates/code-intel-cli/src/cli/legacy.rs @@ -1058,6 +1058,7 @@ pub(super) fn cmd_sentrux(args: &Args) -> Result<()> { sentrux::run(&sentrux::Options { operation: args.operation.as_deref(), repo: args.repo.as_deref(), + json: args.json, no_ratchet: args.no_ratchet, }) } @@ -1147,6 +1148,7 @@ Commands: lint hardcoded-paths [] [--json] route|routes [--action List|Plan|Validate] [--provider repowise|understand] [--operation ] [--repo ] [--json] sentrux [--no-ratchet] + sentrux capabilities [] [--json] (read-only capability matrix audit) (--no-ratchet: `check` only, skip the .sentrux/baseline.json ratchet) capability exec --request --out [--artifact-root ] [--manifest ] model inventory-validate --request [--out ] diff --git a/crates/code-intel-cli/src/hospital_diagnosis.rs b/crates/code-intel-cli/src/hospital_diagnosis.rs index 5f8404b3..50c7af64 100644 --- a/crates/code-intel-cli/src/hospital_diagnosis.rs +++ b/crates/code-intel-cli/src/hospital_diagnosis.rs @@ -34,6 +34,7 @@ struct Signals { modernization_debt: bool, top_target: Option, failing_rules: Vec, + sentrux_capability_refs: Vec, admissions: BTreeMap, } @@ -55,6 +56,7 @@ impl Default for Signals { modernization_debt: false, top_target: None, failing_rules: Vec::new(), + sentrux_capability_refs: Vec::new(), admissions: BTreeMap::new(), } } @@ -260,6 +262,15 @@ fn consume_admission(input: &VerifiedArtifact, signals: &mut Signals) -> Result< ); } } + if let Some(refs) = data.get("capabilityArtifactRefs").and_then(Value::as_array) { + require_provider_modality(provider, "structural_evidence")?; + if !signals.sentrux_capability_refs.is_empty() { + return Err(AdapterError::Contract( + "duplicate admitted Sentrux capability artifacts".into(), + )); + } + signals.sentrux_capability_refs = refs.clone(); + } if let Some(native) = data.get("nativeCode") { require_provider_modality(provider, "native_code")?; if signals.native_seen { @@ -426,7 +437,7 @@ fn diagnose(request: &Value, s: &Signals, audit: Option<&AuditReport>) -> Value "diagnosis":{"findings":[diagnosis],"impression":diagnosis,"risk":status,"evidence":evidence}, "treatment":{"plan":treatment,"follow_up":["Rerun diagnosis.hospital with current admitted evidence."]}, "protocols":[], - "tools":{}, + "tools":{"sentruxCapabilities":s.sentrux_capability_refs}, "surgery_plan":{ "schema":"code-intel-surgery-plan.v1", "status":surgery_status, diff --git a/crates/code-intel-cli/src/main.rs b/crates/code-intel-cli/src/main.rs index 019b67ea..1056a511 100644 --- a/crates/code-intel-cli/src/main.rs +++ b/crates/code-intel-cli/src/main.rs @@ -62,6 +62,7 @@ mod run_error; mod runtime_ci_evidence; mod sentrux; mod sentrux_analysis; +mod sentrux_capabilities; mod sentrux_gate; mod session_evidence; mod snapshot; diff --git a/crates/code-intel-cli/src/sentrux.rs b/crates/code-intel-cli/src/sentrux.rs index ef892bf7..c72febf7 100644 --- a/crates/code-intel-cli/src/sentrux.rs +++ b/crates/code-intel-cli/src/sentrux.rs @@ -1,4 +1,5 @@ use crate::sentrux_analysis; +use crate::sentrux_capabilities; use crate::sentrux_gate; use crate::Result; use std::path::Path; @@ -6,27 +7,26 @@ use std::path::Path; pub struct Options<'a> { pub operation: Option<&'a str>, pub repo: Option<&'a Path>, - /// CLI `--no-ratchet`. Only consulted by the `check` operation; see - /// `sentrux_gate::run_check_aligned`. + pub json: bool, pub no_ratchet: bool, } -/// Structural operations served by the built-in engine. The PATH-resolved -/// `sentrux` binary is no longer consulted here: the gate verdict must be a -/// function of the snapshot and this binary. External Sentrux implementations -/// remain reachable through the provider `toolPathPrefix` seam. -/// -/// `check` evaluates both `.sentrux/rules.toml` and, by default, the -/// `.sentrux/baseline.json` ratchet that `gate` evaluates -- the same two -/// verdicts the authoritative `evidence.sentrux` DAG node always computes -/// (issue #106: before this, `check` only ran the static rules, so -/// `code-intel sentrux check` could report green while `code-intel run -/// execute` failed on the same tree). Pass `--no-ratchet` to see only the -/// static verdict; the output says so explicitly. `check_rules` is the -/// unconditional static-only alias kept for parity with the legacy PS1 -/// tool's `check_rules` operation. pub fn run(options: &Options<'_>) -> Result<()> { let operation = options.operation.ok_or("sentrux requires an operation")?; + + if operation == "capabilities" { + let repo = match options.repo { + Some(repo) => repo.canonicalize().map_err(|error| { + format!( + "sentrux capabilities repository '{}' is unavailable: {error}", + repo.display() + ) + })?, + None => std::env::current_dir()?, + }; + return sentrux_capabilities::run_capabilities(&repo, options.json); + } + let repo = options.repo.ok_or("sentrux requires a repo/path")?; let repo = repo.canonicalize()?; match operation { diff --git a/crates/code-intel-cli/src/sentrux_capabilities.rs b/crates/code-intel-cli/src/sentrux_capabilities.rs new file mode 100644 index 00000000..006e8aad --- /dev/null +++ b/crates/code-intel-cli/src/sentrux_capabilities.rs @@ -0,0 +1,447 @@ +use crate::Result; +use serde_json::{Map, Value}; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::Path; + +const CAPABILITY_MATRIX_RELATIVE_PATH: &str = "orchestration/sentrux-capability-matrix.v1.json"; +const CAPABILITY_MATRIX_SCHEMA: &str = "code-intel-sentrux-capability-matrix.v1"; + +pub(crate) fn run_capabilities(repo: &Path, json: bool) -> Result<()> { + let matrix = load_capability_matrix(repo)?; + let audit = capability_audit(&matrix)?; + + if json { + println!("{}", serde_json::to_string_pretty(&audit)?); + } else { + let coverage = audit + .get("coverage") + .ok_or("sentrux capabilities audit omitted coverage")?; + println!( + "capability coverage: {} ({}/{} required capabilities covered)", + coverage["status"].as_str().unwrap_or("unknown"), + coverage["requiredCovered"].as_u64().unwrap_or(0), + coverage["required"].as_u64().unwrap_or(0), + ); + println!("complete: {}", audit["complete"].as_bool().unwrap_or(false)); + for capability in audit["capabilities"] + .as_array() + .ok_or("sentrux capabilities audit omitted capabilities")? + { + let consumers = capability["decisionConsumers"] + .as_array() + .map(|values| { + values + .iter() + .filter_map(Value::as_str) + .collect::>() + .join(",") + }) + .unwrap_or_default(); + println!( + "- {} operation={} currentState={} route={} decisionConsumers=[{}]", + capability["id"].as_str().unwrap_or(""), + capability["operation"].as_str().unwrap_or(""), + capability["currentState"].as_str().unwrap_or(""), + capability["route"].as_str().unwrap_or(""), + consumers, + ); + } + } + Ok(()) +} + +fn load_capability_matrix(repo: &Path) -> Result { + let path = repo.join(CAPABILITY_MATRIX_RELATIVE_PATH); + let text = fs::read_to_string(&path).map_err(|error| { + format!( + "sentrux capabilities matrix is missing or unreadable at '{}': {error}", + path.display() + ) + })?; + let matrix = serde_json::from_str(text.trim_start_matches('\u{feff}')).map_err(|error| { + format!( + "sentrux capabilities matrix JSON error at '{}': {error}", + path.display() + ) + })?; + validate_capability_matrix(&matrix, &path)?; + Ok(matrix) +} + +fn capability_audit(matrix: &Value) -> Result { + let object = matrix + .as_object() + .ok_or("sentrux capabilities matrix schema/header error: root must be an object")?; + let capabilities = object["capabilities"] + .as_array() + .ok_or("sentrux capabilities matrix schema/header error: capabilities must be an array")?; + let policy = object["completionPolicy"].as_object().ok_or( + "sentrux capabilities matrix schema/header error: completionPolicy must be an object", + )?; + let required_states = policy["requiredStatesForComplete"] + .as_array() + .ok_or("sentrux capabilities matrix schema/header error: required completion states must be an array")? + .iter() + .filter_map(Value::as_str) + .collect::>(); + let automatic_modes = policy["automaticExecutionModes"] + .as_array() + .ok_or("sentrux capabilities matrix schema/header error: automatic execution modes must be an array")? + .iter() + .filter_map(Value::as_str) + .collect::>(); + let explicit_modes = policy["explicitExecutionModes"] + .as_array() + .ok_or("sentrux capabilities matrix schema/header error: explicit execution modes must be an array")? + .iter() + .filter_map(Value::as_str) + .collect::>(); + let forbidden_states = policy["forbiddenSilentStates"] + .as_array() + .ok_or("sentrux capabilities matrix schema/header error: forbidden silent states must be an array")? + .iter() + .filter_map(Value::as_str) + .collect::>(); + + let mut state_counts = BTreeMap::::new(); + let mut required = 0u64; + let mut covered = 0u64; + let mut required_covered = 0u64; + let mut output_capabilities = Vec::with_capacity(capabilities.len()); + + for capability in capabilities { + let item = capability.as_object().ok_or( + "sentrux capabilities matrix schema/header error: capability must be an object", + )?; + let current_state = item["currentState"].as_str().ok_or( + "sentrux capabilities matrix schema/header error: currentState must be a string", + )?; + let execution_mode = item["executionMode"].as_str().ok_or( + "sentrux capabilities matrix schema/header error: executionMode must be a string", + )?; + let is_covered = if automatic_modes.contains(execution_mode) { + required_states.contains(current_state) + } else if explicit_modes.contains(execution_mode) { + !forbidden_states.contains(current_state) + } else { + false + }; + *state_counts.entry(current_state.to_string()).or_default() += 1; + if is_covered { + covered += 1; + } + if item["requiredForRelease"].as_bool().unwrap_or(false) { + required += 1; + if is_covered { + required_covered += 1; + } + } + output_capabilities.push(serde_json::json!({ + "id": item["id"], + "operation": item["operation"], + "currentState": item["currentState"], + "executionMode": item["executionMode"], + "route": item["route"], + "decisionConsumers": item["decisionConsumers"], + })); + } + + let complete = capabilities + .iter() + .filter(|item| item["requiredForRelease"].as_bool().unwrap_or(false)) + .all(|item| { + let execution_mode = item["executionMode"].as_str().unwrap_or(""); + let state_allowed = item["currentState"] + .as_str() + .is_some_and(|state| required_states.contains(state)); + let explicit_state_allowed = item["currentState"] + .as_str() + .is_some_and(|state| !forbidden_states.contains(state)); + let has_artifact = item["artifacts"] + .as_array() + .is_some_and(|values| !values.is_empty()); + let has_consumer = item["decisionConsumers"] + .as_array() + .is_some_and(|values| !values.is_empty()); + ((automatic_modes.contains(execution_mode) && state_allowed) + || (explicit_modes.contains(execution_mode) && explicit_state_allowed)) + && has_artifact + && has_consumer + }); + + let coverage = serde_json::json!({ + "status": object["coverageStatus"], + "total": capabilities.len(), + "required": required, + "covered": covered, + "requiredCovered": required_covered, + "byState": state_counts, + }); + Ok(serde_json::json!({ + "schema": "code-intel-sentrux-capability-audit.v1", + "coverage": coverage, + "capabilities": output_capabilities, + "complete": complete, + })) +} + +fn validate_capability_matrix(matrix: &Value, path: &Path) -> Result<()> { + let object = matrix + .as_object() + .ok_or_else(|| matrix_error(path, "root must be an object"))?; + let schema = object + .get("schema") + .and_then(Value::as_str) + .ok_or_else(|| matrix_error(path, "schema header must be a string"))?; + if schema != CAPABILITY_MATRIX_SCHEMA { + return Err(matrix_error( + path, + &format!("schema header must be '{CAPABILITY_MATRIX_SCHEMA}', got '{schema}'"), + )); + } + if object.get("contractVersion").and_then(Value::as_u64) != Some(1) { + return Err(matrix_error(path, "contractVersion header must be 1")); + } + if object + .get("coverageStatus") + .and_then(Value::as_str) + .is_none() + { + return Err(matrix_error(path, "coverageStatus header must be a string")); + } + + let policy = object + .get("completionPolicy") + .and_then(Value::as_object) + .ok_or_else(|| matrix_error(path, "completionPolicy header must be an object"))?; + string_array( + policy, + "requiredStatesForComplete", + path, + "completionPolicy", + )?; + string_array(policy, "automaticExecutionModes", path, "completionPolicy")?; + string_array(policy, "explicitExecutionModes", path, "completionPolicy")?; + string_array(policy, "forbiddenSilentStates", path, "completionPolicy")?; + if policy.get("rule").and_then(Value::as_str).is_none() { + return Err(matrix_error(path, "completionPolicy.rule must be a string")); + } + + let capabilities = object + .get("capabilities") + .and_then(Value::as_array) + .ok_or_else(|| matrix_error(path, "capabilities must be an array"))?; + if capabilities.is_empty() { + return Err(matrix_error(path, "capabilities must not be empty")); + } + let mut ids = BTreeSet::new(); + let mut operations = BTreeSet::new(); + let mut aliases = BTreeSet::new(); + for (index, capability) in capabilities.iter().enumerate() { + let item = capability.as_object().ok_or_else(|| { + matrix_error(path, &format!("capabilities[{index}] must be an object")) + })?; + for field in ["id", "operation", "executionMode", "currentState", "route"] { + if item.get(field).and_then(Value::as_str).is_none() { + return Err(matrix_error( + path, + &format!("capabilities[{index}].{field} must be a string"), + )); + } + } + let id = item["id"].as_str().expect("validated capability id"); + if !ids.insert(id) { + return Err(matrix_error( + path, + &format!("duplicate capability id '{id}'"), + )); + } + let operation = item["operation"].as_str().expect("validated operation"); + if !operations.insert(operation) { + return Err(matrix_error( + path, + &format!("duplicate capability operation '{operation}'"), + )); + } + for alias in string_array(item, "aliases", path, &format!("capabilities[{index}]"))? { + if !aliases.insert(alias.clone()) || ids.contains(alias.as_str()) { + return Err(matrix_error( + path, + &format!("duplicate capability alias '{alias}'"), + )); + } + } + if item + .get("requiredForRelease") + .and_then(Value::as_bool) + .is_none() + { + return Err(matrix_error( + path, + &format!("capabilities[{index}].requiredForRelease must be a boolean"), + )); + } + for field in ["artifacts", "decisionConsumers"] { + string_array(item, field, path, &format!("capabilities[{index}]"))?; + } + } + Ok(()) +} + +fn string_array( + object: &Map, + field: &str, + path: &Path, + context: &str, +) -> Result> { + let values = object.get(field).and_then(Value::as_array).ok_or_else(|| { + matrix_error( + path, + &format!("{context}.{field} must be an array of strings"), + ) + })?; + values + .iter() + .enumerate() + .map(|(index, value)| { + value.as_str().map(ToString::to_string).ok_or_else(|| { + matrix_error( + path, + &format!("{context}.{field}[{index}] must be a string"), + ) + }) + }) + .collect() +} + +fn matrix_error(path: &Path, message: &str) -> Box { + format!( + "sentrux capabilities matrix schema/header error at '{}': {message}", + path.display() + ) + .into() +} + +#[cfg(test)] +mod tests { + use super::{capability_audit, load_capability_matrix, validate_capability_matrix}; + use serde_json::json; + use std::fs; + use std::path::{Path, PathBuf}; + + fn temp_repo(label: &str) -> PathBuf { + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock") + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-sentrux-capabilities-{label}-{}-{nonce}", + std::process::id() + )); + fs::create_dir_all(path.join("orchestration")).expect("create matrix fixture"); + path + } + + fn write_matrix(repo: &Path, contents: &str) { + fs::write( + repo.join("orchestration/sentrux-capability-matrix.v1.json"), + contents, + ) + .expect("write matrix fixture"); + } + + #[test] + fn current_matrix_reports_partial_coverage_without_claiming_execution() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let repo = manifest_dir + .parent() + .and_then(Path::parent) + .expect("repository root"); + let matrix = load_capability_matrix(repo).expect("load repository matrix"); + let audit = capability_audit(&matrix).expect("render audit"); + assert_eq!(audit["coverage"]["status"], "partial"); + assert_eq!(audit["complete"], false); + assert!(audit["capabilities"][0].get("id").is_some()); + assert!(audit["capabilities"][0].get("operation").is_some()); + assert!(audit["capabilities"][0].get("currentState").is_some()); + assert!(audit["capabilities"][0].get("route").is_some()); + assert!(audit["capabilities"][0].get("decisionConsumers").is_some()); + } + + #[test] + fn missing_matrix_is_an_explicit_error() { + let repo = temp_repo("missing"); + let error = load_capability_matrix(&repo).expect_err("missing matrix must fail"); + assert!(error.to_string().contains("missing or unreadable")); + let _ = fs::remove_dir_all(repo); + } + + #[test] + fn malformed_matrix_json_is_an_explicit_error() { + let repo = temp_repo("json"); + write_matrix(&repo, "{not-json"); + let error = load_capability_matrix(&repo).expect_err("malformed matrix must fail"); + assert!(error.to_string().contains("JSON error")); + let _ = fs::remove_dir_all(repo); + } + + #[test] + fn invalid_matrix_header_is_an_explicit_error() { + let repo = temp_repo("header"); + write_matrix( + &repo, + &serde_json::to_string(&json!({ + "schema": "wrong-schema", + "contractVersion": 1, + "coverageStatus": "partial", + "completionPolicy": { + "requiredStatesForComplete": ["authoritative_automatic"], + "automaticExecutionModes": ["automatic"], + "explicitExecutionModes": ["explicit_authority", "lifecycle_external"], + "forbiddenSilentStates": ["declared_only"], + "rule": "rule" + }, + "capabilities": [] + })) + .expect("serialize matrix fixture"), + ); + let error = load_capability_matrix(&repo).expect_err("invalid header must fail"); + assert!(error.to_string().contains("schema/header error")); + assert!(error.to_string().contains("schema header")); + let _ = fs::remove_dir_all(repo); + } + + #[test] + fn minimal_valid_matrix_obeys_completion_policy_fields() { + let matrix = json!({ + "schema": "code-intel-sentrux-capability-matrix.v1", + "contractVersion": 1, + "coverageStatus": "complete", + "completionPolicy": { + "requiredStatesForComplete": ["authoritative_automatic"], + "automaticExecutionModes": ["automatic"], + "explicitExecutionModes": ["explicit_authority", "lifecycle_external"], + "forbiddenSilentStates": ["declared_only"], + "rule": "rule" + }, + "capabilities": [{ + "id": "sentrux.example", + "operation": "example", + "aliases": [], + "currentState": "authoritative_automatic", + "executionMode": "automatic", + "route": "provider.sentrux-adapt", + "requiredForRelease": true, + "artifacts": ["example.v1"], + "decisionConsumers": ["release_gate"] + }] + }); + validate_capability_matrix(&matrix, Path::new("fixture.json")) + .expect("minimal matrix should validate"); + assert_eq!( + capability_audit(&matrix).expect("render audit")["complete"], + true + ); + } +} diff --git a/crates/code-intel-cli/src/sentrux_capability_artifacts.rs b/crates/code-intel-cli/src/sentrux_capability_artifacts.rs new file mode 100644 index 00000000..c624aad5 --- /dev/null +++ b/crates/code-intel-cli/src/sentrux_capability_artifacts.rs @@ -0,0 +1,582 @@ +use std::path::Path; + +use serde_json::{json, Value}; + +use crate::adapter_contract::{AdapterArtifact, AdapterError}; +use crate::capability::sha256_hex; + +use super::{command_evidence, run_sentrux, SentruxCommand}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum RouteKind { + Command, + ReuseScan, + NotApplicable { + failure_kind: &'static str, + message: &'static str, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct CapabilityRoute { + capability_id: &'static str, + operation: &'static str, + command: &'static str, + kind: RouteKind, +} + +// This is the executor's canonical dispatch table. The matrix remains the +// inventory of record; keeping the table explicit here makes an omitted route +// observable in tests and in the emitted artifact set instead of becoming a +// silent loop omission. +const SENTRUX_CAPABILITY_ROUTES: [CapabilityRoute; 15] = [ + route("sentrux.gate", "gate", "gate", RouteKind::Command), + route("sentrux.check", "check", "check", RouteKind::Command), + route("sentrux.scan", "scan", "scan", RouteKind::Command), + route("sentrux.health", "health", "health", RouteKind::Command), + route("sentrux.dsm", "dsm", "dsm", RouteKind::Command), + route( + "sentrux.check_rules", + "check_rules", + "check_rules", + RouteKind::Command, + ), + route( + "sentrux.baseline_save", + "gate_save", + "gate_save", + RouteKind::NotApplicable { + failure_kind: "explicit_mutation_required", + message: "baseline_save mutates the repository baseline and requires explicit authority", + }, + ), + route( + "sentrux.rescan", + "rescan", + "rescan", + RouteKind::ReuseScan, + ), + route( + "sentrux.git_stats", + "git_stats", + "git_stats", + RouteKind::Command, + ), + route( + "sentrux.evolution", + "evolution", + "evolution", + RouteKind::Command, + ), + route( + "sentrux.test_gaps", + "test_gaps", + "test_gaps", + RouteKind::Command, + ), + route("sentrux.what_if", "what_if", "what_if", RouteKind::Command), + route( + "sentrux.session_start", + "session_start", + "session_start", + RouteKind::NotApplicable { + failure_kind: "session_lifecycle_outside_dag", + message: "session_start is an agent lifecycle event and is not applicable to this repository DAG run", + }, + ), + route( + "sentrux.session_end", + "session_end", + "session_end", + RouteKind::NotApplicable { + failure_kind: "session_lifecycle_outside_dag", + message: "session_end is an agent lifecycle event and is not applicable to this repository DAG run", + }, + ), + route( + "sentrux.provider_discovery", + "provider_discovery", + "provider_discovery", + RouteKind::Command, + ), +]; + +const fn route( + capability_id: &'static str, + operation: &'static str, + command: &'static str, + kind: RouteKind, +) -> CapabilityRoute { + CapabilityRoute { + capability_id, + operation, + command, + kind, + } +} + +pub(super) fn collect_sentrux_capabilities( + repo: &Path, + tool_path_prefix: Option<&Path>, +) -> Result<(SentruxCommand, SentruxCommand, Vec), AdapterError> { + let mut gate = None; + let mut check = None; + let mut observations = Vec::with_capacity(SENTRUX_CAPABILITY_ROUTES.len()); + for route in SENTRUX_CAPABILITY_ROUTES { + let provider_mode = route_provider_mode(&route, tool_path_prefix); + let route_tool_path_prefix = if provider_mode == "lite_fallback" { + None + } else { + tool_path_prefix + }; + let observation = match route.kind { + RouteKind::NotApplicable { + failure_kind, + message, + } => not_applicable_observation(&route, provider_mode, failure_kind, message), + RouteKind::ReuseScan => { + match run_sentrux( + repo, + route_tool_path_prefix, + if route_tool_path_prefix.is_some() { + route.command + } else { + "scan" + }, + ) { + Ok(command) => capability_observation( + &route, + provider_mode, + &command, + Some("authoritative"), + ), + Err(error) => route_error_observation(&route, provider_mode, &error), + } + } + RouteKind::Command => { + if route_tool_path_prefix.is_none() + && !matches!( + route.command, + "gate" + | "check" + | "scan" + | "health" + | "dsm" + | "check_rules" + | "git_stats" + | "evolution" + | "test_gaps" + | "what_if" + | "provider_discovery" + ) + { + unavailable_observation( + &route, + provider_mode, + "the built-in Rust provider has no route for this capability", + ) + } else { + match run_sentrux(repo, route_tool_path_prefix, route.command) { + Ok(command) => capability_observation( + &route, + provider_mode, + &command, + Some("authoritative"), + ), + Err(error) if matches!(route.command, "gate" | "check") => { + return Err(error) + } + Err(error) => route_error_observation(&route, provider_mode, &error), + } + } + } + }; + if route.capability_id == "sentrux.gate" { + if let Some(command) = observation_command(&observation) { + gate = Some(command); + } + } else if route.capability_id == "sentrux.check" { + if let Some(command) = observation_command(&observation) { + check = Some(command); + } + } + observations.push(observation); + } + Ok(( + gate.ok_or_else(|| AdapterError::Internal("Sentrux gate observation is missing".into()))?, + check + .ok_or_else(|| AdapterError::Internal("Sentrux check observation is missing".into()))?, + observations, + )) +} + +pub(super) fn build_capability_artifacts( + observations: &[Value], + snapshot_identity: &str, + run_id: &str, +) -> Result<(Vec, Vec), AdapterError> { + let mut artifacts = Vec::with_capacity(observations.len()); + let mut refs = Vec::with_capacity(observations.len()); + for observation in observations { + let capability_id = observation["capabilityId"].as_str().ok_or_else(|| { + AdapterError::Contract("Sentrux capability observation has no capabilityId".into()) + })?; + let operation = observation["operation"].as_str().ok_or_else(|| { + AdapterError::Contract("Sentrux capability observation has no operation".into()) + })?; + let provider = + sentrux_capability_provider(observation["providerMode"].as_str().unwrap_or("builtin")); + let raw_status = observation["status"].as_str().unwrap_or("failed"); + let status = match raw_status { + "not_run" | "not_applicable" => "not_applicable", + "succeeded" | "degraded" | "unavailable" | "skipped" | "failed" => raw_status, + other => { + return Err(AdapterError::Contract(format!( + "unsupported Sentrux capability status: {other}" + ))) + } + }; + let artifact = json!({ + "schema":"code-intel-sentrux-capability-artifact.v1", + "contractVersion":1, + "capabilityId":capability_id, + "operation":operation, + "runId":run_id, + "snapshotIdentity":snapshot_identity, + "provider":provider.clone(), + "status":status, + "authority":artifact_authority(observation, status), + "inputs":{"snapshotIdentity":snapshot_identity}, + "outputs":{ + "command":observation["command"], + "verdict":observation["verdict"], + "outputSummary":observation["outputSummary"], + "structuredData":capability_structured_data(observation) + }, + "failure":capability_failure(observation, status), + "freshness":{ + "status":"current", + // The parent Sentrux observation carries the wall-clock + // freshness authority. Capability artifacts are themselves + // content-addressed, so their snapshot-bound projection + // must not embed a per-run timestamp. + "evaluatedAt":null, + "consumedSnapshotIdentity":snapshot_identity + }, + "decisionConsumers":sentrux_decision_consumers(capability_id) + }); + let bytes = serde_json::to_vec(&artifact).map_err(|error| { + AdapterError::Internal(format!("serialize Sentrux capability artifact: {error}")) + })?; + let relative_path = format!( + "sentrux-capability-{}.json", + capability_id.replace('.', "-") + ); + refs.push(json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-sentrux-capability-artifact.v1", + "type":"provider.sentrux.capability-artifact", + "path":relative_path, + "sha256":sha256_hex(&bytes), + "consumedSnapshotIdentity":snapshot_identity + })); + artifacts.push(AdapterArtifact { + artifact_schema: "code-intel-sentrux-capability-artifact.v1".into(), + artifact_type: "provider.sentrux.capability-artifact".into(), + relative_path, + bytes, + }); + } + Ok((artifacts, refs)) +} + +fn capability_structured_data(observation: &Value) -> Value { + observation["command"]["stdout"] + .as_str() + .and_then(|stdout| serde_json::from_str::(stdout).ok()) + .filter(|value| value.is_object() || value.is_array()) + .unwrap_or(Value::Null) +} + +fn sentrux_capability_provider(provider_mode: &str) -> Value { + match provider_mode { + "external" => json!({ + "mode":"external", + "id":"sentrux.command-adapter", + "version":"1.0.0", + "digest":sha256_hex(include_bytes!("builtin_provider_evidence.rs")) + }), + "lite_fallback" => json!({ + "mode":"lite_fallback", + "id":"sentrux.lite-capabilities", + "version":"1.0.0", + "digest":sha256_hex(include_bytes!("sentrux_lite_capabilities.rs")) + }), + _ => json!({ + "mode":"builtin", + "id":super::sentrux_gate::ENGINE_ID, + "version":super::sentrux_gate::ENGINE_VERSION, + "digest":sha256_hex(include_bytes!("sentrux_gate.rs")) + }), + } +} + +fn capability_failure(observation: &Value, status: &str) -> Value { + if status == "succeeded" { + return Value::Null; + } + let raw_kind = observation["failure"]["kind"].as_str().unwrap_or("unknown"); + let kind = match raw_kind { + "degraded" => "degraded", + "explicit_mutation_required" + | "dag_scope_not_supported" + | "session_lifecycle_outside_dag" + | "not_applicable" => "not_applicable", + "provider_unavailable" | "capability_unavailable" => "provider_unavailable", + "contract_error" | "invalid_options" => "config_error", + "io_error" => "local_tool_error", + _ => "provider_error", + }; + let message = observation["failure"]["message"] + .as_str() + .or_else(|| observation["failure"]["kind"].as_str()) + .unwrap_or("Sentrux capability did not complete") + .to_string(); + json!({ + "kind":kind, + "message":message, + "retryable":kind == "provider_unavailable" || kind == "local_tool_error" + }) +} + +fn sentrux_decision_consumers(capability_id: &str) -> Value { + match capability_id { + "sentrux.gate" | "sentrux.check" => { + json!(["diagnosis.hospital", "pr_gate", "release_gate"]) + } + "sentrux.baseline_save" => json!(["sentrux.gate"]), + "sentrux.dsm" | "sentrux.test_gaps" => json!([ + "evidence.sentrux", + "diagnosis.hospital", + "report", + "change_impact", + "test_selection", + "pr_gate", + "release_gate" + ]), + "sentrux.what_if" => json!(["change_impact", "pr_gate", "release_gate"]), + "sentrux.session_start" => json!(["sentrux.rescan", "sentrux.session_end"]), + "sentrux.session_end" => json!(["pr_gate", "release_gate"]), + "sentrux.provider_discovery" => { + json!(["doctor", "run_planner", "install_smoke", "release_gate"]) + } + _ => json!([ + "evidence.sentrux", + "diagnosis.hospital", + "report", + "release_gate" + ]), + } +} + +fn capability_observation( + route: &CapabilityRoute, + provider_mode: &str, + command: &SentruxCommand, + authority: Option<&str>, +) -> Value { + let (status, verdict, failure) = if command.success && !command.output_summary.complete() { + ( + "degraded", + "unknown", + json!({ + "kind":"degraded", + "message":format!( + "Sentrux {} output exceeded the bounded evidence limit; only metadata and preview were retained", + route.operation + ) + }), + ) + } else if command.success { + ("succeeded", "pass", json!({"kind":"none"})) + } else if !command.governed { + ("succeeded", "unknown", json!({"kind":"none"})) + } else { + ( + "failed", + "fail", + json!({ + "kind":"command_failed", + "message":command_failure_message(command) + }), + ) + }; + json!({ + "capabilityId":route.capability_id, + "operation":route.operation, + "providerMode":provider_mode, + "authority":authority.unwrap_or("compatibility"), + "status":status, + "verdict":verdict, + "command":capability_command_evidence(route.operation, command), + "outputSummary":command.output_summary.to_json(&command.stdout, &command.stderr), + "failure":failure + }) +} + +fn capability_command_evidence(operation: &str, command: &SentruxCommand) -> Value { + let mut evidence = command_evidence(operation, command); + evidence["governed"] = json!(command.governed); + evidence["violations"] = command.violations_json(); + evidence +} + +fn not_applicable_observation( + route: &CapabilityRoute, + provider_mode: &str, + failure_kind: &str, + message: &str, +) -> Value { + json!({ + "capabilityId":route.capability_id, + "operation":route.operation, + "providerMode":provider_mode, + "authority":"declared_only", + "status":"not_applicable", + "verdict":"unknown", + "command":Value::Null, + "failure":{"kind":failure_kind,"message":message} + }) +} + +fn unavailable_observation(route: &CapabilityRoute, provider_mode: &str, message: &str) -> Value { + json!({ + "capabilityId":route.capability_id, + "operation":route.operation, + "providerMode":provider_mode, + "authority":"compatibility", + "status":"unavailable", + "verdict":"unknown", + "command":Value::Null, + "failure":{"kind":"capability_unavailable","message":message} + }) +} + +fn route_error_observation( + route: &CapabilityRoute, + provider_mode: &str, + error: &AdapterError, +) -> Value { + let unavailable = matches!(error, AdapterError::Unavailable(_)); + json!({ + "capabilityId":route.capability_id, + "operation":route.operation, + "providerMode":provider_mode, + "authority":if unavailable { "compatibility" } else { "authoritative" }, + "status":if unavailable { "unavailable" } else { "failed" }, + "verdict":"unknown", + "command":Value::Null, + "failure":{ + "kind":if unavailable { "provider_unavailable" } else { adapter_error_kind(error) }, + "message":format!("{error:?}") + } + }) +} + +fn artifact_authority(observation: &Value, status: &str) -> &'static str { + match observation["authority"].as_str() { + Some("authoritative") => "authoritative", + Some("fallback") => "fallback", + Some("compatibility") => "compatibility", + Some("declared_only") => "declared_only", + _ => match status { + "succeeded" | "failed" => "authoritative", + "not_applicable" => "declared_only", + _ => "compatibility", + }, + } +} + +fn observation_command(observation: &Value) -> Option { + let command = observation["command"].as_object()?; + let stdout = command["stdout"].as_str().unwrap_or_default().to_owned(); + let stderr = command["stderr"].as_str().unwrap_or_default().to_owned(); + let output_summary = command + .get("outputSummary") + .unwrap_or(&Value::Null) + .as_object() + .and_then(|summary| { + Some(super::sentrux_command::OutputSummary::from_metadata( + summary, + )) + }) + .unwrap_or_else(|| { + super::sentrux_command::OutputSummary::from_bytes(stdout.as_bytes(), stderr.as_bytes()) + }); + Some(SentruxCommand { + argv: command["argv"] + .as_array()? + .iter() + .filter_map(Value::as_str) + .map(str::to_owned) + .collect(), + exit_code: command["exitCode"].as_i64().map(|value| value as i32), + success: command["success"].as_bool().unwrap_or(false), + stdout, + stderr, + violations: SentruxCommand::violations_from_json(command.get("violations")), + // Capability artifacts intentionally retain an ungoverned gate as a + // successful, unknown observation. Rehydrate that distinction here so + // the capability evidence cannot turn an absent baseline into a + // structural failure when it is fed back into the authoritative rules. + governed: command + .get("governed") + .and_then(Value::as_bool) + .unwrap_or_else(|| { + !(command["success"] == false + && observation["status"] == "succeeded" + && observation["verdict"] == "unknown") + }), + output_summary, + }) +} + +fn route_provider_mode(route: &CapabilityRoute, tool_path_prefix: Option<&Path>) -> &'static str { + if matches!(route.kind, RouteKind::Command) && uses_lite_fallback(route.command) { + "lite_fallback" + } else if tool_path_prefix.is_some() { + "external" + } else { + "builtin" + } +} + +fn uses_lite_fallback(command: &str) -> bool { + matches!( + command, + "git_stats" | "evolution" | "test_gaps" | "what_if" | "provider_discovery" + ) +} + +fn adapter_error_kind(error: &AdapterError) -> &'static str { + match error { + AdapterError::Unavailable(_) => "provider_unavailable", + AdapterError::Contract(_) => "contract_error", + AdapterError::InvalidOptions(_) => "invalid_options", + AdapterError::Internal(_) => "provider_error", + AdapterError::Io(_) => "io_error", + } +} + +fn command_failure_message(command: &SentruxCommand) -> String { + command + .stderr + .lines() + .chain(command.stdout.lines()) + .find(|line| !line.trim().is_empty()) + .map(str::trim) + .unwrap_or("Sentrux command reported a failing verdict") + .chars() + .take(1024) + .collect() +} diff --git a/crates/code-intel-cli/src/sentrux_command.rs b/crates/code-intel-cli/src/sentrux_command.rs new file mode 100644 index 00000000..2fde7a41 --- /dev/null +++ b/crates/code-intel-cli/src/sentrux_command.rs @@ -0,0 +1,204 @@ +use std::process::Output; + +use serde_json::{json, Value}; + +use super::sentrux_gate::Violation; +use crate::capability::sha256_hex; + +pub(crate) const MAX_COMMAND_EVIDENCE_BYTES: usize = 1024 * 1024; +const MAX_COMMAND_PREVIEW_BYTES: usize = 8 * 1024; + +pub(crate) struct SentruxCommand { + pub(crate) argv: Vec, + pub(crate) exit_code: Option, + pub(crate) success: bool, + pub(crate) stdout: String, + pub(crate) stderr: String, + pub(crate) violations: Vec, + pub(crate) governed: bool, + pub(crate) output_summary: OutputSummary, +} + +#[derive(Clone, Debug)] +pub(crate) struct OutputSummary { + stdout_bytes: usize, + stdout_sha256: String, + stderr_bytes: usize, + stderr_sha256: String, +} + +impl OutputSummary { + pub(crate) fn from_bytes(stdout: &[u8], stderr: &[u8]) -> Self { + Self { + stdout_bytes: stdout.len(), + stdout_sha256: sha256_hex(stdout), + stderr_bytes: stderr.len(), + stderr_sha256: sha256_hex(stderr), + } + } + + pub(crate) fn complete(&self) -> bool { + self.stdout_bytes <= MAX_COMMAND_EVIDENCE_BYTES + && self.stderr_bytes <= MAX_COMMAND_EVIDENCE_BYTES + } + + pub(crate) fn to_json(&self, stdout_preview: &str, stderr_preview: &str) -> Value { + json!({ + "authority":"metadata_only", + "complete":self.complete(), + "bounded":!self.complete(), + "limitBytes":MAX_COMMAND_EVIDENCE_BYTES, + "totalBytes":self.stdout_bytes + self.stderr_bytes, + "stdout":{ + "bytes":self.stdout_bytes, + "sha256":self.stdout_sha256, + "preview":stdout_preview, + "previewBytes":stdout_preview.len() + }, + "stderr":{ + "bytes":self.stderr_bytes, + "sha256":self.stderr_sha256, + "preview":stderr_preview, + "previewBytes":stderr_preview.len() + }, + "note":"preview is non-authoritative; consumers must use the artifact metadata" + }) + } + + pub(crate) fn from_metadata(summary: &serde_json::Map) -> Self { + fn digest(summary: &serde_json::Map, stream: &str) -> String { + summary[stream]["sha256"] + .as_str() + .unwrap_or_default() + .to_owned() + } + fn bytes(summary: &serde_json::Map, stream: &str) -> usize { + summary[stream]["bytes"].as_u64().unwrap_or(0) as usize + } + Self { + stdout_bytes: bytes(summary, "stdout"), + stdout_sha256: digest(summary, "stdout"), + stderr_bytes: bytes(summary, "stderr"), + stderr_sha256: digest(summary, "stderr"), + } + } +} + +fn bounded_text(bytes: &[u8]) -> String { + let end = bytes.len().min(MAX_COMMAND_PREVIEW_BYTES); + String::from_utf8_lossy(&bytes[..end]).into_owned() +} + +impl SentruxCommand { + pub(crate) fn violations_json(&self) -> Value { + json!(self + .violations + .iter() + .map(Violation::to_json) + .collect::>()) + } + + pub(crate) fn violations_from_json(value: Option<&Value>) -> Vec { + value + .and_then(Value::as_array) + .map(|items| { + items + .iter() + .filter_map(|item| { + Some(Violation { + rule: item["rule"].as_str()?.to_owned(), + message: item["message"].as_str()?.to_owned(), + targets: item["targets"] + .as_array()? + .iter() + .filter_map(Value::as_str) + .map(str::to_owned) + .collect(), + }) + }) + .collect() + }) + .unwrap_or_default() + } + + pub(crate) fn from_native(run: super::sentrux_gate::EngineRun, subcommand: &str) -> Self { + let stdout_bytes = run.stdout.into_bytes(); + let output_summary = OutputSummary::from_bytes(&stdout_bytes, &[]); + Self { + argv: vec![ + "code-intel".into(), + "sentrux".into(), + subcommand.into(), + ".".into(), + ], + exit_code: Some(if run.success { 0 } else { 1 }), + success: run.success, + stdout: bounded_text(&stdout_bytes), + stderr: String::new(), + violations: run.violations, + governed: run.governed, + output_summary, + } + } + + pub(crate) fn from_external(output: Output, subcommand: &str) -> Self { + let output_summary = OutputSummary::from_bytes(&output.stdout, &output.stderr); + let stdout_full = String::from_utf8_lossy(&output.stdout).into_owned(); + let violations = if output.status.success() { + Vec::new() + } else { + stdout_full + .lines() + .filter_map(|line| line.strip_prefix("- ")) + .map(str::trim) + .filter(|message| !message.is_empty()) + .take(32) + .map(|message| Violation { + rule: format!("sentrux_{subcommand}"), + message: message.chars().take(1024).collect(), + targets: Vec::new(), + }) + .collect() + }; + Self { + argv: vec!["sentrux".into(), subcommand.into(), ".".into()], + exit_code: output.status.code(), + success: output.status.success(), + stdout: bounded_text(&output.stdout), + stderr: bounded_text(&output.stderr), + violations, + governed: true, + output_summary, + } + } + + pub(crate) fn from_json(stdout: Vec, subcommand: &str) -> Self { + let output_summary = OutputSummary::from_bytes(&stdout, &[]); + Self { + argv: vec![ + "code-intel".into(), + "sentrux".into(), + subcommand.into(), + ".".into(), + ], + exit_code: Some(0), + success: true, + stdout: bounded_text(&stdout), + stderr: String::new(), + violations: Vec::new(), + governed: true, + output_summary, + } + } +} + +pub(crate) fn command_evidence(subcommand: &str, command: &SentruxCommand) -> Value { + json!({ + "id":subcommand, + "argv":command.argv, + "exitCode":command.exit_code, + "success":command.success, + "stdout":command.stdout, + "stderr":command.stderr + }) +} diff --git a/crates/code-intel-cli/src/sentrux_lite_capabilities.rs b/crates/code-intel-cli/src/sentrux_lite_capabilities.rs new file mode 100644 index 00000000..66f8a531 --- /dev/null +++ b/crates/code-intel-cli/src/sentrux_lite_capabilities.rs @@ -0,0 +1,385 @@ +use std::fs; +use std::path::Path; +use std::process::Command; + +use serde_json::{json, Value}; + +pub(super) fn provider_discovery_json() -> Result { + Ok(json!({ + "provider":"sentrux", + "mode":"builtin_lite", + "available":true, + "operations":["scan","health","dsm","git_stats","evolution","test_gaps","what_if","check_rules","check","gate","rescan"], + "aliases":["pro_status","plugin_list","plugin_validate"], + "explicitAuthorityOperations":["gate_save"], + "lifecycleOperations":["session_start","session_end"], + "legacyFallback":"legacy/Invoke-SentruxAgentTool.ps1" + })) +} + +fn git_command(repo: &Path, args: &[&str]) -> Result { + let output = Command::new("git") + .args(args) + .current_dir(repo) + .output() + .map_err(|error| format!("start git {}: {error}", args.join(" ")))?; + if !output.status.success() { + return Err(format!( + "git {} failed: {}", + args.join(" "), + String::from_utf8_lossy(&output.stderr).trim() + )); + } + String::from_utf8(output.stdout).map_err(|error| format!("git output is not UTF-8: {error}")) +} + +pub(super) fn git_stats_json(repo: &Path) -> Result { + let count_output = match git_command(repo, &["rev-list", "--count", "HEAD"]) { + Ok(output) => output, + Err(reason) => { + return Ok(json!({ + "commitCount":0, + "recentCommits":[], + "status":"unavailable", + "reason":reason + })) + } + }; + let count = count_output + .trim() + .parse::() + .map_err(|error| format!("git commit count is invalid: {error}"))?; + let recent_output = match git_command(repo, &["log", "-n", "20", "--format=%H%x09%aI"]) { + Ok(output) => output, + Err(reason) => { + return Ok(json!({ + "commitCount":count, + "recentCommits":[], + "status":"unavailable", + "reason":reason + })) + } + }; + let recent = recent_output + .lines() + .filter_map(|line| { + let (commit, authored_at) = line.split_once('\t')?; + Some(json!({"commit":commit,"authoredAt":authored_at})) + }) + .collect::>(); + Ok(json!({"commitCount":count,"recentCommits":recent,"status":"ok"})) +} + +pub(super) fn evolution_json(repo: &Path) -> Result { + let recent_output = match git_command(repo, &["log", "-n", "20", "--format=%H%x09%aI%x09%an"]) { + Ok(output) => output, + Err(reason) => { + return Ok(json!({ + "status":"unavailable", + "windowCommits":0, + "trend":"unknown", + "recentCommits":[], + "reason":reason + })) + } + }; + let recent = recent_output + .lines() + .filter_map(|line| { + let mut fields = line.splitn(3, '\t'); + Some(json!({ + "commit":fields.next()?, + "authoredAt":fields.next()?, + "author":fields.next()? + })) + }) + .collect::>(); + Ok(json!({ + "status":"ok", + "windowCommits":recent.len(), + "trend":"observed", + "recentCommits":recent + })) +} + +#[cfg(test)] +mod tests { + #[test] + fn history_capabilities_are_auditable_without_git_history() { + let repo = std::path::Path::new("this-path-does-not-contain-a-git-checkout"); + let stats = + super::git_stats_json(repo).expect("missing git history is a valid lite result"); + let evolution = + super::evolution_json(repo).expect("missing git history is a valid lite result"); + + assert_eq!(stats["status"], "unavailable"); + assert_eq!(stats["commitCount"], 0); + assert_eq!(evolution["status"], "unavailable"); + assert_eq!(evolution["windowCommits"], 0); + assert_eq!(evolution["trend"], "unknown"); + } + + #[test] + fn what_if_is_a_bounded_snapshot_capability() { + let root = std::env::temp_dir().join(format!( + "code-intel-lite-what-if-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock") + .as_nanos() + )); + std::fs::create_dir_all(root.join(".sentrux")).expect("rules directory"); + std::fs::create_dir_all(root.join("src")).expect("source directory"); + std::fs::write( + root.join(".sentrux/rules.toml"), + "max_cc = 1\nmax_coupling = 1\n", + ) + .expect("rules"); + std::fs::write( + root.join("src/lib.rs"), + "pub fn sample() { if true { println!(\"x\"); } }\n", + ) + .expect("source"); + + let value = super::what_if_json(&root).expect("what_if should be available"); + assert_eq!(value["status"], "ok"); + assert_eq!(value["summary"]["scenarioCount"], 4); + assert!(value["summary"]["failingScenarioCount"].as_u64().unwrap() > 0); + assert_eq!(value["scenarios"].as_array().unwrap().len(), 4); + assert!(value["limitations"].as_array().unwrap().len() >= 2); + let _ = std::fs::remove_dir_all(root); + } +} + +pub(super) fn test_gaps_json(repo: &Path) -> Result { + let mut source_files = 0_u64; + let mut test_files = 0_u64; + let mut stack = vec![repo.to_path_buf()]; + while let Some(directory) = stack.pop() { + for entry in fs::read_dir(&directory).map_err(|error| { + format!( + "read test inventory directory {}: {error}", + directory.display() + ) + })? { + let entry = entry.map_err(|error| format!("read test inventory entry: {error}"))?; + let path = entry.path(); + let name = entry.file_name().to_string_lossy().to_ascii_lowercase(); + if entry + .file_type() + .map_err(|error| format!("inspect test inventory entry: {error}"))? + .is_dir() + { + if !matches!(name.as_str(), ".git" | "target" | "node_modules") { + stack.push(path); + } + continue; + } + let extension = path + .extension() + .and_then(|value| value.to_str()) + .unwrap_or(""); + if !matches!( + extension, + "rs" | "py" | "js" | "ts" | "tsx" | "go" | "java" | "cs" + ) { + continue; + } + let relative = path + .strip_prefix(repo) + .unwrap_or(&path) + .to_string_lossy() + .to_ascii_lowercase(); + if relative.contains("test") || relative.contains("spec") { + test_files += 1; + } else { + source_files += 1; + } + } + } + Ok(json!({ + "status":"heuristic", + "sourceFiles":source_files, + "testFiles":test_files, + "gapStatus":if test_files == 0 { "unknown" } else { "inventory_only" }, + "limitations":["This lite fallback inventories test files; it does not prove symbol-level test coverage."] + })) +} + +pub(super) fn what_if_json(repo: &Path) -> Result { + let dsm = super::sentrux_analysis::analyze(repo)?; + let max_cc = read_rule_number(repo, "max_cc").unwrap_or(25.0); + let max_coupling = read_rule_number(repo, "max_coupling").unwrap_or(76.0); + let blast_limit = max_coupling + 2.0; + + let complexity = dsm["file_details"] + .as_array() + .into_iter() + .flatten() + .flat_map(|file| { + file["functions"] + .as_array() + .into_iter() + .flatten() + .filter_map(|function| { + let value = function["complexity"].as_f64()?; + (value > max_cc).then(|| { + json!({ + "id":function["id"], + "name":function["name"], + "file":file["path"], + "sourceAnchor":function["source_anchor"], + "value":value, + "limit":max_cc, + "overBy":value - max_cc + }) + }) + }) + }) + .collect::>(); + let coupling = dsm["modules"] + .as_array() + .into_iter() + .flatten() + .filter_map(|module| { + let value = module["metrics"]["coupling"].as_f64()?; + (value > max_coupling).then(|| { + json!({ + "id":module["id"], + "name":module["name"], + "metric":"coupling", + "value":value, + "limit":max_coupling, + "risk":module["metrics"]["risk"] + }) + }) + }) + .collect::>(); + let blast_radius = dsm["modules"] + .as_array() + .into_iter() + .flatten() + .filter_map(|module| { + let value = module["metrics"]["blast_radius"].as_f64()?; + (value > blast_limit).then(|| { + json!({ + "id":module["id"], + "name":module["name"], + "metric":"blast_radius", + "value":value, + "limit":blast_limit, + "risk":module["metrics"]["risk"] + }) + }) + }) + .collect::>(); + let test_gaps = dsm["modules"] + .as_array() + .into_iter() + .flatten() + .filter_map(|module| { + let value = module["metrics"]["test_gap"].as_f64()?; + (value > 0.0).then(|| { + json!({ + "id":module["id"], + "name":module["name"], + "metric":"test_gap", + "value":value, + "limit":0, + "risk":module["metrics"]["risk"] + }) + }) + }) + .collect::>(); + + let scenarios = vec![ + what_if_scenario( + "current_max_cc_gate", + "max_cc", + max_cc, + complexity, + "Split or simplify functions above the current Sentrux complexity ceiling.", + ), + what_if_scenario( + "module_coupling_cap", + "max_coupling", + max_coupling, + coupling, + "Inspect dependency edges and preserve provider boundaries before adding coupling.", + ), + what_if_scenario( + "blast_radius_cap", + "max_blast_radius", + blast_limit, + blast_radius, + "Reduce fan-out or split the highest-impact module before expanding its surface.", + ), + what_if_scenario( + "test_gap_gate", + "test_gap", + 0.0, + test_gaps, + "Add or select tests for source-heavy modules before treating the change as fully covered.", + ), + ]; + let failing = scenarios + .iter() + .filter(|scenario| scenario["pass"] == false) + .count(); + let primary_risk = scenarios + .iter() + .find(|scenario| scenario["pass"] == false) + .and_then(|scenario| scenario["id"].as_str()) + .unwrap_or("none"); + Ok(json!({ + "status":"ok", + "scope":"repository_snapshot", + "rules":{ + "max_cc":max_cc, + "max_coupling":max_coupling, + "max_blast_radius":blast_limit, + "source":if repo.join(".sentrux/rules.toml").is_file() { "repository" } else { "defaults" } + }, + "scenarios":scenarios, + "summary":{ + "scenarioCount":4, + "failingScenarioCount":failing, + "primaryRisk":primary_risk + }, + "limitations":[ + "Lite what_if evaluates the current snapshot; it does not mutate or synthesize a hypothetical checkout.", + "Function and dependency extraction are heuristic and remain bounded by the lite DSM parser." + ] + })) +} + +fn read_rule_number(repo: &Path, name: &str) -> Option { + let text = fs::read_to_string(repo.join(".sentrux/rules.toml")).ok()?; + text.lines() + .find_map(|line| { + let (key, value) = line.split_once('=')?; + (key.trim() == name).then(|| value.trim().trim_matches('"').parse().ok()) + }) + .flatten() +} + +fn what_if_scenario( + id: &str, + metric: &str, + limit: f64, + affected: Vec, + action: &str, +) -> Value { + let pass = affected.is_empty(); + json!({ + "id":id, + "metric":metric, + "pass":pass, + "severity":if pass { "ok" } else { "high" }, + "impactCount":affected.len(), + "affected":affected.into_iter().take(20).collect::>(), + "limit":limit, + "action":action + }) +} diff --git a/crates/code-intel-cli/tests/capability_exec.rs b/crates/code-intel-cli/tests/capability_exec.rs index 6f53e8e4..036cf132 100644 --- a/crates/code-intel-cli/tests/capability_exec.rs +++ b/crates/code-intel-cli/tests/capability_exec.rs @@ -14,7 +14,7 @@ const STRUCTURED_EDIT_DIGEST: &str = const REPO_SNAPSHOT_DIGEST: &str = "4f42b080fd19e501a6315ee204add188d69625bedd15c566fea48bb1f3e78764"; const CODENEXUS_TOOLCHAIN_DIGESTS: [&str; 5] = [ - "004ad153b019ac9ae9869c095d90dce8b7a49bc5cdf745ce68f16dcd9ad795e0", + "1ee8976cc50b9a9f71bce61abe1be32fd406527d702040f7f223db48899b4150", "645675312135932dfce365a8dfc14e214cec78ee733f248606547b3eaa56edc8", "52644a812174988ede91d98ddfec63c6a91f8478277d7bf74c73f106dd0f776b", "98ccc64478b2c61bfd7af741ea1f8ee01a88094065c0f025700e8110b525ef26", diff --git a/crates/code-intel-cli/tests/dag_run.rs b/crates/code-intel-cli/tests/dag_run.rs index 368d0176..1f8bdb19 100644 --- a/crates/code-intel-cli/tests/dag_run.rs +++ b/crates/code-intel-cli/tests/dag_run.rs @@ -341,6 +341,51 @@ fn production_dag_output_commits_and_enters_the_authoritative_index() { assert_eq!(impact["schema"], "code-intel-change-impact.v1"); assert_eq!(impact["runOutcome"], "completed"); assert_eq!(impact["freshness"]["status"], "current"); + assert_eq!(impact["sentruxEvidence"]["status"], "available"); + assert!(!impact["sentruxEvidenceRefs"].as_array().unwrap().is_empty()); + assert!(impact["sentruxEvidenceRefs"] + .as_array() + .unwrap() + .iter() + .all(|reference| { + reference["artifactSchema"] == "code-intel-sentrux-capability-artifact.v1" + && reference["type"] == "provider.sentrux.capability-artifact" + })); + assert_eq!( + impact["testSelection"]["sentruxSignals"]["status"], + "available" + ); + assert_eq!( + impact["testSelection"]["sentruxSignals"]["testGap"]["status"], + "available" + ); + assert_eq!( + impact["testSelection"]["sentruxSignals"]["testGap"]["capabilityStatus"], + "succeeded" + ); + assert_eq!( + impact["testSelection"]["sentruxSignals"]["dsm"]["status"], + "available" + ); + assert_eq!( + impact["testSelection"]["sentruxSignals"]["candidateTestImpact"], + "retains_graph_candidates_with_what_if_risk" + ); + assert!( + impact["testSelection"]["sentruxSignals"]["whatIfFailingScenarioCount"] + .as_u64() + .unwrap_or(0) + > 0 + ); + assert!( + impact["testSelection"]["sentruxSignals"]["testGap"]["limitations"] + .as_array() + .unwrap() + .iter() + .any(|item| item + .as_str() + .is_some_and(|text| text.contains("no structured candidate test paths"))) + ); assert_eq!( impact["testSelection"]["files"], json!(["tests/lib_test.rs"]) diff --git a/crates/code-intel-cli/tests/fixtures/cli-head-parity.v2.json b/crates/code-intel-cli/tests/fixtures/cli-head-parity.v2.json index 0c0fe555..4caf989d 100644 --- a/crates/code-intel-cli/tests/fixtures/cli-head-parity.v2.json +++ b/crates/code-intel-cli/tests/fixtures/cli-head-parity.v2.json @@ -587,7 +587,7 @@ }, "new": { "exitCode": 0, - "stdoutUtf8": "code-intel [options]\n\nCommands:\n --version|-V [--json]\n help|--help|-h [--all]\n run [] [--mode lite|normal|full] [--json]\n status [] [--json]\n query [] --kind evidence [--artifact-schema ] [--type ] [--contains ] [--limit <1..100>] --json\n report --repo [--artifact-root ] [--json]\n resume --repo [--artifact-root ] [--json]\n classify --report [--json]\n sentrux-normalize --steps [--out ]\n sentrux-debt-register --failures [--repo ] [--out ]\n doctor [--artifact-root ] [--json]\n doctor bootstrap [--repo ] [--repo-path ] [--config ] [--platform auto|windows|macos|linux] [--no-require-repowise] [--require-understand] [--json]\n graph|understand --repo [--language zh] [--full] [--write] [--json]\n provider|providers [--action List|Plan|Validate|Invoke] [--provider repowise|understand] [--operation ] [--repo ] [--language zh] [--write] [--json]\n provider repowise-adapt --request --artifact-root --evaluated-at --max-age-seconds \n provider graph-adapt --request --artifact-root --evaluated-at --max-age-seconds \n provider sentrux-adapt --request --artifact-root --evaluated-at --max-age-seconds \n provider session-adapt --repo --trace [--hotspots ] [--out ] [--working-tree-policy head_only|explicit_overlay]\n provider codenexus-adapt --request --artifact-root --evaluated-at --max-age-seconds \n provider file-boundary --request --out \n provider runtime-ci-evidence --artifact-root --request --out \n compatibility retirement-ticket lint --ticket --evaluated-at \n lint hardcoded-paths [] [--json]\n route|routes [--action List|Plan|Validate] [--provider repowise|understand] [--operation ] [--repo ] [--json]\n sentrux [--no-ratchet]\n (--no-ratchet: `check` only, skip the .sentrux/baseline.json ratchet)\n capability exec --request --out [--artifact-root ] [--manifest ]\n model inventory-validate --request [--out ]\n model route --request [--out ]\n snapshot identity --repo --working-tree-policy [--scope ]...\n repin [--repo ] [--write] [--json] [--exclude ]...\n repowise-hooks [--repo ] [--write] (detects/installs the optional repowise post-commit and distill-rewrite hooks; no-op if repowise is not on PATH)\n evidence validate --request --artifact-root \n repository survival-scan --request --artifact-root \n audit --operation validate|render --repo --report [--format markdown|html]\n audit --operation scope --repo --since \n artifact index --artifact-root [--output ] [--operation rebuild|incremental] [--existing ]\n artifact query --artifact-root --repo [--repo-path ] [--artifact-schema ] [--type ] [--contains ] [--limit <1..100>]\n change impact --artifact-root --repo --repo-path --changed [--changed ]... [--staleness current|advisory]\n change risk [--repo ] [--sample ] [--format json|text] (git-only defect-risk score, no prior run, no index)\n change agenda [--repo ] [--min-cochange ] [--format json|text] (git-only review units clustered by co-change, ranked worst first)\n edit impact --repo-path --changed [--changed ]... [--scope ]... (working tree, no prior run, authority: none)\n edit apply --repo-path --file (--span --expect-sha256 --replacement |--replacement-file )... [--out ] [--manifest ] [--envelope] (span-addressed patch; refuses with evidence on digest drift, exit 10)\n decision request-response --request [--response |--cancel ] --now --branch ...\n decision record --resolution --store \n decision replay --query --store \n run execute --repo --out --authority-root --final-name [--profile default|strict|offline] [--manifest ] [--max-concurrency ] [--session-evidence ]\n run dag-coordinate --repo --out [--manifest ] [--max-concurrency ] [--session-evidence ]\n run commit --source-root --authority-root --manifest-ref --final-name \n serve --mcp [--repo-path ] [--repo ] [--artifact-root ] [--manifest ] (stdio MCP query surface over the committed run; read-only, gates nowhere)\n benchmark orientation --out [--repetitions <2..10>]\n benchmark tools --corpus --runs --artifact-root --out \n governance ponytail-gate --request \n orchestrate|orchestration [--action Validate|List|Plan] [--repo ] [--mode lite|normal|full] [--capability ] [--manifest ] [--json]\n language set --language --repo [--json]\n", + "stdoutUtf8": "code-intel [options]\n\nCommands:\n --version|-V [--json]\n help|--help|-h [--all]\n run [] [--mode lite|normal|full] [--json]\n status [] [--json]\n query [] --kind evidence [--artifact-schema ] [--type ] [--contains ] [--limit <1..100>] --json\n report --repo [--artifact-root ] [--json]\n resume --repo [--artifact-root ] [--json]\n classify --report [--json]\n sentrux-normalize --steps [--out ]\n sentrux-debt-register --failures [--repo ] [--out ]\n doctor [--artifact-root ] [--json]\n doctor bootstrap [--repo ] [--repo-path ] [--config ] [--platform auto|windows|macos|linux] [--no-require-repowise] [--require-understand] [--json]\n graph|understand --repo [--language zh] [--full] [--write] [--json]\n provider|providers [--action List|Plan|Validate|Invoke] [--provider repowise|understand] [--operation ] [--repo ] [--language zh] [--write] [--json]\n provider repowise-adapt --request --artifact-root --evaluated-at --max-age-seconds \n provider graph-adapt --request --artifact-root --evaluated-at --max-age-seconds \n provider sentrux-adapt --request --artifact-root --evaluated-at --max-age-seconds \n provider session-adapt --repo --trace [--hotspots ] [--out ] [--working-tree-policy head_only|explicit_overlay]\n provider codenexus-adapt --request --artifact-root --evaluated-at --max-age-seconds \n provider file-boundary --request --out \n provider runtime-ci-evidence --artifact-root --request --out \n compatibility retirement-ticket lint --ticket --evaluated-at \n lint hardcoded-paths [] [--json]\n route|routes [--action List|Plan|Validate] [--provider repowise|understand] [--operation ] [--repo ] [--json]\n sentrux [--no-ratchet]\n sentrux capabilities [] [--json] (read-only capability matrix audit)\n (--no-ratchet: `check` only, skip the .sentrux/baseline.json ratchet)\n capability exec --request --out [--artifact-root ] [--manifest ]\n model inventory-validate --request [--out ]\n model route --request [--out ]\n snapshot identity --repo --working-tree-policy [--scope ]...\n repin [--repo ] [--write] [--json] [--exclude ]...\n repowise-hooks [--repo ] [--write] (detects/installs the optional repowise post-commit and distill-rewrite hooks; no-op if repowise is not on PATH)\n evidence validate --request --artifact-root \n repository survival-scan --request --artifact-root \n audit --operation validate|render --repo --report [--format markdown|html]\n audit --operation scope --repo --since \n artifact index --artifact-root [--output ] [--operation rebuild|incremental] [--existing ]\n artifact query --artifact-root --repo [--repo-path ] [--artifact-schema ] [--type ] [--contains ] [--limit <1..100>]\n change impact --artifact-root --repo --repo-path --changed [--changed ]... [--staleness current|advisory]\n change risk [--repo ] [--sample ] [--format json|text] (git-only defect-risk score, no prior run, no index)\n change agenda [--repo ] [--min-cochange ] [--format json|text] (git-only review units clustered by co-change, ranked worst first)\n edit impact --repo-path --changed [--changed ]... [--scope ]... (working tree, no prior run, authority: none)\n edit apply --repo-path --file (--span --expect-sha256 --replacement |--replacement-file )... [--out ] [--manifest ] [--envelope] (span-addressed patch; refuses with evidence on digest drift, exit 10)\n decision request-response --request [--response |--cancel ] --now --branch ...\n decision record --resolution --store \n decision replay --query --store \n run execute --repo --out --authority-root --final-name [--profile default|strict|offline] [--manifest ] [--max-concurrency ] [--session-evidence ]\n run dag-coordinate --repo --out [--manifest ] [--max-concurrency ] [--session-evidence ]\n run commit --source-root --authority-root --manifest-ref --final-name \n serve --mcp [--repo-path ] [--repo ] [--artifact-root ] [--manifest ] (stdio MCP query surface over the committed run; read-only, gates nowhere)\n benchmark orientation --out [--repetitions <2..10>]\n benchmark tools --corpus --runs --artifact-root --out \n governance ponytail-gate --request \n orchestrate|orchestration [--action Validate|List|Plan] [--repo ] [--mode lite|normal|full] [--capability ] [--manifest ] [--json]\n language set --language --repo [--json]\n", "stderrUtf8": "" } }, diff --git a/orchestration/integrations.json b/orchestration/integrations.json index 6f63caf9..2b1119fa 100644 --- a/orchestration/integrations.json +++ b/orchestration/integrations.json @@ -535,7 +535,7 @@ "id": "provider.graph-builtin.compat", "version": "1.0.0", "toolchainDigests": [ - "004ad153b019ac9ae9869c095d90dce8b7a49bc5cdf745ce68f16dcd9ad795e0", + "1ee8976cc50b9a9f71bce61abe1be32fd406527d702040f7f223db48899b4150", "245bd6d7eb6f774ea6f9cead43eb67f8b351fc92c930252d180d072f00524947", "68a4f55b76080575721cde3ee62fd746999c6df8e79303b809baecf5d2514338", "52644a812174988ede91d98ddfec63c6a91f8478277d7bf74c73f106dd0f776b", @@ -587,7 +587,7 @@ "id": "provider.sentrux-builtin.compat", "version": "1.0.0", "toolchainDigests": [ - "004ad153b019ac9ae9869c095d90dce8b7a49bc5cdf745ce68f16dcd9ad795e0", + "1ee8976cc50b9a9f71bce61abe1be32fd406527d702040f7f223db48899b4150", "29ad124d984f6ab0756bdc7c0b523a84dfada2a655fe0d282ce681165f6bae4f", "f69776e6800f917099794a5a28dbdb1117eaf657568dd09e34bf33e4207ddb4c", "52644a812174988ede91d98ddfec63c6a91f8478277d7bf74c73f106dd0f776b", @@ -662,7 +662,7 @@ "id": "provider.codenexus-builtin.compat", "version": "1.0.0", "toolchainDigests": [ - "004ad153b019ac9ae9869c095d90dce8b7a49bc5cdf745ce68f16dcd9ad795e0", + "1ee8976cc50b9a9f71bce61abe1be32fd406527d702040f7f223db48899b4150", "645675312135932dfce365a8dfc14e214cec78ee733f248606547b3eaa56edc8", "52644a812174988ede91d98ddfec63c6a91f8478277d7bf74c73f106dd0f776b", "98ccc64478b2c61bfd7af741ea1f8ee01a88094065c0f025700e8110b525ef26", @@ -878,7 +878,7 @@ "toolchainDigests": [ "3ba256f4ca0bf62aca08f688f7ecf31800dd10e68371054e2f1605eff197ea81", "295eb1ce67760638a81136febf727285f0feb4692a228df65ac75316b4a566c5", - "255483f15ef2cdfe0b759d84ef64bc20ec5236716ea6668642c6806e834ab437", + "d51f1db572c1ed8a52f2b9575572d76ad4880d25b3d942d7801cc29d8a2b087a", "b118ba78ff3ef4a189525c642184cbd320a268d2885681ab153544db554c5965", "e3072b6b01a4f692c2ac75c7c4995747f9a0fd1ce4f32e1ab1599f348661f570", "40c532ed3aa52144bdc8bb4422e04b23c54196d352ac9cb0f3e93a5a059af120" @@ -939,7 +939,7 @@ "version": "1.0.0", "toolchainDigests": [ "2d2b38c4650795e2bba99e8ec38ca21adcb204042e9289577c535f7087e899d8", - "255483f15ef2cdfe0b759d84ef64bc20ec5236716ea6668642c6806e834ab437", + "d51f1db572c1ed8a52f2b9575572d76ad4880d25b3d942d7801cc29d8a2b087a", "295eb1ce67760638a81136febf727285f0feb4692a228df65ac75316b4a566c5" ] }, @@ -990,7 +990,7 @@ "version": "1.0.0", "toolchainDigests": [ "a6da84444e815dd94c15887190c7d6bd48c7bca5975fb5d493eb77ab39f27d80", - "255483f15ef2cdfe0b759d84ef64bc20ec5236716ea6668642c6806e834ab437", + "d51f1db572c1ed8a52f2b9575572d76ad4880d25b3d942d7801cc29d8a2b087a", "295eb1ce67760638a81136febf727285f0feb4692a228df65ac75316b4a566c5" ] }, @@ -1584,7 +1584,7 @@ "id": "diagnosis.hospital.compat", "version": "1.0.0", "toolchainDigests": [ - "15fe44854b79b0b860ad2b63fbd461f358f2b19ec4ac8921eca08978239a9703" + "196e6364b88635ceea6216545a680a486d57399636d58e9aefbf0b55dbc4704c" ] }, "determinism": "deterministic", diff --git a/orchestration/internalization/ast-grep.json b/orchestration/internalization/ast-grep.json index bdb525ec..65f4bf79 100644 --- a/orchestration/internalization/ast-grep.json +++ b/orchestration/internalization/ast-grep.json @@ -2,15 +2,15 @@ "schema": "code-intel-internalization-record.v1", "id": "internalization.ast-grep-record", "projectId": "code-intel-pipeline", - "subject": { "name": "ast-grep structural search executable", "kind": "adapted_capability", "source": { "uri": "https://github.com/ast-grep/ast-grep; installed-evidence=ast-grep 0.42.3", "revision": "installed-version:0.42.3; local-native-source-sha256:fb1bc02fbe9335e1ccbe66ad12ca2927bb3bace4722735e62b1fb2ab053af72d; local-conformance-sha256:91f9ee984fef0c28da495058e8c016ec3a0616aa01ebd6e1103b4b7df9b60a7d" }, "license": { "id": "MIT-UPSTREAM-CLAIM-LOCAL-COPY-MISSING", "obligations": ["do not redistribute or upgrade from this record until the MIT license text and package provenance are retained locally", "preserve preview-only authority, snapshot lease binding, scope and escape guards, generated-content exclusions, and failure semantics exactly"] } }, + "subject": { "name": "ast-grep structural search executable", "kind": "adapted_capability", "source": { "uri": "https://github.com/ast-grep/ast-grep; installed-evidence=ast-grep 0.42.3", "revision": "installed-version:0.42.3; local-native-source-sha256:fb1bc02fbe9335e1ccbe66ad12ca2927bb3bace4722735e62b1fb2ab053af72d; local-conformance-sha256:49753a4a9c491931453b0b8be775f01d3e8920bbccba919d087376f876c32171" }, "license": { "id": "MIT-UPSTREAM-CLAIM-LOCAL-COPY-MISSING", "obligations": ["do not redistribute or upgrade from this record until the MIT license text and package provenance are retained locally", "preserve preview-only authority, snapshot lease binding, scope and escape guards, generated-content exclusions, and failure semantics exactly"] } }, "adoption": { "rung": "invoke", "ownedBoundary": ["Pipeline owns edit.ast-grep-plan request validation, snapshot lease, path scope and escape guards, artifact contract, and failure mapping", "ast-grep owns pattern parsing, structural matching, and rewrite preview computation; this record does not authorize upgrade, vendoring, repository mutation, or reimplementation"], "necessityEvidence": { "evidenceIds": ["local:registry:edit.ast-grep-plan:optional", "local:i40:installed-ast-grep-0.42.3", "gap:ast-grep:package-provenance"], "checkedAt": 1785024000, "expiresAt": 1792800000 }, "compatibilityEvidence": { "evidenceIds": ["local:i40:preview-only-artifact-contract", "gap:ast-grep:replacement-command-drill"], "checkedAt": 1785024000, "expiresAt": 1792800000 }, "conformanceEvidence": { "evidenceIds": ["local:i40:scope-and-escape-conformance", "local:i40:operation-trace", "local:i41:ci-platform-matrix"], "checkedAt": 1785024000, "expiresAt": 1792800000 } }, "operationTrace": [ - { "integrationId": "edit.ast-grep-plan", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec edit.ast-grep-plan --request --out ", "implementationIdentity": { "providerId": "ast-grep", "implementationId": "edit.ast-grep-plan.compat+ast-grep-0.42.3", "activation": "optional preview-only capability envelope" }, "source": { "path": "crates/code-intel-cli/src/structured_edit.rs", "sha256": "fb1bc02fbe9335e1ccbe66ad12ca2927bb3bace4722735e62b1fb2ab053af72d" }, "conformance": { "path": "crates/code-intel-cli/tests/capability_exec.rs", "sha256": "91f9ee984fef0c28da495058e8c016ec3a0616aa01ebd6e1103b4b7df9b60a7d", "testName": "structured_edit_plan_is_scope_bound_and_preview_only" } } + { "integrationId": "edit.ast-grep-plan", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec edit.ast-grep-plan --request --out ", "implementationIdentity": { "providerId": "ast-grep", "implementationId": "edit.ast-grep-plan.compat+ast-grep-0.42.3", "activation": "optional preview-only capability envelope" }, "source": { "path": "crates/code-intel-cli/src/structured_edit.rs", "sha256": "fb1bc02fbe9335e1ccbe66ad12ca2927bb3bace4722735e62b1fb2ab053af72d" }, "conformance": { "path": "crates/code-intel-cli/tests/capability_exec.rs", "sha256": "49753a4a9c491931453b0b8be775f01d3e8920bbccba919d087376f876c32171", "testName": "structured_edit_plan_is_scope_bound_and_preview_only" } } ], "economics": { "benefit": { "metric": "registered preview-only structural edit operations with recomputable invocation trace", "value": 1, "unit": "operations" }, "cost": { "metric": "unclosed executable lifecycle gaps", "value": 4, "unit": "gaps" }, "benefitEvidence": { "evidenceIds": ["local:i40:operation-trace", "local:i40:scope-and-escape-conformance"], "checkedAt": 1785024000, "expiresAt": 1792800000 }, "costEvidence": { "evidenceIds": ["gap:ast-grep:package-provenance", "gap:ast-grep:local-license-copy", "gap:ast-grep:replacement-command-drill", "gap:ast-grep:latency-p50-p95-measurement"], "checkedAt": 1785024000, "expiresAt": 1792800000 } }, "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:i40:pinned-installed-version", "gap:ast-grep:upstream-maintenance-review"], "checkedAt": 1785024000, "expiresAt": 1792800000 }, "securityEvidence": { "evidenceIds": ["local:i40:no-network-read-only-invocation", "local:i41:ci-pinned-artifact-digests", "gap:ast-grep:package-supply-chain-review"], "checkedAt": 1785024000, "expiresAt": 1792800000 } }, "update": { "policy": "Do not upgrade ast-grep implicitly; before 2026-10-24 retain package provenance/license, rerun the scope and escape conformance test on the CI platform matrix, and refresh the pinned release artifact digests in ci.yml together with this record", "nextCheckAt": 1792800000, "evidence": { "evidenceIds": ["gap:ast-grep:update-review"], "checkedAt": 1785024000, "expiresAt": 1792800000 } }, - "ownedModifications": [{ "path": "crates/code-intel-cli/src/structured_edit.rs", "description": "Pipeline-owned preview-only planning adapter and snapshot-bound invocation boundary", "evidenceIds": ["local:i40:native-source-sha256:fb1bc02fbe9335e1ccbe66ad12ca2927bb3bace4722735e62b1fb2ab053af72d", "local:i40:conformance-sha256:91f9ee984fef0c28da495058e8c016ec3a0616aa01ebd6e1103b4b7df9b60a7d"] }], + "ownedModifications": [{ "path": "crates/code-intel-cli/src/structured_edit.rs", "description": "Pipeline-owned preview-only planning adapter and snapshot-bound invocation boundary", "evidenceIds": ["local:i40:native-source-sha256:fb1bc02fbe9335e1ccbe66ad12ca2927bb3bace4722735e62b1fb2ab053af72d", "local:i40:conformance-sha256:49753a4a9c491931453b0b8be775f01d3e8920bbccba919d087376f876c32171"] }], "rollback": { "strategy": "disable the optional edit.ast-grep-plan capability without changing any other artifact contract; no consumer holds authority through it", "evidence": { "evidenceIds": ["local:registry:edit.ast-grep-plan:optional", "gap:ast-grep:replacement-command-drill"], "checkedAt": 1785024000, "expiresAt": 1792800000 } }, "exit": { "strategy": "replace the executable only behind edit.ast-grep-plan after exact artifact and failure parity", "replacementCriteria": ["alternate command passes pattern, rewrite, scope, escape, generated-content, snapshot-lease, and oversized-output fixtures", "representative latency p50/p95 and cost do not regress beyond the approved budget", "new executable has pinned provenance, license, security, update, rollback, and retirement evidence"], "evidence": { "evidenceIds": ["gap:ast-grep:replacement-command-drill", "gap:ast-grep:latency-p50-p95-measurement"], "checkedAt": 1785024000, "expiresAt": 1792800000 } }, "retirement": { "status": "candidate", "triggers": ["replacement passes the complete structural edit planning contract", "installed executable identity or package provenance becomes unverifiable", "security or maintenance policy rejects the pinned package"], "evidence": { "evidenceIds": ["local:i40:operation-trace", "gap:ast-grep:replacement-command-drill"], "checkedAt": 1785024000, "expiresAt": 1792800000 } }, diff --git a/orchestration/internalization/codenexus.json b/orchestration/internalization/codenexus.json index bab533d6..f2352f02 100644 --- a/orchestration/internalization/codenexus.json +++ b/orchestration/internalization/codenexus.json @@ -13,7 +13,7 @@ "operationTrace": [ { "integrationId": "provider.codenexus-adapt", "operation": "adapt", "command": "target/debug/code-intel.exe provider codenexus-adapt --request --artifact-root --evaluated-at --max-age-seconds ", "implementationIdentity": { "providerId": "codenexus.full", "implementationId": "codenexus.service.v1", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/codenexus_adapter.rs", "sha256": "645675312135932dfce365a8dfc14e214cec78ee733f248606547b3eaa56edc8" }, "conformance": { "path": "crates/code-intel-cli/tests/codenexus_adapter.rs", "sha256": "4b2ea42c33aa9c180df550278d6e74501deb9f3f2c6133f7c2def850e468bdc5", "testName": "production_route_runs_full_lite_and_unavailable_through_a04" } }, { "integrationId": "provider.codenexus-adapt", "operation": "facade", "command": "legacy/run-code-intel.ps1 -CodeNexusAdapterRequest -CodeNexusAdapterArtifactRoot -CodeNexusAdapterEvaluatedAt -CodeNexusAdapterMaxAgeSeconds ", "implementationIdentity": { "providerId": "codenexus.lite-compat", "implementationId": "invoke-codenexus-lite.ps1", "activation": "explicit_fallback" }, "source": { "path": "crates/code-intel-cli/src/codenexus_adapter.rs", "sha256": "645675312135932dfce365a8dfc14e214cec78ee733f248606547b3eaa56edc8" }, "conformance": { "path": "crates/code-intel-cli/tests/codenexus_adapter.rs", "sha256": "4b2ea42c33aa9c180df550278d6e74501deb9f3f2c6133f7c2def850e468bdc5", "testName": "production_registry_facade_and_route_schema_are_declared" } }, - { "integrationId": "provider.codenexus-adapt", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec provider.codenexus-adapt --request --out --artifact-root ", "implementationIdentity": { "providerId": "codenexus.lite-compat", "implementationId": "invoke-codenexus-lite.ps1", "activation": "legacy_rollback" }, "source": { "path": "crates/code-intel-cli/src/builtin_provider_evidence.rs", "sha256": "004ad153b019ac9ae9869c095d90dce8b7a49bc5cdf745ce68f16dcd9ad795e0" }, "conformance": { "path": "crates/code-intel-cli/tests/capability_exec.rs", "sha256": "91f9ee984fef0c28da495058e8c016ec3a0616aa01ebd6e1103b4b7df9b60a7d", "testName": "codenexus_builtin_compat_dispatches_through_provider_codenexus_adapt" } }, + { "integrationId": "provider.codenexus-adapt", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec provider.codenexus-adapt --request --out --artifact-root ", "implementationIdentity": { "providerId": "codenexus.lite-compat", "implementationId": "invoke-codenexus-lite.ps1", "activation": "legacy_rollback" }, "source": { "path": "crates/code-intel-cli/src/builtin_provider_evidence.rs", "sha256": "1ee8976cc50b9a9f71bce61abe1be32fd406527d702040f7f223db48899b4150" }, "conformance": { "path": "crates/code-intel-cli/tests/capability_exec.rs", "sha256": "49753a4a9c491931453b0b8be775f01d3e8920bbccba919d087376f876c32171", "testName": "codenexus_builtin_compat_dispatches_through_provider_codenexus_adapt" } }, { "integrationId": "runtime.code-nexus-lite", "operation": "compat", "command": "pwsh -NoProfile -File \"$env:CODE_INTEL_HOME\\legacy/Invoke-CodeNexusLite.ps1\" -RepoPath ''", "implementationIdentity": { "providerId": "codenexus.lite-compat", "implementationId": "invoke-codenexus-lite.ps1", "activation": "explicit_fallback" }, "source": { "path": "legacy/Invoke-CodeNexusLite.ps1", "sha256": "cdd5c6d0fe940d2756c45a51095c275b13ebe64347914b581641693e1288ca56" }, "conformance": { "path": "legacy/scripts/tests/test-codenexus-adapter-contract.ps1", "sha256": "a835382e48223e35d680f29d27e05fc12c9d92306e1bafafc7add1135f26b1a8", "testName": "Invoke-LiteScriptEndToEnd" } }, { "integrationId": "localization.codenexus-lite", "operation": "compat", "command": "pwsh -NoProfile -File \"$env:CODE_INTEL_HOME\\legacy/Invoke-CodeNexusLite.ps1\" -RepoPath ''", "implementationIdentity": { "providerId": "codenexus.lite-compat", "implementationId": "invoke-codenexus-lite.ps1", "activation": "legacy_rollback" }, "source": { "path": "legacy/Invoke-CodeNexusLite.ps1", "sha256": "cdd5c6d0fe940d2756c45a51095c275b13ebe64347914b581641693e1288ca56" }, "conformance": { "path": "legacy/scripts/tests/test-codenexus-adapter-contract.ps1", "sha256": "a835382e48223e35d680f29d27e05fc12c9d92306e1bafafc7add1135f26b1a8", "testName": "Invoke-LiteScriptEndToEnd" } } ], diff --git a/orchestration/internalization/graph.json b/orchestration/internalization/graph.json index 59c38fab..50344c7c 100644 --- a/orchestration/internalization/graph.json +++ b/orchestration/internalization/graph.json @@ -13,7 +13,7 @@ "operationTrace": [ { "integrationId": "provider.graph-adapt", "operation": "adapt", "command": "target/debug/code-intel.exe provider graph-adapt --request --artifact-root --evaluated-at --max-age-seconds ", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "graph-adapter.v1", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/graph_adapter.rs", "sha256": "68a4f55b76080575721cde3ee62fd746999c6df8e79303b809baecf5d2514338" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "024625a11f874ccc93185a82e3285845150ebea301909bd23e3dfa9becd76f32", "testName": "public_route_usage_registry_facade_and_schemas_are_real" } }, { "integrationId": "provider.graph-adapt", "operation": "facade", "command": "legacy/run-code-intel.ps1 -GraphAdapterRequest -GraphAdapterArtifactRoot -GraphAdapterEvaluatedAt -GraphAdapterMaxAgeSeconds ", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "graph-adapter.v1", "activation": "compatibility_facade" }, "source": { "path": "crates/code-intel-cli/src/graph_adapter.rs", "sha256": "68a4f55b76080575721cde3ee62fd746999c6df8e79303b809baecf5d2514338" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "024625a11f874ccc93185a82e3285845150ebea301909bd23e3dfa9becd76f32", "testName": "public_route_usage_registry_facade_and_schemas_are_real" } }, - { "integrationId": "provider.graph-adapt", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec provider.graph-adapt --request --out --artifact-root ", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "provider.graph-builtin.compat", "activation": "required production capability envelope" }, "source": { "path": "crates/code-intel-cli/src/builtin_provider_evidence.rs", "sha256": "004ad153b019ac9ae9869c095d90dce8b7a49bc5cdf745ce68f16dcd9ad795e0" }, "conformance": { "path": "crates/code-intel-cli/tests/dag_run.rs", "sha256": "198cb2ca6abaffd40445ce0e03f821356ab191ee084446fd78235dabe93ccc45", "testName": "production_dag_output_commits_and_enters_the_authoritative_index" } }, + { "integrationId": "provider.graph-adapt", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec provider.graph-adapt --request --out --artifact-root ", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "provider.graph-builtin.compat", "activation": "required production capability envelope" }, "source": { "path": "crates/code-intel-cli/src/builtin_provider_evidence.rs", "sha256": "1ee8976cc50b9a9f71bce61abe1be32fd406527d702040f7f223db48899b4150" }, "conformance": { "path": "crates/code-intel-cli/tests/dag_run.rs", "sha256": "ab974d8eb1763e772545dc4236aa30f2c5b67dcb8e0a1464f58f51ef8f882c8b", "testName": "production_dag_output_commits_and_enters_the_authoritative_index" } }, { "integrationId": "graph.code-intel-understand", "operation": "refresh", "command": "target/debug/code-intel.exe graph --repo --language zh --write --json", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "internal-rust-graph", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/graph/mod.rs", "sha256": "245bd6d7eb6f774ea6f9cead43eb67f8b351fc92c930252d180d072f00524947" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "024625a11f874ccc93185a82e3285845150ebea301909bd23e3dfa9becd76f32", "testName": "internal_and_external_current_outputs_share_one_port_and_provenance_schema" } }, { "integrationId": "graph.code-intel-understand", "operation": "refreshFull", "command": "target/debug/code-intel.exe graph --repo --language zh --full --write --json", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "internal-rust-graph", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/graph/mod.rs", "sha256": "245bd6d7eb6f774ea6f9cead43eb67f8b351fc92c930252d180d072f00524947" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "024625a11f874ccc93185a82e3285845150ebea301909bd23e3dfa9becd76f32", "testName": "internal_and_external_current_outputs_share_one_port_and_provenance_schema" } }, { "integrationId": "graph.understand-external", "operation": "refresh", "command": "/understand --language zh", "implementationIdentity": { "providerId": "understand-anything", "implementationId": "unverified-upstream", "activation": "explicit_fallback" }, "source": { "path": "docs/graph-provider-adapter.md", "sha256": "21a6633e164aa5aab122e1d2de36fad47866611abe930b64d03bb67633408095" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "024625a11f874ccc93185a82e3285845150ebea301909bd23e3dfa9becd76f32", "testName": "fallback_identity_and_payload_identity_cannot_be_relabelled" } }, diff --git a/orchestration/internalization/rg.json b/orchestration/internalization/rg.json index 0bf9af9c..46c71664 100644 --- a/orchestration/internalization/rg.json +++ b/orchestration/internalization/rg.json @@ -2,16 +2,16 @@ "schema": "code-intel-internalization-record.v1", "id": "internalization.rg-record", "projectId": "code-intel-pipeline", - "subject": { "name": "ripgrep inventory executable", "kind": "adapted_capability", "source": { "uri": "https://github.com/BurntSushi/ripgrep; installed-evidence=rg 15.1.0 (rev af60c2de9d)", "revision": "installed-version:15.1.0-af60c2de9d; local-native-source-sha256:295eb1ce67760638a81136febf727285f0feb4692a228df65ac75316b4a566c5; local-conformance-sha256:91f9ee984fef0c28da495058e8c016ec3a0616aa01ebd6e1103b4b7df9b60a7d" }, "license": { "id": "MIT-OR-Unlicense-UPSTREAM-CLAIM-LOCAL-COPY-MISSING", "obligations": ["do not redistribute or upgrade from this record until the selected license text and package provenance are retained locally", "preserve exact inventory scope, exclusions, order normalization, snapshot lease, and failure semantics"] } }, + "subject": { "name": "ripgrep inventory executable", "kind": "adapted_capability", "source": { "uri": "https://github.com/BurntSushi/ripgrep; installed-evidence=rg 15.1.0 (rev af60c2de9d)", "revision": "installed-version:15.1.0-af60c2de9d; local-native-source-sha256:295eb1ce67760638a81136febf727285f0feb4692a228df65ac75316b4a566c5; local-conformance-sha256:49753a4a9c491931453b0b8be775f01d3e8920bbccba919d087376f876c32171" }, "license": { "id": "MIT-OR-Unlicense-UPSTREAM-CLAIM-LOCAL-COPY-MISSING", "obligations": ["do not redistribute or upgrade from this record until the selected license text and package provenance are retained locally", "preserve exact inventory scope, exclusions, order normalization, snapshot lease, and failure semantics"] } }, "adoption": { "rung": "invoke", "ownedBoundary": ["Pipeline owns inventory.rg request normalization, snapshot-controlled mirror, exclusions, artifact contract, and failure mapping", "ripgrep owns executable search and traversal behavior; this record does not authorize upgrade, vendoring, or reimplementation"], "necessityEvidence": { "evidenceIds": ["local:registry:inventory.rg:required", "local:r09:installed-rg-15.1.0-af60c2de9d", "gap:rg:package-provenance"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "compatibilityEvidence": { "evidenceIds": ["local:a00:inventory-parity", "local:r09:cross-platform-contract", "gap:rg:replacement-command-drill"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "conformanceEvidence": { "evidenceIds": ["local:r09:scope-exclusion-conformance", "local:r09:operation-trace", "gap:rg:representative-platform-matrix"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, "operationTrace": [ - { "integrationId": "inventory.rg", "operation": "run", "command": "legacy/run-code-intel.ps1 -RepoPath -Mode ", "implementationIdentity": { "providerId": "ripgrep", "implementationId": "rg-15.1.0-af60c2de9d-via-compat-facade", "activation": "required production facade" }, "source": { "path": "legacy/run-code-intel.ps1", "sha256": "2059c6a80e3bf4ac73dfe8297a1474714d881e83dac5d1e55a80d78184384a3e" }, "conformance": { "path": "crates/code-intel-cli/tests/capability_exec.rs", "sha256": "91f9ee984fef0c28da495058e8c016ec3a0616aa01ebd6e1103b4b7df9b60a7d", "testName": "normalized_inventory_matches_real_legacy_runner_with_custom_exclude" } }, - { "integrationId": "inventory.rg", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec inventory.rg --request --out ", "implementationIdentity": { "providerId": "ripgrep", "implementationId": "inventory.rg.compat+rg-15.1.0-af60c2de9d", "activation": "required production capability envelope" }, "source": { "path": "crates/code-intel-cli/src/capability_inventory.rs", "sha256": "295eb1ce67760638a81136febf727285f0feb4692a228df65ac75316b4a566c5" }, "conformance": { "path": "crates/code-intel-cli/tests/capability_exec.rs", "sha256": "91f9ee984fef0c28da495058e8c016ec3a0616aa01ebd6e1103b4b7df9b60a7d", "testName": "inventory_rg_exec_emits_one_result_and_stable_real_rg_artifact" } } + { "integrationId": "inventory.rg", "operation": "run", "command": "legacy/run-code-intel.ps1 -RepoPath -Mode ", "implementationIdentity": { "providerId": "ripgrep", "implementationId": "rg-15.1.0-af60c2de9d-via-compat-facade", "activation": "required production facade" }, "source": { "path": "legacy/run-code-intel.ps1", "sha256": "2059c6a80e3bf4ac73dfe8297a1474714d881e83dac5d1e55a80d78184384a3e" }, "conformance": { "path": "crates/code-intel-cli/tests/capability_exec.rs", "sha256": "49753a4a9c491931453b0b8be775f01d3e8920bbccba919d087376f876c32171", "testName": "normalized_inventory_matches_real_legacy_runner_with_custom_exclude" } }, + { "integrationId": "inventory.rg", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec inventory.rg --request --out ", "implementationIdentity": { "providerId": "ripgrep", "implementationId": "inventory.rg.compat+rg-15.1.0-af60c2de9d", "activation": "required production capability envelope" }, "source": { "path": "crates/code-intel-cli/src/capability_inventory.rs", "sha256": "295eb1ce67760638a81136febf727285f0feb4692a228df65ac75316b4a566c5" }, "conformance": { "path": "crates/code-intel-cli/tests/capability_exec.rs", "sha256": "49753a4a9c491931453b0b8be775f01d3e8920bbccba919d087376f876c32171", "testName": "inventory_rg_exec_emits_one_result_and_stable_real_rg_artifact" } } ], "economics": { "benefit": { "metric": "registered production inventory operations with recomputable invocation trace", "value": 2, "unit": "operations" }, "cost": { "metric": "unclosed executable lifecycle gaps", "value": 4, "unit": "gaps" }, "benefitEvidence": { "evidenceIds": ["local:r09:operation-trace", "local:r09:scope-exclusion-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["gap:rg:package-provenance", "gap:rg:local-license-copy", "gap:rg:replacement-command-drill", "gap:rg:latency-p50-p95-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:r09:pinned-installed-version", "gap:rg:upstream-maintenance-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:r09:no-network-read-only-invocation", "gap:rg:package-supply-chain-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, "update": { "policy": "Do not upgrade rg implicitly; before 2026-10-11 retain package provenance/license, rerun cross-platform conformance and benchmark, and exercise the replacement adapter", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:rg:update-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, - "ownedModifications": [{ "path": "crates/code-intel-cli/src/capability_inventory.rs", "description": "Pipeline-owned inventory adapter and snapshot-controlled invocation boundary", "evidenceIds": ["local:r09:native-source-sha256:295eb1ce67760638a81136febf727285f0feb4692a228df65ac75316b4a566c5", "local:r09:conformance-sha256:91f9ee984fef0c28da495058e8c016ec3a0616aa01ebd6e1103b4b7df9b60a7d"] }], + "ownedModifications": [{ "path": "crates/code-intel-cli/src/capability_inventory.rs", "description": "Pipeline-owned inventory adapter and snapshot-controlled invocation boundary", "evidenceIds": ["local:r09:native-source-sha256:295eb1ce67760638a81136febf727285f0feb4692a228df65ac75316b4a566c5", "local:r09:conformance-sha256:49753a4a9c491931453b0b8be775f01d3e8920bbccba919d087376f876c32171"] }], "rollback": { "strategy": "route inventory.rg to the preserved compatibility facade without changing the artifact contract or upgrading rg", "evidence": { "evidenceIds": ["local:a00:inventory-parity", "gap:rg:replacement-command-drill"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, "exit": { "strategy": "replace the executable only behind inventory.rg after exact artifact and failure parity", "replacementCriteria": ["alternate command passes scope, exclusion, symlink, ignore, empty-repository, and snapshot fixtures", "representative latency p50/p95 and cost do not regress beyond the approved budget", "new executable has pinned provenance, license, security, update, rollback, and retirement evidence"], "evidence": { "evidenceIds": ["gap:rg:replacement-command-drill", "gap:rg:latency-p50-p95-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, "retirement": { "status": "candidate", "triggers": ["replacement passes the complete inventory contract", "installed executable identity or package provenance becomes unverifiable", "security or maintenance policy rejects the pinned package"], "evidence": { "evidenceIds": ["local:r09:operation-trace", "gap:rg:replacement-command-drill"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, diff --git a/orchestration/internalization/sentrux.json b/orchestration/internalization/sentrux.json index 041fa656..dbee67f6 100644 --- a/orchestration/internalization/sentrux.json +++ b/orchestration/internalization/sentrux.json @@ -122,11 +122,11 @@ }, "source": { "path": "crates/code-intel-cli/src/builtin_provider_evidence.rs", - "sha256": "004ad153b019ac9ae9869c095d90dce8b7a49bc5cdf745ce68f16dcd9ad795e0" + "sha256": "1ee8976cc50b9a9f71bce61abe1be32fd406527d702040f7f223db48899b4150" }, "conformance": { "path": "crates/code-intel-cli/tests/dag_run.rs", - "sha256": "198cb2ca6abaffd40445ce0e03f821356ab191ee084446fd78235dabe93ccc45", + "sha256": "ab974d8eb1763e772545dc4236aa30f2c5b67dcb8e0a1464f58f51ef8f882c8b", "testName": "production_dag_output_commits_and_enters_the_authoritative_index" } }, @@ -141,7 +141,7 @@ }, "source": { "path": "crates/code-intel-cli/src/sentrux.rs", - "sha256": "e9eb3fcb06ffbd736dbb75c36af7126fed7a57b57fdab362124f7e19140f8066" + "sha256": "d69f51b84a1f60036acc42e72bc7fe99eef4e9d6333944e2ffc3c90848490724" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", @@ -160,7 +160,7 @@ }, "source": { "path": "crates/code-intel-cli/src/sentrux.rs", - "sha256": "e9eb3fcb06ffbd736dbb75c36af7126fed7a57b57fdab362124f7e19140f8066" + "sha256": "d69f51b84a1f60036acc42e72bc7fe99eef4e9d6333944e2ffc3c90848490724" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", @@ -179,7 +179,7 @@ }, "source": { "path": "crates/code-intel-cli/src/sentrux.rs", - "sha256": "e9eb3fcb06ffbd736dbb75c36af7126fed7a57b57fdab362124f7e19140f8066" + "sha256": "d69f51b84a1f60036acc42e72bc7fe99eef4e9d6333944e2ffc3c90848490724" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", @@ -259,7 +259,7 @@ }, "conformance": { "path": "crates/code-intel-cli/tests/dag_run.rs", - "sha256": "198cb2ca6abaffd40445ce0e03f821356ab191ee084446fd78235dabe93ccc45", + "sha256": "ab974d8eb1763e772545dc4236aa30f2c5b67dcb8e0a1464f58f51ef8f882c8b", "testName": "production_run_preserves_doctor_domain_failure_and_completes_unrelated_branch" } } diff --git a/orchestration/retirements/e04-codenexus-direct/compatibility-retirement-deletion-diff.json b/orchestration/retirements/e04-codenexus-direct/compatibility-retirement-deletion-diff.json index a1d09912..737b867b 100644 --- a/orchestration/retirements/e04-codenexus-direct/compatibility-retirement-deletion-diff.json +++ b/orchestration/retirements/e04-codenexus-direct/compatibility-retirement-deletion-diff.json @@ -1 +1 @@ -{"schema":"code-intel-compatibility-retirement-deletion-diff.v1","snapshotIdentity":"4a84b1744f16b7fc73e5bd3025f8851b0c6031cc5ede0a939240d2c85221d301","retirementId":"retire-codenexus-direct-branch","legacyBranchId":"run-code-intel.codenexus-lite.direct","affectedFiles":["run-code-intel.ps1"],"deletionsOnly":true,"summary":"Proposed deletion contains only the one live direct CodeNexus-lite facade branch. It is not executable until a separate route substitution is approved.","patch":{"algorithm":"replayable-delete-only-v1","sha256":"e57b8bfa80d9ea74e6f8f73aa9accbd404ae3978a2f6b9284e3ab03556868c68","files":[{"baseBlobSha256":"2059c6a80e3bf4ac73dfe8297a1474714d881e83dac5d1e55a80d78184384a3e","baseText":"#requires -Version 7.2\n\nparam(\n [string]$Repo = \"\",\n [string]$RepoPath = \"\",\n\n [string]$Config = \"\",\n\n [ValidateSet(\"auto\", \"windows\", \"macos\", \"linux\")]\n [string]$Platform = \"auto\",\n\n [ValidateSet(\"lite\", \"normal\", \"full\")]\n [string]$Mode = \"normal\",\n\n [string]$Language = \"\",\n\n [string]$ArtifactRoot = \"\",\n [string]$SentruxPath = \"\",\n [string]$RepowiseWorkspaceRoot = \"\",\n [string]$RepowiseShadowRoot = \"\",\n [string[]]$RepowiseScopePaths = @(),\n [string[]]$RepowiseRootFiles = @(),\n [int]$RepowiseTimeoutSeconds = 600,\n [string]$RepowiseProvider = \"\",\n [string]$RepowiseModel = \"\",\n [string]$RepowiseReasoning = \"\",\n [string]$ModelRoutingResult = \"\",\n [string]$ModelInventoryResult = \"\",\n [string]$ModelExecutableHandle = \"\",\n [string]$ModelPromptFile = \"\",\n [string]$ModelEndpoint = \"\",\n [ValidateSet(\"\", \"openai\", \"anthropic\", \"ollama\")]\n [string]$ModelProtocol = \"\",\n [string]$ModelCredentialEnvName = \"\",\n [ValidateRange(1, 3600)]\n [int]$ModelTimeoutSeconds = 300,\n [ValidateSet(\"json\", \"jsonl\")]\n [string]$ModelResponseFormat = \"json\",\n [string]$ModelAdapterRequest = \"\",\n [string]$ModelAdapterArtifactRoot = \"\",\n [string]$RuntimeCiEvidenceRequest = \"\",\n [string]$RuntimeCiEvidenceArtifactRoot = \"\",\n [string]$RepowiseAdapterRequest = \"\",\n [string]$RepowiseAdapterArtifactRoot = \"\",\n [long]$RepowiseAdapterEvaluatedAt = 0,\n [long]$RepowiseAdapterMaxAgeSeconds = 0,\n [string]$GraphAdapterRequest = \"\",\n [string]$GraphAdapterArtifactRoot = \"\",\n [long]$GraphAdapterEvaluatedAt = 0,\n [long]$GraphAdapterMaxAgeSeconds = 0,\n [string]$SentruxAdapterRequest = \"\",\n [string]$SentruxAdapterArtifactRoot = \"\",\n [long]$SentruxAdapterEvaluatedAt = 0,\n [long]$SentruxAdapterMaxAgeSeconds = 0,\n [string]$CodeNexusAdapterRequest = \"\",\n [string]$CodeNexusAdapterArtifactRoot = \"\",\n [long]$CodeNexusAdapterEvaluatedAt = 0,\n [long]$CodeNexusAdapterMaxAgeSeconds = 0,\n [string]$SurvivalScanRequest = \"\",\n [string]$SurvivalScanArtifactRoot = \"\",\n [string]$RunCommitSourceRoot = \"\",\n [string]$RunCommitAuthorityRoot = \"\",\n [string]$RunCommitManifestRef = \"\",\n [string]$RunCommitFinalName = \"\",\n [string[]]$InventoryExclude = @(),\n\n [switch]$DagCoordinate,\n\n [switch]$SaveSentruxBaseline,\n [switch]$AutoSaveMissingSentruxBaseline,\n [switch]$SkipRepowise,\n [switch]$RepowiseDocs,\n [switch]$AllowRepowiseShadowMutation,\n [switch]$SkipRepomix,\n [ValidateSet(\"xml\", \"markdown\", \"json\", \"plain\")]\n [string]$RepomixStyle = \"markdown\",\n [switch]$RepomixCompress,\n [switch]$SkipSentrux,\n[switch]$SkipSentruxCheck,\n[switch]$SkipSentruxGate,\n[switch]$RequireUnderstandGraph,\n[switch]$WorkspaceAdd,\n[switch]$SkipOpenSpec,\n[switch]$AutoOpenSpec,\n[ValidateSet(\"auto\", \"enabled\", \"disabled\")]\n[string]$ProactiveSkillSuggestions = \"auto\",\n[ValidateSet(\"auto\", \"ask\", \"enabled\", \"disabled\")]\n[string]$AutomaticPullRequests = \"auto\",\n[string]$BugSkill = \"\"\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\n$platformModule = Join-Path (Join-Path $PSScriptRoot \"tools\") \"code-intel-platform.psm1\"\nImport-Module $platformModule -Force\n$followUpAutomationModule = Join-Path (Join-Path $PSScriptRoot \"tools\") \"code-intel-follow-up-automation.psm1\"\nImport-Module $followUpAutomationModule -Force\n$effectivePlatform = Get-CodeIntelPlatform -Platform $Platform\n$codeIntelPaths = Get-CodeIntelPaths -Platform $effectivePlatform -Root (Split-Path -Parent $PSScriptRoot)\n$rustExecutableName = if ($effectivePlatform -eq \"windows\") { \"code-intel.exe\" } else { \"code-intel\" }\n$defaultRustCli = Join-Path (Split-Path -Parent $PSScriptRoot) (Join-Path \"target/debug\" $rustExecutableName)\n\n[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()\n$OutputEncoding = [System.Text.UTF8Encoding]::new()\n$env:PYTHONIOENCODING = \"utf-8\"\n$env:PYTHONUTF8 = \"1\"\n$env:TERM = \"xterm\"\n$env:NO_COLOR = \"1\"\n$env:RICH_FORCE_TERMINAL = \"0\"\n\nif (-not [string]::IsNullOrWhiteSpace($ModelInventoryResult)) {\n if ([string]::IsNullOrWhiteSpace($ModelRoutingResult) -or\n [string]::IsNullOrWhiteSpace($ModelPromptFile) -or\n [string]::IsNullOrWhiteSpace($ModelAdapterArtifactRoot)) {\n throw \"Model request synthesis requires inventory, routing, prompt, and adapter artifact root\"\n }\n $synthesisScript = Join-Path $PSScriptRoot \"New-ModelAdapterRequest.ps1\"\n $delegateScript = Join-Path $PSScriptRoot \"Invoke-ModelChannelDelegate.ps1\"\n if (-not (Test-Path -LiteralPath $synthesisScript -PathType Leaf) -or -not (Test-Path -LiteralPath $delegateScript -PathType Leaf)) {\n throw \"Model request synthesis or delegate implementation is missing\"\n }\n New-Item -ItemType Directory -Force -Path $ModelAdapterArtifactRoot | Out-Null\n $synthesizedRequest = Join-Path ([IO.Path]::GetFullPath($ModelAdapterArtifactRoot)) \"model-adapter-request.v2.json\"\n $synthesisParameters = @{\n Inventory = $ModelInventoryResult\n Routing = $ModelRoutingResult\n PromptFile = $ModelPromptFile\n OutputPath = $synthesizedRequest\n TimeoutSeconds = $ModelTimeoutSeconds\n ResponseFormat = $ModelResponseFormat\n }\n if (-not [string]::IsNullOrWhiteSpace($ModelExecutableHandle)) { $synthesisParameters.ExecutableHandle = $ModelExecutableHandle }\n if (-not [string]::IsNullOrWhiteSpace($ModelEndpoint)) { $synthesisParameters.Endpoint = $ModelEndpoint }\n if (-not [string]::IsNullOrWhiteSpace($ModelProtocol)) { $synthesisParameters.Protocol = $ModelProtocol }\n if (-not [string]::IsNullOrWhiteSpace($ModelCredentialEnvName)) { $synthesisParameters.CredentialEnvName = $ModelCredentialEnvName }\n & $synthesisScript @synthesisParameters | Out-Null\n & $delegateScript -Request $synthesizedRequest -ArtifactRoot $ModelAdapterArtifactRoot\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($ModelAdapterRequest)) {\n if ([string]::IsNullOrWhiteSpace($ModelAdapterArtifactRoot)) { throw \"Model adapter facade requires an artifact root\" }\n $delegateScript = Join-Path $PSScriptRoot \"Invoke-ModelChannelDelegate.ps1\"\n if (-not (Test-Path -LiteralPath $delegateScript -PathType Leaf)) { throw \"Model channel delegate is missing: $delegateScript\" }\n & $delegateScript -Request $ModelAdapterRequest -ArtifactRoot $ModelAdapterArtifactRoot\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($RepowiseAdapterRequest)) {\n if ([string]::IsNullOrWhiteSpace($RepowiseAdapterArtifactRoot) -or\n $RepowiseAdapterEvaluatedAt -lt 0 -or\n $RepowiseAdapterMaxAgeSeconds -le 0) {\n throw \"Repowise adapter facade requires artifact root, non-negative evaluated-at, and positive max-age\"\n }\n $rustCli = $defaultRustCli\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) {\n throw \"Repowise adapter binary is missing: $rustCli\"\n }\n & $rustCli provider repowise-adapt `\n --request $RepowiseAdapterRequest `\n --artifact-root $RepowiseAdapterArtifactRoot `\n --evaluated-at $RepowiseAdapterEvaluatedAt `\n --max-age-seconds $RepowiseAdapterMaxAgeSeconds\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($GraphAdapterRequest)) {\n if ([string]::IsNullOrWhiteSpace($GraphAdapterArtifactRoot) -or\n $GraphAdapterEvaluatedAt -lt 0 -or\n $GraphAdapterMaxAgeSeconds -le 0) {\n throw \"Graph adapter facade requires artifact root, non-negative evaluated-at, and positive max-age\"\n }\n $rustCli = $defaultRustCli\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) {\n throw \"Graph adapter binary is missing: $rustCli\"\n }\n & $rustCli provider graph-adapt `\n --request $GraphAdapterRequest `\n --artifact-root $GraphAdapterArtifactRoot `\n --evaluated-at $GraphAdapterEvaluatedAt `\n --max-age-seconds $GraphAdapterMaxAgeSeconds\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($SentruxAdapterRequest)) {\n if ([string]::IsNullOrWhiteSpace($SentruxAdapterArtifactRoot) -or\n $SentruxAdapterEvaluatedAt -lt 0 -or\n $SentruxAdapterMaxAgeSeconds -le 0) {\n throw \"Sentrux adapter facade requires artifact root, non-negative evaluated-at, and positive max-age\"\n }\n $rustCli = $defaultRustCli\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) { throw \"Sentrux adapter binary is missing: $rustCli\" }\n & $rustCli provider sentrux-adapt --request $SentruxAdapterRequest --artifact-root $SentruxAdapterArtifactRoot --evaluated-at $SentruxAdapterEvaluatedAt --max-age-seconds $SentruxAdapterMaxAgeSeconds\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($CodeNexusAdapterRequest)) {\n if ([string]::IsNullOrWhiteSpace($CodeNexusAdapterArtifactRoot) -or\n $CodeNexusAdapterEvaluatedAt -lt 0 -or\n $CodeNexusAdapterMaxAgeSeconds -le 0) {\n throw \"CodeNexus adapter facade requires artifact root, non-negative evaluated-at, and positive max-age\"\n }\n $rustCli = $defaultRustCli\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) {\n throw \"CodeNexus adapter binary is missing: $rustCli\"\n }\n & $rustCli provider codenexus-adapt `\n --request $CodeNexusAdapterRequest `\n --artifact-root $CodeNexusAdapterArtifactRoot `\n --evaluated-at $CodeNexusAdapterEvaluatedAt `\n --max-age-seconds $CodeNexusAdapterMaxAgeSeconds\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($SurvivalScanRequest)) {\n if ([string]::IsNullOrWhiteSpace($SurvivalScanArtifactRoot)) {\n throw \"Repository survival scan facade requires an artifact root\"\n }\n $rustCli = $defaultRustCli\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) {\n throw \"Repository survival scan binary is missing: $rustCli\"\n }\n & $rustCli repository survival-scan `\n --request $SurvivalScanRequest `\n --artifact-root $SurvivalScanArtifactRoot\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($RunCommitManifestRef)) {\n if ([string]::IsNullOrWhiteSpace($RunCommitSourceRoot) -or\n [string]::IsNullOrWhiteSpace($RunCommitAuthorityRoot) -or\n [string]::IsNullOrWhiteSpace($RunCommitFinalName)) {\n throw \"Run commit facade requires source root, authority root, manifest Artifact Ref, and final name\"\n }\n $rustCli = $defaultRustCli\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) {\n throw \"Run commit binary is missing: $rustCli\"\n }\n & $rustCli run commit `\n --source-root $RunCommitSourceRoot `\n --authority-root $RunCommitAuthorityRoot `\n --manifest-ref $RunCommitManifestRef `\n --final-name $RunCommitFinalName\n exit $LASTEXITCODE\n}\n\nfunction Resolve-Repo {\n param([string]$Path)\n\n $item = Get-Item -LiteralPath $Path -ErrorAction Stop\n if (-not $item.PSIsContainer) {\n throw \"Repo path is not a directory: $Path\"\n }\n return $item.FullName\n}\n\nfunction Find-RepoConfigByPath {\n param([object]$ReposConfig, [string]$ResolvedRepoPath)\n\n if ($null -eq $ReposConfig -or [string]::IsNullOrWhiteSpace($ResolvedRepoPath)) { return $null }\n $normalizedRepoPath = [System.IO.Path]::TrimEndingDirectorySeparator($ResolvedRepoPath)\n foreach ($entry in $ReposConfig.PSObject.Properties) {\n $configuredPath = Get-JsonProperty $entry.Value \"path\"\n if ([string]::IsNullOrWhiteSpace([string]$configuredPath)) { continue }\n try {\n $resolvedConfiguredPath = Resolve-Repo ([string]$configuredPath)\n }\n catch {\n continue\n }\n $normalizedConfiguredPath = [System.IO.Path]::TrimEndingDirectorySeparator($resolvedConfiguredPath)\n if ([string]::Equals($normalizedConfiguredPath, $normalizedRepoPath, [System.StringComparison]::OrdinalIgnoreCase)) {\n return $entry.Value\n }\n }\n return $null\n}\n\nfunction Test-CommandAvailable {\n param([string]$Name)\n return [bool](Get-Command $Name -ErrorAction SilentlyContinue)\n}\n\n# Git config keys that name a program Git will execute, pinned empty so a\n# scanned repository's own .git/config cannot supply one. `core.fsmonitor`\n# runs on ordinary read commands like `git status`, before any gate in this\n# pipeline has looked at the repository. Mirrors\n# crates/code-intel-cli/src/hardened_git.rs.\n$script:GitHardening = @(\n \"-c\", \"core.fsmonitor=\",\n \"-c\", \"core.hooksPath=\",\n \"-c\", \"core.sshCommand=\",\n \"-c\", \"diff.external=\",\n \"-c\", \"core.pager=\"\n)\n\nfunction Test-GitRepository {\nparam([string]$Path)\n\nif (-not (Test-CommandAvailable \"git\")) { return $false }\n$output = & git @script:GitHardening -C $Path rev-parse --is-inside-work-tree 2>$null\nreturn ($LASTEXITCODE -eq 0 -and [string]$output -eq \"true\")\n}\n\n# Workflow recommendations are owned by the standalone advisory atom in OpenSpec-Detector.ps1.\n\nfunction Get-JsonProperty {\n param(\n [object]$Object,\n [string]$Name\n )\n\n if ($null -eq $Object) { return $null }\n $prop = $Object.PSObject.Properties[$Name]\n if ($null -eq $prop) { return $null }\n return $prop.Value\n}\n\nfunction Resolve-ConfigString {\n param(\n [string]$Value,\n [object]$RepoConfig,\n [object]$ConfigData,\n [string]$Name,\n [string[]]$EnvNames = @(),\n [string]$Default = \"\"\n )\n\n if (-not [string]::IsNullOrWhiteSpace($Value)) { return $Value }\n\n $repoValue = Get-JsonProperty $RepoConfig $Name\n if (-not [string]::IsNullOrWhiteSpace([string]$repoValue)) { return [string]$repoValue }\n\n $globalValue = Get-JsonProperty $ConfigData $Name\n if (-not [string]::IsNullOrWhiteSpace([string]$globalValue)) { return [string]$globalValue }\n\n foreach ($envName in $EnvNames) {\n $envValue = [Environment]::GetEnvironmentVariable($envName, \"Process\")\n if ([string]::IsNullOrWhiteSpace($envValue)) {\n $envValue = [Environment]::GetEnvironmentVariable($envName, \"User\")\n }\n if (-not [string]::IsNullOrWhiteSpace($envValue)) { return $envValue }\n }\n\n return $Default\n}\n\nfunction Normalize-RepowiseProvider {\n param([string]$Provider)\n if ([string]::IsNullOrWhiteSpace($Provider)) { return \"mock\" }\n $normalized = $Provider.Trim()\n if ($normalized -ieq \"ccw\") { return \"codex_cli\" }\n return $normalized\n}\n\nfunction Get-RepowiseProviderArgs {\n param(\n [string]$Provider,\n [string]$Model,\n [string]$Reasoning\n )\n\n $args = @(\"--provider\", $Provider)\n if (-not [string]::IsNullOrWhiteSpace($Model)) { $args += @(\"--model\", $Model) }\n if (-not [string]::IsNullOrWhiteSpace($Reasoning)) { $args += @(\"--reasoning\", $Reasoning) }\n return $args\n}\n\nfunction Get-DefaultArtifactRoot {\n return (Get-CodeIntelArtifactRoot -Platform $effectivePlatform)\n}\n\nfunction Get-DefaultShadowRoot {\n return (Get-CodeIntelShadowRoot -Platform $effectivePlatform)\n}\n\nfunction Resolve-ChildPath {\n param(\n [string]$Base,\n [string]$Path\n )\n\n if ([string]::IsNullOrWhiteSpace($Path)) { return $Base }\n if ([System.IO.Path]::IsPathRooted($Path)) { return (Resolve-Repo $Path) }\n return Resolve-Repo (Join-Path $Base $Path)\n}\n\nfunction Invoke-LoggedStep {\n param(\n [string]$Name,\n [scriptblock]$Body\n )\n\n $started = Get-Date\n $entry = [ordered]@{\n name = $Name\n startedAt = $started.ToString(\"o\")\n status = \"running\"\n exitCode = $null\n output = \"\"\n error = \"\"\n finishedAt = $null\n durationMs = $null\n }\n\n try {\n $global:LASTEXITCODE = 0\n $previousErrorActionPreference = $ErrorActionPreference\n try {\n $ErrorActionPreference = \"Continue\"\n $output = & $Body 2>&1\n }\n finally {\n $ErrorActionPreference = $previousErrorActionPreference\n }\n $entry.output = ($output | ForEach-Object { $_.ToString() } | Out-String).Trim()\n if ($global:LASTEXITCODE -ne 0) {\n throw \"Command exited with code $global:LASTEXITCODE\"\n }\n $entry.status = \"passed\"\n $entry.exitCode = 0\n }\n catch {\n $entry.status = \"failed\"\n if ($global:LASTEXITCODE -ne 0) {\n $entry.exitCode = $global:LASTEXITCODE\n }\n else {\n $entry.exitCode = 1\n }\n $entry.error = $_.Exception.Message\n if ([string]::IsNullOrWhiteSpace([string]$entry.output)) {\n $entry.output = ($_ | Out-String).Trim()\n }\n }\n finally {\n $finished = Get-Date\n $entry.finishedAt = $finished.ToString(\"o\")\n $entry.durationMs = [int]($finished - $started).TotalMilliseconds\n }\n\n return [pscustomobject]$entry\n}\n\nfunction Convert-OptionalRepowiseTimeout {\n param([object]$Step)\n\n if ($null -eq $Step) { return $Step }\n $blob = (([string]$Step.error) + \"`n\" + ([string]$Step.output)).ToLowerInvariant()\n if ([string]$Step.status -eq \"failed\" -and [string]$Step.name -like \"repowise*\" -and $blob -match \"timed out after\") {\n $Step.status = \"skipped\"\n $Step.exitCode = $null\n $Step.output = \"Optional Repowise step skipped after timeout. $($Step.error)\"\n $Step.error = \"\"\n }\n return $Step\n}\n\nfunction Get-RelativePathSafe {\n param(\n [string]$Base,\n [string]$Path\n )\n\n try {\n return [System.IO.Path]::GetRelativePath($Base, $Path)\n }\n catch {\n try {\n $baseFull = [System.IO.Path]::GetFullPath($Base)\n $pathFull = [System.IO.Path]::GetFullPath($Path)\n if (-not $baseFull.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {\n $baseFull = $baseFull + [System.IO.Path]::DirectorySeparatorChar\n }\n if ((Test-Path -LiteralPath $pathFull -PathType Container) -and -not $pathFull.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {\n $pathFull = $pathFull + [System.IO.Path]::DirectorySeparatorChar\n }\n $relative = ([uri]$baseFull).MakeRelativeUri([uri]$pathFull).ToString()\n $relative = [uri]::UnescapeDataString($relative).Replace(\"/\", [System.IO.Path]::DirectorySeparatorChar)\n if ([string]::IsNullOrWhiteSpace($relative)) { return \".\" }\n return $relative\n }\n catch {\n return $Path\n }\n }\n}\n\nfunction Get-StepFailureCategory {\n param([object]$Step)\n\n $name = [string]$Step.name\n $status = [string]$Step.status\n $blob = (([string]$Step.error) + \"`n\" + ([string]$Step.output)).ToLowerInvariant()\n\n if ($name -eq \"understand graph\" -and ($status -eq \"failed\" -or $status -eq \"manual_required\")) {\n return \"graph_missing\"\n }\n if ($name -like \"sentrux*\" -and ($status -eq \"failed\" -or $status -eq \"manual_required\")) {\n return \"sentrux_fail\"\n }\n if (($name -like \"repowise*\" -or $name -eq \"provider preflight\") -and $blob -match \"rate_limit|quota|usage limit exceeded|error code: 429|too many requests|provider_quota\") {\n return \"provider_quota\"\n }\n if (($name -like \"repowise*\" -or $name -eq \"provider preflight\") -and $blob -match \"provider_unavailable|model_not_found|not_found_error|error code: 404|status code: 404\") {\n return \"provider_unavailable\"\n }\n if (($name -like \"repowise*\" -or $name -eq \"provider preflight\") -and $blob -match \"config_error|authentication_error|invalid api key|not authorized|token not match\") {\n return \"config_error\"\n }\n if ($status -eq \"failed\") {\n return \"local_tool_error\"\n }\n return $null\n}\n\nfunction Get-CodeIntelEffectiveFailedSteps {\n param(\n [object[]]$FailedSteps,\n [int]$BlockingSentruxDebt,\n # Issue #130: a \"sentrux gate\" step that failed/manual_required because it found\n # architecture/quality debt (god_files, coupling, quality, cycles, ...) is a FINDING,\n # not a process failure -- it is the gate doing its job. A \"sentrux check\" step keeps\n # its prior blocking-debt-gated behavior; this exemption is scoped to gate only.\n # $Failures (the code-intel-sentrux-failures.v1 object from New-CodeIntelSentruxFailures)\n # is optional so existing callers that only pass -FailedSteps/-BlockingSentruxDebt keep\n # their old behavior unchanged. When it is supplied, $Failures.gate is the discriminator:\n # non-null means a gate:* record was actually parsed from \"sentrux gate\" stdout (a real\n # finding); null means the gate step failed without producing any recognizable output at\n # all (a genuine crash), which must still count as an effective failure.\n [object]$Failures = $null\n )\n\n $gateHasParsedRecord = ($null -ne $Failures -and $null -ne $Failures.gate)\n\n return @($FailedSteps | Where-Object {\n $category = [string](Get-StepFailureCategory $_)\n if ($category -ne \"sentrux_fail\") { return $true }\n if ([string]$_.name -like \"sentrux gate*\") {\n return -not $gateHasParsedRecord\n }\n return $BlockingSentruxDebt -gt 0\n })\n}\n\nfunction Complete-NodeLintHygieneStep {\n param(\n [System.Collections.Specialized.OrderedDictionary]$Step,\n [datetime]$Started\n )\n\n $finished = Get-Date\n $Step[\"finishedAt\"] = $finished.ToString(\"o\")\n $Step[\"durationMs\"] = [int]($finished - $Started).TotalMilliseconds\n return [pscustomobject]$Step\n}\n\nfunction Get-NodeLintHygieneStep {\n param(\n [string]$RepoPath,\n [bool]$RgAvailable\n )\n\n $started = Get-Date\n $step = [ordered]@{\n name = \"node lint hygiene\"\n startedAt = $started.ToString(\"o\")\n status = \"skipped\"\n exitCode = $null\n output = \"\"\n error = \"\"\n finishedAt = \"\"\n durationMs = 0\n }\n\n try {\n $packageJson = Join-Path $RepoPath \"package.json\"\n if (-not (Test-Path -LiteralPath $packageJson -PathType Leaf)) {\n $step[\"output\"] = \"No package.json found.\"\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n }\n\n $package = Get-Content -LiteralPath $packageJson -Raw | ConvertFrom-Json\n $scripts = Get-JsonProperty $package \"scripts\"\n $lintScript = [string](Get-JsonProperty $scripts \"lint\")\n if ([string]::IsNullOrWhiteSpace($lintScript) -or $lintScript -notmatch \"\\beslint\\b\") {\n $step[\"output\"] = \"No root ESLint lint script detected.\"\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n }\n\n if (-not $RgAvailable) {\n $step[\"output\"] = \"rg unavailable; skip static ESLint asset-boundary check.\"\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n }\n\n $rgArgs = @(\n \"--files\",\n \"--hidden\",\n \"--no-ignore\",\n \"-g\", \"!**/.git/**\",\n \"-g\", \"!**/node_modules/**\",\n \"-g\", \"!**/dist/**\",\n \"-g\", \"!**/build/**\",\n $RepoPath\n )\n $repoFiles = @(& rg @rgArgs 2>$null)\n $global:LASTEXITCODE = 0\n $normalizedFiles = @($repoFiles | ForEach-Object { ([string]$_).Replace(\"\\\", \"/\") })\n\n $assetPatterns = New-Object System.Collections.Generic.List[string]\n if (@($normalizedFiles | Where-Object { $_ -match \"(^|/)apps/[^/]+/public/charting_library/\" } | Select-Object -First 1).Count -gt 0) {\n $assetPatterns.Add(\"apps/*/public/charting_library/**\")\n }\n if (@($normalizedFiles | Where-Object { $_ -match \"(^|/)apps/[^/]+/public/datafeeds/\" } | Select-Object -First 1).Count -gt 0) {\n $assetPatterns.Add(\"apps/*/public/datafeeds/**\")\n }\n if (@($normalizedFiles | Where-Object { $_ -match \"(^|/)packages/[^/]+/vendor/\" } | Select-Object -First 1).Count -gt 0) {\n $assetPatterns.Add(\"packages/*/vendor/**\")\n }\n if (@($normalizedFiles | Where-Object { $_ -match \"(^|/)vendor/\" } | Select-Object -First 1).Count -gt 0) {\n $assetPatterns.Add(\"vendor/**\")\n }\n\n if ($assetPatterns.Count -eq 0) {\n $step[\"status\"] = \"passed\"\n $step[\"exitCode\"] = 0\n $step[\"output\"] = \"Root ESLint lint script detected; no known generated/vendor static asset directories found.\"\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n }\n\n $configNames = @(\"eslint.config.js\", \"eslint.config.mjs\", \"eslint.config.cjs\", \".eslintignore\", \".eslintrc\", \".eslintrc.json\", \".eslintrc.js\", \".eslintrc.cjs\")\n $configFiles = @($configNames | ForEach-Object {\n $candidate = Join-Path $RepoPath $_\n if (Test-Path -LiteralPath $candidate -PathType Leaf) { $candidate }\n })\n if ($configFiles.Count -eq 0) {\n $step[\"status\"] = \"manual_required\"\n $step[\"exitCode\"] = 0\n $step[\"output\"] = \"Root lint script uses ESLint and known generated/vendor static asset dirs exist, but no root ESLint config or ignore file was found. Add ignores for: $($assetPatterns -join ', '), then run root lint before push.\"\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n }\n\n $configText = (($configFiles | ForEach-Object { Get-Content -LiteralPath $_ -Raw }) -join [Environment]::NewLine).Replace(\"\\\", \"/\")\n $missing = New-Object System.Collections.Generic.List[string]\n foreach ($pattern in $assetPatterns) {\n $covered = $false\n if ($pattern -eq \"apps/*/public/charting_library/**\") {\n $covered = ($configText -match \"charting_library|apps/\\*/public|\\*\\*/public|public/\\*\\*\")\n }\n elseif ($pattern -eq \"apps/*/public/datafeeds/**\") {\n $covered = ($configText -match \"datafeeds|apps/\\*/public|\\*\\*/public|public/\\*\\*\")\n }\n elseif ($pattern -eq \"packages/*/vendor/**\" -or $pattern -eq \"vendor/**\") {\n $covered = ($configText -match \"vendor\")\n }\n\n if (-not $covered) {\n $missing.Add($pattern)\n }\n }\n\n if ($missing.Count -gt 0) {\n $step[\"status\"] = \"manual_required\"\n $step[\"exitCode\"] = 0\n $step[\"output\"] = \"Root lint script uses ESLint and known generated/vendor static asset dirs exist, but ignore coverage appears incomplete for: $($missing -join ', '). Add explicit ESLint ignores or run root lint before push.\"\n }\n else {\n $step[\"status\"] = \"passed\"\n $step[\"exitCode\"] = 0\n $step[\"output\"] = \"Root ESLint lint script has ignore coverage for known generated/vendor static asset dirs: $($assetPatterns -join ', ').\"\n }\n }\n catch {\n $step[\"status\"] = \"manual_required\"\n $step[\"exitCode\"] = 0\n $step[\"output\"] = \"Node lint hygiene check could not complete. Run root lint before push and inspect generated/vendor asset ignores.\"\n $step[\"error\"] = $_.Exception.Message\n }\n finally {\n $finished = Get-Date\n $step[\"finishedAt\"] = $finished.ToString(\"o\")\n $step[\"durationMs\"] = [int]($finished - $started).TotalMilliseconds\n }\n\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n}\n\nfunction New-GitHubSolutionResearchNotApplicable {\n return [ordered]@{\n status = \"not_applicable\"\n required = $false\n path = \"\"\n markdown = \"\"\n reason = \"No blocker category requires GitHub solution research.\"\n candidates = 0\n queries = 0\n evidenceLinks = @()\n exitCriteria = @(\"GitHub research is not required for clean, graph-missing, governance-only, or surgery-plan-only scans.\")\n }\n}\n\nfunction Join-StatusNames {\n param(\n [object[]]$Items,\n [string]$Empty = \"none\"\n )\n\n if ($Items.Count -eq 0) { return $Empty }\n return (($Items | ForEach-Object { \"$($_.name)=$($_.status)\" }) -join \"; \")\n}\n\nfunction Read-JsonFileSafe {\n param([string]$Path)\n\n if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) {\n return $null\n }\n try {\n return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json\n }\n catch {\n return $null\n }\n}\n\nfunction Get-CodeEvidenceLanguage {\n param([string]$Extension)\n\n switch ($Extension.ToLowerInvariant()) {\n \".ps1\" { return \"powershell\" }\n \".psm1\" { return \"powershell\" }\n \".py\" { return \"python\" }\n \".js\" { return \"javascript\" }\n \".jsx\" { return \"javascript\" }\n \".mjs\" { return \"javascript\" }\n \".cjs\" { return \"javascript\" }\n \".ts\" { return \"typescript\" }\n \".tsx\" { return \"typescript\" }\n \".rs\" { return \"rust\" }\n \".go\" { return \"go\" }\n \".java\" { return \"java\" }\n \".cs\" { return \"csharp\" }\n default { return \"text\" }\n }\n}\n\nfunction New-CodeEvidenceNativeSymbol {\n param(\n [string]$RelativePath,\n [string]$Language,\n [int]$LineNumber,\n [string]$Kind,\n [string]$Name\n )\n\n return [ordered]@{\n id = \"$RelativePath#$Kind`:$Name\"\n kind = $Kind\n name = $Name\n file = $RelativePath\n startLine = $LineNumber\n endLine = $LineNumber\n language = $Language\n confidence = 0.55\n source = \"native-minimal\"\n }\n}\n\nfunction Get-CodeEvidencePowerShellSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*function\\s+([A-Za-z0-9_\\-:]+)') {\n return [ordered]@{ kind = \"function\"; name = $Matches[1] }\n }\n return $null\n}\n\nfunction Get-CodeEvidencePythonSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*(def|class)\\s+([A-Za-z_][A-Za-z0-9_]*)') {\n $kind = if ($Matches[1] -eq \"class\") { \"class\" } else { \"function\" }\n return [ordered]@{ kind = $kind; name = $Matches[2] }\n }\n return $null\n}\n\nfunction Get-CodeEvidenceJavaScriptSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*(export\\s+)?(async\\s+)?function\\s+([A-Za-z_$][A-Za-z0-9_$]*)') {\n return [ordered]@{ kind = \"function\"; name = $Matches[3] }\n }\n if ($Line -match '^\\s*(export\\s+)?(const|let|var)\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*=\\s*(async\\s*)?(\\([^)]*\\)|[A-Za-z_$][A-Za-z0-9_$]*)\\s*=>') {\n return [ordered]@{ kind = \"function\"; name = $Matches[3] }\n }\n if ($Line -match '^\\s*(export\\s+)?(class|interface)\\s+([A-Za-z_$][A-Za-z0-9_$]*)') {\n return [ordered]@{ kind = $Matches[2]; name = $Matches[3] }\n }\n return $null\n}\n\nfunction Get-CodeEvidenceRustSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*(pub\\s+)?(async\\s+)?fn\\s+([A-Za-z_][A-Za-z0-9_]*)') {\n return [ordered]@{ kind = \"function\"; name = $Matches[3] }\n }\n return $null\n}\n\nfunction Get-CodeEvidenceGoSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*func\\s+(\\([^)]+\\)\\s*)?([A-Za-z_][A-Za-z0-9_]*)') {\n return [ordered]@{ kind = \"function\"; name = $Matches[2] }\n }\n return $null\n}\n\nfunction Get-CodeEvidenceJavaSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*(public|private|protected)?\\s*(class|interface|enum)\\s+([A-Za-z_][A-Za-z0-9_]*)') {\n return [ordered]@{ kind = $Matches[2]; name = $Matches[3] }\n }\n return $null\n}\n\nfunction Get-CodeEvidenceSymbolCandidate {\n param(\n [string]$Language,\n [string]$Line\n )\n\n switch ($Language) {\n \"powershell\" { return Get-CodeEvidencePowerShellSymbol $Line }\n \"python\" { return Get-CodeEvidencePythonSymbol $Line }\n \"javascript\" { return Get-CodeEvidenceJavaScriptSymbol $Line }\n \"typescript\" { return Get-CodeEvidenceJavaScriptSymbol $Line }\n \"rust\" { return Get-CodeEvidenceRustSymbol $Line }\n \"go\" { return Get-CodeEvidenceGoSymbol $Line }\n \"java\" { return Get-CodeEvidenceJavaSymbol $Line }\n default { return $null }\n }\n}\n\nfunction Get-CodeEvidenceSymbols {\n param(\n [string]$RelativePath,\n [string]$Language,\n [string[]]$Lines\n )\n\n $symbols = New-Object System.Collections.Generic.List[object]\n for ($i = 0; $i -lt $Lines.Count; $i++) {\n $candidate = Get-CodeEvidenceSymbolCandidate -Language $Language -Line ([string]$Lines[$i])\n if ($null -eq $candidate -or [string]::IsNullOrWhiteSpace([string]$candidate[\"name\"])) {\n continue\n }\n\n $symbols.Add((New-CodeEvidenceNativeSymbol `\n -RelativePath $RelativePath `\n -Language $Language `\n -LineNumber ($i + 1) `\n -Kind ([string]$candidate[\"kind\"]) `\n -Name ([string]$candidate[\"name\"])))\n }\n return $symbols.ToArray()\n}\n\nfunction Get-CodeEvidenceImports {\nparam(\n[string]$RelativePath,\n[string]$Language,\n[string[]]$Lines\n )\n\n $imports = New-Object System.Collections.Generic.List[object]\n for ($i = 0; $i -lt $Lines.Count; $i++) {\n $line = [string]$Lines[$i]\n $target = \"\"\n if ($Language -in @(\"javascript\", \"typescript\") -and $line -match 'from\\s+[\"'']([^\"'']+)[\"'']') {\n $target = $Matches[1]\n } elseif ($Language -in @(\"javascript\", \"typescript\") -and $line -match 'require\\([\"'']([^\"'']+)[\"'']\\)') {\n $target = $Matches[1]\n } elseif ($Language -eq \"python\" -and $line -match '^\\s*(from|import)\\s+([A-Za-z0-9_\\.]+)') {\n $target = $Matches[2]\n } elseif ($Language -eq \"rust\" -and $line -match '^\\s*use\\s+([^;]+);') {\n $target = $Matches[1].Trim()\n } elseif ($Language -eq \"go\" -and $line -match '^\\s*import\\s+[\"'']([^\"'']+)[\"'']') {\n $target = $Matches[1]\n } elseif ($line -match '^\\s*#include\\s+[<\"]([^>\"]+)[>\"]') {\n $target = $Matches[1]\n }\n\n if (-not [string]::IsNullOrWhiteSpace($target)) {\n $imports.Add([ordered]@{\n file = $RelativePath\n line = $i + 1\n target = $target\n language = $Language\n confidence = 0.6\n source = \"native-minimal\"\n })\n }\n }\nreturn $imports.ToArray()\n}\n\nfunction New-AgentCodeSliceRanking {\nparam(\n[object[]]$Files,\n[object[]]$Symbols,\n[object[]]$Imports\n)\n\n$symbolsByFile = @{}\nforeach ($symbol in @($Symbols)) {\n$file = [string]$symbol.file\nif ([string]::IsNullOrWhiteSpace($file)) { continue }\nif (-not $symbolsByFile.ContainsKey($file)) {\n$symbolsByFile[$file] = New-Object System.Collections.Generic.List[object]\n}\n$symbolsByFile[$file].Add($symbol)\n}\n\n$importsByFile = @{}\nforeach ($import in @($Imports)) {\n$file = [string]$import.file\nif ([string]::IsNullOrWhiteSpace($file)) { continue }\nif (-not $importsByFile.ContainsKey($file)) {\n$importsByFile[$file] = New-Object System.Collections.Generic.List[object]\n}\n$importsByFile[$file].Add($import)\n}\n\n$rankedFiles = New-Object System.Collections.Generic.List[object]\nforeach ($file in @($Files)) {\n$path = [string]$file.path\nif ([string]::IsNullOrWhiteSpace($path)) { continue }\n\n$reasons = New-Object System.Collections.Generic.List[string]\n$score = 0\n$isTestFile = ($path -match '(test|spec)\\.' -or $path -match '(^|/)(tests?|spec)/')\n$isSupportFile = ($path -match '(^|/)(examples?|fixtures?|demos?|benchmarks?)/')\nif (-not $isTestFile -and -not $isSupportFile -and $path -match '(^|/)(index|main|app|server|cli)\\.') {\n$reasons.Add(\"entrypoint\")\n$score += 40\n}\nif ($isTestFile) {\n$reasons.Add(\"test\")\n}\nif ($symbolsByFile.ContainsKey($path) -and $symbolsByFile[$path].Count -gt 0) {\n$reasons.Add(\"symbols\")\n$score += [Math]::Min(20, 5 * $symbolsByFile[$path].Count)\n}\nif ($importsByFile.ContainsKey($path) -and $importsByFile[$path].Count -gt 0) {\n$reasons.Add(\"imports\")\n$score += [Math]::Min(15, 5 * $importsByFile[$path].Count)\n}\nif ($score -eq 0) {\n$reasons.Add(\"inventory\")\n$score = 1\n}\n\n$rankedFiles.Add([ordered]@{\npath = $path\nlanguage = [string]$file.language\nscore = $score\nreasons = @($reasons.ToArray())\nsymbols = if ($symbolsByFile.ContainsKey($path)) { @($symbolsByFile[$path] | ForEach-Object { $_.name }) } else { @() }\nimports = if ($importsByFile.ContainsKey($path)) { @($importsByFile[$path] | ForEach-Object { $_.target }) } else { @() }\n})\n}\n\n$ordered = @($rankedFiles.ToArray() | Sort-Object -Property @{ Expression = \"score\"; Descending = $true }, @{ Expression = \"path\"; Descending = $false })\nreturn [ordered]@{\nschema = \"agent-code-slice-ranking.v1\"\nstrategy = \"native-evidence-default\"\nfiles = $ordered\n}\n}\n\nfunction Write-CodeEvidenceAgentSlices {\nparam(\n[string]$AgentDir,\n[string]$SliceDir,\n[object[]]$Files,\n[object[]]$Symbols,\n[object[]]$Imports,\n[object]$CocoOutcome\n)\n\n$ranking = New-AgentCodeSliceRanking -Files $Files -Symbols $Symbols -Imports $Imports\n$rankingPath = Join-Path $AgentDir \"ranking.json\"\n$ranking | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $rankingPath -Encoding UTF8\n\n$agentIndexPath = Join-Path $AgentDir \"index.md\"\n@(\n\"# Agent Code Map\",\n\"\",\n\"## Status\",\n\"- Code Evidence Layer: ok\",\n\"- Native minimal layer: enabled\",\n\"- Ranking: [ranking.json](ranking.json)\",\n\"- Native retrieval slice: [native-retrieval](slices/native-retrieval.md)\",\n\"- cocoindex-code adapter: $($CocoOutcome.status) ($($CocoOutcome.reasonCode))\",\n\"\",\n\"## Full Dumps\",\n\"- [files](../full/files.json)\",\n\"- [symbols](../full/symbols.json)\",\n\"- [chunks](../full/chunks.json)\",\n\"- [symbol chunks](../full/symbol-chunks.json)\",\n\"- [imports](../full/imports.json)\",\n\"\",\n\"## Slices\",\n\"- [native retrieval](slices/native-retrieval.md)\",\n\"- [entrypoints](slices/entrypoints.md)\",\n\"- [tests](slices/tests.md)\",\n\"- [risk hotspots](slices/risk-hotspots.md)\"\n) | Set-Content -LiteralPath $agentIndexPath -Encoding UTF8\n\n$topRanked = @($ranking.files | Select-Object -First 20)\n@(\n\"# Native Retrieval Slice\",\n\"\",\n\"- Strategy: native-evidence-default\",\n\"- Source: Code Evidence files/symbols/imports only\",\n\"\",\n\"## Ranked Files\"\n) + @($topRanked | ForEach-Object {\n\"- $($_.path) score=$($_.score) reasons=$(@($_.reasons) -join ',')\"\n}) | Set-Content -LiteralPath (Join-Path $SliceDir \"native-retrieval.md\") -Encoding UTF8\n\n$entrypoints = @($Files | Where-Object { $_.path -match '(^|/)(index|main|app|server|cli)\\.' -and -not ($_.path -match '(test|spec)\\.' -or $_.path -match '(^|/)(tests?|spec)/') -and -not ($_.path -match '(^|/)(examples?|fixtures?|demos?|benchmarks?)/') } | Select-Object -First 20)\n@(\"# Entrypoints\", \"\") + @($entrypoints | ForEach-Object { \"- $($_.path) ($($_.language))\" }) | Set-Content -LiteralPath (Join-Path $SliceDir \"entrypoints.md\") -Encoding UTF8\n\n$tests = @($Files | Where-Object { $_.path -match '(test|spec)\\.' -or $_.path -match '(^|/)(tests?|spec)/' } | Select-Object -First 30)\n@(\"# Tests\", \"\") + @($tests | ForEach-Object { \"- $($_.path) ($($_.language))\" }) | Set-Content -LiteralPath (Join-Path $SliceDir \"tests.md\") -Encoding UTF8\n\n@(\n\"# Risk Hotspots\",\n\"\",\n\"- Native minimal layer does not calculate complexity.\",\n\"- Treat file-sized chunks as fallback evidence until structural chunking is enabled.\",\n\"- cocoindex-code adapter outcome: $($CocoOutcome.status) ($($CocoOutcome.reasonCode)).\"\n) | Set-Content -LiteralPath (Join-Path $SliceDir \"risk-hotspots.md\") -Encoding UTF8\n\nreturn [ordered]@{\nagentIndex = $agentIndexPath\nranking = $rankingPath\nnativeRetrieval = Join-Path $SliceDir \"native-retrieval.md\"\n}\n}\n\nfunction New-CodeEvidenceLayer {\nparam(\n[string]$RepoPath,\n[string]$RunDir,\n[object[]]$Files,\n[object]$CodeEvidenceConfig = $null\n)\n\n$root = Join-Path $RunDir \"code-evidence\"\n$fullDir = Join-Path $root \"merged\\full\"\n$agentDir = Join-Path $root \"merged\\agent\"\n$sliceDir = Join-Path $agentDir \"slices\"\n$adapterDir = Join-Path $root \"adapters\\cocoindex-code\"\nforeach ($dir in @($fullDir, $agentDir, $sliceDir, $adapterDir)) {\nNew-Item -ItemType Directory -Force -Path $dir | Out-Null\n}\n\n$fileRows = New-Object System.Collections.Generic.List[object]\n$symbols = New-Object System.Collections.Generic.List[object]\n$chunks = New-Object System.Collections.Generic.List[object]\n$symbolChunks = New-Object System.Collections.Generic.List[object]\n$imports = New-Object System.Collections.Generic.List[object]\n\nforeach ($file in @($Files)) {\n$fileText = [string]$file\nif ([string]::IsNullOrWhiteSpace($fileText)) { continue }\n$fullPath = if ([System.IO.Path]::IsPathRooted($fileText)) { $fileText } else { Join-Path $RepoPath $fileText }\nif (-not (Test-Path -LiteralPath $fullPath -PathType Leaf)) { continue }\n\n$relativePath = (Get-RelativePathSafe $RepoPath $fullPath).Replace(\"\\\", \"/\")\n$extension = [System.IO.Path]::GetExtension($fullPath)\n$language = Get-CodeEvidenceLanguage -Extension $extension\n$content = Get-Content -LiteralPath $fullPath -Raw -ErrorAction SilentlyContinue\n if ($null -eq $content) { $content = \"\" }\n $lines = if ([string]::IsNullOrEmpty($content)) { @() } else { @($content -split \"`r?`n\") }\n $lines = @($lines)\n $contentBytes = [System.Text.Encoding]::UTF8.GetBytes($content)\n $hashBytes = [System.Security.Cryptography.SHA256]::HashData($contentBytes)\n$hash = [System.BitConverter]::ToString($hashBytes).Replace(\"-\", \"\").ToLowerInvariant()\n\n$fileRows.Add([ordered]@{\npath = $relativePath\nlanguage = $language\nbytes = $contentBytes.Length\nlines = $lines.Count\ntextHash = $hash\nsource = \"native-minimal\"\n})\n\n$fileSymbols = @(Get-CodeEvidenceSymbols -RelativePath $relativePath -Language $language -Lines $lines)\nforeach ($symbol in $fileSymbols) { $symbols.Add($symbol) }\n\n$chunkId = \"$relativePath#file\"\n$chunks.Add([ordered]@{\nid = $chunkId\nfile = $relativePath\nstartLine = 1\nendLine = [Math]::Max(1, $lines.Count)\nkind = \"file\"\ncontainsSymbols = @($fileSymbols | ForEach-Object { $_.id })\ntextHash = $hash\nsource = \"native-minimal\"\n})\n\nforeach ($symbol in $fileSymbols) {\n$symbolChunks.Add([ordered]@{\nsymbolId = $symbol.id\nchunkId = $chunkId\nrelation = \"contained_by\"\nconfidence = 0.55\n})\n}\n\nforeach ($import in @(Get-CodeEvidenceImports -RelativePath $relativePath -Language $language -Lines $lines)) {\n$imports.Add($import)\n}\n}\n\n$fileRowsArray = @($fileRows.ToArray())\n$symbolsArray = @($symbols.ToArray())\n$chunksArray = @($chunks.ToArray())\n$symbolChunksArray = @($symbolChunks.ToArray())\n$importsArray = @($imports.ToArray())\n\n([ordered]@{ schema = \"code-evidence-files.v1\"; files = $fileRowsArray }) | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $fullDir \"files.json\") -Encoding UTF8\n([ordered]@{ schema = \"code-evidence-symbols.v1\"; symbols = $symbolsArray }) | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $fullDir \"symbols.json\") -Encoding UTF8\n([ordered]@{ schema = \"code-evidence-chunks.v1\"; chunks = $chunksArray }) | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $fullDir \"chunks.json\") -Encoding UTF8\n([ordered]@{ schema = \"code-evidence-symbol-chunks.v1\"; mappings = $symbolChunksArray }) | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $fullDir \"symbol-chunks.json\") -Encoding UTF8\n([ordered]@{ schema = \"code-evidence-imports.v1\"; imports = $importsArray }) | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $fullDir \"imports.json\") -Encoding UTF8\n\n# R07 reviewed retirement: this compatibility artifact is a static tombstone.\n# There is intentionally no configuration lookup, executable discovery, or provider invocation.\n$cocoOutcome = [ordered]@{\nschema = \"code-evidence-adapter-outcome.v1\"\nadapter = \"cocoindex-code\"\nenabled = $false\nrequired = $false\nstatus = \"skipped\"\nfatal = $false\nreasonCode = \"reviewed_deletion\"\nreason = \"cocoindex-code is a reviewed retirement tombstone; legacy configuration cannot restore discovery or invocation.\"\ncommand = \"\"\n}\n\n$cocoOutcomePath = Join-Path $adapterDir \"outcome.json\"\n$cocoOutcome | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $cocoOutcomePath -Encoding UTF8\n\n$scorecard = [ordered]@{\nschema = \"code-evidence-scorecard.v1\"\nstatus = \"ok\"\nnativeMinimal = $true\nadapters = @($cocoOutcome)\nmetrics = [ordered]@{\nfiles = $fileRowsArray.Count\nsymbols = $symbolsArray.Count\nchunks = $chunksArray.Count\nimports = $importsArray.Count\nsymbolContainmentRate = if ($symbolsArray.Count -gt 0) { 1.0 } else { $null }\nfallbackChunkRate = 1.0\n}\n}\n$scorecardPath = Join-Path $root \"merged\\scorecard.json\"\n$scorecard | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $scorecardPath -Encoding UTF8\n$scorecardMarkdownPath = Join-Path $root \"merged\\scorecard.md\"\n@(\n\"# Code Evidence Scorecard\",\n\"\",\n\"- Status: ok\",\n\"- Native minimal: true\",\n\"- Files: $($fileRowsArray.Count)\",\n\"- Symbols: $($symbolsArray.Count)\",\n\"- Chunks: $($chunksArray.Count)\",\n\"- Imports: $($importsArray.Count)\",\n\"- cocoindex-code: $($cocoOutcome.status) ($($cocoOutcome.reasonCode))\"\n) | Set-Content -LiteralPath $scorecardMarkdownPath -Encoding UTF8\n\n$agentSlices = Write-CodeEvidenceAgentSlices `\n-AgentDir $agentDir `\n-SliceDir $sliceDir `\n-Files $fileRowsArray `\n-Symbols $symbolsArray `\n-Imports $importsArray `\n-CocoOutcome $cocoOutcome\n\nreturn [ordered]@{\nschema = \"code-evidence-summary.v1\"\nstatus = \"ok\"\nfatal = $false\nroot = $root\nagentIndex = $agentSlices.agentIndex\nscorecard = $scorecardPath\nscorecardMarkdown = $scorecardMarkdownPath\nfiles = $fileRowsArray.Count\nsymbols = $symbolsArray.Count\nchunks = $chunksArray.Count\nimports = $importsArray.Count\nadapters = @($cocoOutcome)\n}\n}\n\nfunction ConvertTo-NullableDouble {\n param([object]$Value)\n\n if ($null -eq $Value) { return $null }\n try {\n return [double]$Value\n }\n catch {\n return $null\n }\n}\n\nfunction Get-SentruxMetricPair {\n param(\n [string]$Output,\n [string]$Label\n )\n\n if ([string]::IsNullOrWhiteSpace($Output)) { return $null }\n $pattern = [regex]::Escape($Label) + \":\\s+([0-9.]+)\\s+[^\\r\\n0-9.]+\\s+([0-9.]+)\"\n $match = [regex]::Match($Output, $pattern)\n if (-not $match.Success) { return $null }\n\n return [ordered]@{\n before = ConvertTo-NullableDouble $match.Groups[1].Value\n after = ConvertTo-NullableDouble $match.Groups[2].Value\n }\n}\n\nfunction New-SentruxMetricDelta {\n param(\n [string]$Name,\n [object]$Before,\n [object]$After,\n [ValidateSet(\"higher_is_better\", \"lower_is_better\")]\n [string]$Polarity = \"lower_is_better\"\n )\n\n $beforeValue = ConvertTo-NullableDouble $Before\n $afterValue = ConvertTo-NullableDouble $After\n $delta = $null\n $direction = \"unknown\"\n $regressed = $false\n\n if ($null -ne $beforeValue -and $null -ne $afterValue) {\n $delta = $afterValue - $beforeValue\n if ([math]::Abs($delta) -lt 0.000001) {\n $direction = \"stable\"\n }\n elseif ($delta -gt 0) {\n $direction = \"up\"\n }\n else {\n $direction = \"down\"\n }\n\n if ($Polarity -eq \"higher_is_better\") {\n $regressed = $delta -lt 0\n }\n else {\n $regressed = $delta -gt 0\n }\n }\n\n return [ordered]@{\n name = $Name\n before = $beforeValue\n after = $afterValue\n delta = $delta\n direction = $direction\n polarity = $Polarity\n regressed = $regressed\n }\n}\n\nfunction Test-SentruxGateNoDegradation {\n param([string]$GateOutput)\n\n return (-not [string]::IsNullOrWhiteSpace($GateOutput) -and $GateOutput -match \"No degradation detected\")\n}\n\nfunction Resolve-SentruxMetricRegressions {\n param(\n [object[]]$Metrics,\n [bool]$NoDegradation\n )\n\n foreach ($metric in @($Metrics)) {\n if ($null -eq $metric) {\n continue\n }\n\n $rawRegressed = [bool]$metric.regressed\n $gateAccepted = $NoDegradation -and $rawRegressed\n $metric | Add-Member -NotePropertyName rawRegressed -NotePropertyValue $rawRegressed -Force\n $metric | Add-Member -NotePropertyName gateAccepted -NotePropertyValue $gateAccepted -Force\n if ($gateAccepted) {\n $metric.regressed = $false\n }\n $metric\n }\n}\n\nfunction New-SentruxInsight {\n param(\n [string]$RepoName,\n [string]$TargetPath,\n [string]$BaselinePath,\n [object[]]$Steps\n )\n\n $gateStep = @($Steps | Where-Object { $_.name -like \"sentrux gate*\" } | Select-Object -Last 1)\n $checkStep = @($Steps | Where-Object { $_.name -eq \"sentrux check\" } | Select-Object -First 1)\n $rulesPath = if ([string]::IsNullOrWhiteSpace($TargetPath)) { \"\" } else { Join-Path (Join-Path $TargetPath \".sentrux\") \"rules.toml\" }\n $baseline = Read-JsonFileSafe $BaselinePath\n $gateOutput = if ($gateStep.Count -gt 0) { [string]$gateStep[0].output } else { \"\" }\n $noDegradation = Test-SentruxGateNoDegradation $gateOutput\n\n $qualityPair = Get-SentruxMetricPair $gateOutput \"Quality\"\n $couplingPair = Get-SentruxMetricPair $gateOutput \"Coupling\"\n $cyclesPair = Get-SentruxMetricPair $gateOutput \"Cycles\"\n $godFilesPair = Get-SentruxMetricPair $gateOutput \"God files\"\n $distance = $null\n $distanceMatch = [regex]::Match($gateOutput, \"Distance from Main Sequence:\\s+([0-9.]+)\")\n if ($distanceMatch.Success) {\n $distance = ConvertTo-NullableDouble $distanceMatch.Groups[1].Value\n }\n\n $scan = [ordered]@{}\n $resolveMatch = [regex]::Match($gateOutput, \"\\[resolve\\]\\s+([0-9]+)\\s+resolved,\\s+([0-9]+)\\s+unresolved\")\n if ($resolveMatch.Success) {\n $scan[\"resolvedImports\"] = [int]$resolveMatch.Groups[1].Value\n $scan[\"unresolvedImports\"] = [int]$resolveMatch.Groups[2].Value\n }\n $graphMatch = [regex]::Match($gateOutput, \"\\[build_graphs\\]\\s+([0-9]+)\\s+files.*\\|\\s+([0-9]+)\\s+import,\\s+([0-9]+)\\s+call,\\s+([0-9]+)\\s+inherit edges\")\n if ($graphMatch.Success) {\n $scan[\"files\"] = [int]$graphMatch.Groups[1].Value\n $scan[\"importEdges\"] = [int]$graphMatch.Groups[2].Value\n $scan[\"callEdges\"] = [int]$graphMatch.Groups[3].Value\n $scan[\"inheritEdges\"] = [int]$graphMatch.Groups[4].Value\n }\n\n $metrics = @()\n if ($null -ne $qualityPair) {\n $metrics += [pscustomobject](New-SentruxMetricDelta \"quality\" $qualityPair[\"before\"] $qualityPair[\"after\"] \"higher_is_better\")\n }\n if ($null -ne $couplingPair) {\n $metrics += [pscustomobject](New-SentruxMetricDelta \"coupling\" $couplingPair[\"before\"] $couplingPair[\"after\"] \"lower_is_better\")\n }\n if ($null -ne $cyclesPair) {\n $metrics += [pscustomobject](New-SentruxMetricDelta \"cycles\" $cyclesPair[\"before\"] $cyclesPair[\"after\"] \"lower_is_better\")\n }\n if ($null -ne $godFilesPair) {\n $metrics += [pscustomobject](New-SentruxMetricDelta \"god_files\" $godFilesPair[\"before\"] $godFilesPair[\"after\"] \"lower_is_better\")\n }\n $metrics = @(Resolve-SentruxMetricRegressions -Metrics $metrics -NoDegradation $noDegradation)\n\n $regressions = @($metrics | Where-Object { $_.regressed })\n $nextActions = @()\n $codeNexusHints = @()\n\n if ([string]::IsNullOrWhiteSpace($TargetPath)) {\n $nextActions += \"Sentrux target was not resolved; inspect pipeline configuration.\"\n }\n elseif (-not (Test-Path -LiteralPath $BaselinePath -PathType Leaf)) {\n $nextActions += \"Create an intentional Sentrux baseline for this scope before using it as a gate.\"\n }\n elseif ($gateStep.Count -gt 0 -and $gateStep[0].status -eq \"failed\") {\n $nextActions += \"Inspect the Sentrux gate output before saving any new baseline.\"\n }\n elseif ($regressions.Count -gt 0) {\n $nextActions += \"Investigate regressed structural metrics before accepting this change.\"\n }\n else {\n $nextActions += \"No structural regression detected for this scope.\"\n }\n\n if (-not [string]::IsNullOrWhiteSpace($rulesPath) -and -not (Test-Path -LiteralPath $rulesPath -PathType Leaf)) {\n $nextActions += \"Add .sentrux/rules.toml when this scope needs explicit architecture boundary rules.\"\n }\n\n if (@($regressions | Where-Object { $_.name -in @(\"coupling\", \"cycles\") }).Count -gt 0) {\n $codeNexusHints += \"Use CodeNexus impact/context on symbols in newly coupled modules.\"\n $codeNexusHints += \"Suggested query: gitnexus query `\"cross module import dependency cycle`\" --repo $RepoName\"\n }\n elseif (@($regressions | Where-Object { $_.name -eq \"quality\" }).Count -gt 0) {\n $codeNexusHints += \"Use CodeNexus query to locate the flow behind the quality drop.\"\n $codeNexusHints += \"Suggested query: gitnexus query `\"complex hotspot structural regression`\" --repo $RepoName\"\n }\n else {\n $codeNexusHints += \"If a future gate regresses, start with CodeNexus context/impact on the changed files.\"\n }\n\n return [ordered]@{\n targetPath = $TargetPath\n baselinePath = $BaselinePath\n baselineExists = (-not [string]::IsNullOrWhiteSpace($BaselinePath) -and (Test-Path -LiteralPath $BaselinePath -PathType Leaf))\n rulesPath = $rulesPath\n rulesExists = (-not [string]::IsNullOrWhiteSpace($rulesPath) -and (Test-Path -LiteralPath $rulesPath -PathType Leaf))\n checkStatus = if ($checkStep.Count -gt 0) { $checkStep[0].status } else { \"not_run\" }\n gateStatus = if ($gateStep.Count -gt 0) { $gateStep[0].status } else { \"not_run\" }\n noDegradation = $noDegradation\n metrics = $metrics\n baseline = [ordered]@{\n qualitySignal = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"quality_signal\")\n couplingScore = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"coupling_score\")\n cycleCount = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"cycle_count\")\n complexFnCount = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"complex_fn_count\")\n crossModuleEdges = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"cross_module_edges\")\n totalImportEdges = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"total_import_edges\")\n }\n distanceFromMainSequence = $distance\n scan = $scan\n regressions = $regressions\n nextActions = $nextActions\n codeNexusHints = $codeNexusHints\n }\n}\n\nfunction Get-StepMatch {\n param(\n [object[]]$Steps,\n [string]$Pattern,\n [switch]$Last\n )\n\n $matches = @($Steps | Where-Object { [string]$_.name -like $Pattern })\n if ($matches.Count -eq 0) { return $null }\n if ($Last) { return $matches[-1] }\n return $matches[0]\n}\n\nfunction Get-StepScore {\n param([object]$Step)\n\n if ($null -eq $Step) { return 0 }\n switch ([string]$Step.status) {\n \"passed\" { return 100 }\n default { return 0 }\n }\n}\n\nfunction Get-FailureCount {\n param(\n [object]$FailureCounts,\n [string]$Name\n )\n\n if ($FailureCounts -is [System.Collections.IDictionary] -and $FailureCounts.Contains($Name)) {\n return [int]$FailureCounts[$Name]\n }\n if ($null -ne $FailureCounts -and $null -ne $FailureCounts.PSObject.Properties[$Name]) {\n return [int]$FailureCounts.$Name\n }\n\n return 0\n}\n\nfunction Get-FirstLine {\n param([string]$Text)\n\n if ([string]::IsNullOrWhiteSpace($Text)) { return \"\" }\n return (($Text -split \"\\r?\\n\") | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1)\n}\n\nfunction New-QualityDimension {\n param(\n [string]$Name,\n [int]$Score,\n [string]$Status,\n [string]$Evidence\n )\n\n return [ordered]@{\n name = $Name\n score = [math]::Max(0, [math]::Min(100, $Score))\n status = $Status\n evidence = $Evidence\n }\n}\n\nfunction New-Modality {\n param(\n [string]$Name,\n [string]$Role,\n [object]$Step,\n [int]$Confidence,\n [string]$Artifact,\n [string]$Finding,\n [string]$Limit\n )\n\n $status = if ($Finding -eq \"not generated\" -and [string]::IsNullOrWhiteSpace($Artifact)) {\n \"missing\"\n }\n elseif ($null -ne $Step) {\n [string]$Step.status\n }\n elseif (-not [string]::IsNullOrWhiteSpace($Artifact)) {\n \"generated\"\n }\n else {\n \"not_run\"\n }\n return [ordered]@{\n name = $Name\n role = $Role\n status = $status\n confidence = [math]::Max(0, [math]::Min(100, $Confidence))\n artifact = $Artifact\n finding = $Finding\n limit = $Limit\n durationMs = if ($null -eq $Step -or $null -eq $Step.durationMs) { $null } else { [int]$Step.durationMs }\n }\n}\n\nfunction New-HospitalProtocol {\n param(\n [string]$Name,\n [string]$Status,\n [string]$Command,\n [string]$ExitCriteria\n )\n\n return [ordered]@{\n name = $Name\n status = $Status\n command = $Command\n exit_criteria = $ExitCriteria\n }\n}\n\nfunction New-StateTransition {\n param(\n [string]$From,\n [string]$To,\n [string]$Guard,\n [bool]$Pass\n )\n\n return [ordered]@{\n from = $From\n to = $To\n guard = $Guard\n pass = $Pass\n }\n}\n\nfunction New-HospitalStateMachine {\n param(\n [object]$FailureCounts,\n [bool]$RulesExists,\n [string]$GateStatus,\n [string]$CheckStatus,\n [int]$FailingWhatIfCount,\n [string]$Disposition,\n [string]$NextProtocol,\n [bool]$StructuralEvidenceComplete = $true,\n [string]$SurgeryTarget = \"\",\n [string]$CurrentTopHotspot = \"\"\n )\n\n # Keep this guard self-contained because the state-machine seam is also\n # extracted independently by the regression harness.\n $providerQuotaCount = 0\n $providerUnavailableCount = 0\n $configErrorCount = 0\n if ($FailureCounts -is [System.Collections.IDictionary] -and $FailureCounts.Contains(\"providerQuota\")) {\n $providerQuotaCount = [int]$FailureCounts[\"providerQuota\"]\n }\n elseif ($null -ne $FailureCounts -and $null -ne $FailureCounts.PSObject.Properties[\"providerQuota\"]) {\n $providerQuotaCount = [int]$FailureCounts.providerQuota\n }\n if ($FailureCounts -is [System.Collections.IDictionary] -and $FailureCounts.Contains(\"providerUnavailable\")) {\n $providerUnavailableCount = [int]$FailureCounts[\"providerUnavailable\"]\n }\n elseif ($null -ne $FailureCounts -and $null -ne $FailureCounts.PSObject.Properties[\"providerUnavailable\"]) {\n $providerUnavailableCount = [int]$FailureCounts.providerUnavailable\n }\n if ($FailureCounts -is [System.Collections.IDictionary] -and $FailureCounts.Contains(\"configError\")) {\n $configErrorCount = [int]$FailureCounts[\"configError\"]\n }\n elseif ($null -ne $FailureCounts -and $null -ne $FailureCounts.PSObject.Properties[\"configError\"]) {\n $configErrorCount = [int]$FailureCounts.configError\n }\n\n $toolsOk = ([int]$FailureCounts.localToolError -eq 0)\n $providerAvailable = ($providerQuotaCount -eq 0 -and $providerUnavailableCount -eq 0 -and $configErrorCount -eq 0)\n $graphOk = ([int]$FailureCounts.graphMissing -eq 0)\n $sentruxOk = ([int]$FailureCounts.sentruxFail -eq 0 -and $RulesExists -and $GateStatus -eq \"passed\" -and $CheckStatus -eq \"passed\")\n $surgeryDebtCleared = ($StructuralEvidenceComplete -and $FailingWhatIfCount -eq 0)\n\n # surgery_plan -> post_op: the surgery target has actually been treated\n # (it no longer shows up as the current top hotspot) and sentrux confirms\n # the governed scope is clean, so it is safe to move on to post-op review.\n $surgeryTargetResolved = (-not [string]::IsNullOrWhiteSpace($SurgeryTarget) -and\n -not [string]::IsNullOrWhiteSpace($CurrentTopHotspot) -and\n ($SurgeryTarget -ne $CurrentTopHotspot))\n $surgeryToPostOpOk = ($sentruxOk -and $StructuralEvidenceComplete -and $surgeryTargetResolved)\n $postOpOk = ($toolsOk -and $providerAvailable -and $graphOk -and $sentruxOk -and $surgeryDebtCleared -and $surgeryTargetResolved)\n\n $currentState = switch ($NextProtocol) {\n \"triage\" { \"triage\" }\n \"diagnose\" { \"diagnose\" }\n \"govern\" { \"govern\" }\n \"surgery_plan\" { \"surgery_plan\" }\n \"post_op\" { if ($Disposition -eq \"discharge_ready\") { \"discharge_ready\" } else { \"post_op\" } }\n default { \"triage\" }\n }\n\n return [ordered]@{\n schema = \"code-intel-hospital-state-machine.v1\"\n current_state = $currentState\n disposition = $Disposition\n next_protocol = $NextProtocol\n states = @(\"triage\", \"diagnose\", \"govern\", \"surgery_plan\", \"post_op\", \"discharge_ready\")\n transitions = @(\n (New-StateTransition \"triage\" \"diagnose\" \"local toolchain is available\" $toolsOk)\n (New-StateTransition \"diagnose\" \"govern\" \"architecture graph exists or graph absence is accepted\" $graphOk)\n (New-StateTransition \"govern\" \"surgery_plan\" \"rules and gate pass, but what-if still has planned debt\" ($sentruxOk -and -not $surgeryDebtCleared))\n (New-StateTransition \"govern\" \"post_op\" \"rules and gate pass, no planned surgery debt remains\" ($sentruxOk -and $surgeryDebtCleared))\n (New-StateTransition \"surgery_plan\" \"post_op\" \"sentrux gate/check pass and the surgery target no longer appears as the current top hotspot\" $surgeryToPostOpOk)\n (New-StateTransition \"post_op\" \"discharge_ready\" \"post-op verification passes with no regressions\" $postOpOk)\n )\n guards = [ordered]@{\n tools_ok = $toolsOk\n provider_available = $providerAvailable\n graph_ok = $graphOk\n rules_exists = $RulesExists\n sentrux_check = $CheckStatus\n sentrux_gate = $GateStatus\n sentrux_ok = $sentruxOk\n failing_what_if = $FailingWhatIfCount\n structural_evidence_complete = $StructuralEvidenceComplete\n surgery_debt_cleared = $surgeryDebtCleared\n surgery_target = $SurgeryTarget\n current_top_hotspot = $CurrentTopHotspot\n surgery_target_resolved = $surgeryTargetResolved\n surgery_to_post_op_ok = $surgeryToPostOpOk\n post_op_ok = $postOpOk\n }\n }\n}\n\nfunction Read-JsonPathIfExists {\n param([string]$Path)\n\n if ([string]::IsNullOrWhiteSpace($Path)) { return $null }\n if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }\n\n return Read-JsonFileSafe $Path\n}\n\nfunction Get-SourceAnchorText {\n param([object]$SourceAnchor)\n\n if ($null -eq $SourceAnchor) { return \"\" }\n if ($SourceAnchor -is [string]) { return [string]$SourceAnchor }\n if ($null -ne $SourceAnchor.label) { return [string]$SourceAnchor.label }\n if ($null -ne $SourceAnchor.path) { return [string]$SourceAnchor.path }\n\n return [string]$SourceAnchor\n}\n\nfunction New-CodeIntelSurgeryPlan {\n param(\n [object]$Hospital,\n [string]$RepoPath,\n [string]$SentruxTargetPath,\n [string]$HotspotsPath,\n [string]$WhatIfPath,\n [string]$CodeNexusPath\n )\n\n $hotspots = Read-JsonPathIfExists $HotspotsPath\n $whatIf = Read-JsonPathIfExists $WhatIfPath\n $codeNexus = Read-JsonPathIfExists $CodeNexusPath\n\n $primaryFunction = $null\n if ($null -ne $hotspots -and $null -ne $hotspots.functions -and @($hotspots.functions).Count -gt 0) {\n $primaryFunction = $hotspots.functions[0]\n }\n $primaryFile = $null\n if ($null -ne $hotspots -and $null -ne $hotspots.files -and @($hotspots.files).Count -gt 0) {\n $primaryFile = $hotspots.files[0]\n }\n $primaryScenario = $null\n $failingScenarios = @()\n if ($null -ne $whatIf -and $null -ne $whatIf.scenarios) {\n $failingScenarios = @($whatIf.scenarios | Where-Object { -not $_.pass })\n if ($failingScenarios.Count -gt 0) { $primaryScenario = $failingScenarios[0] }\n }\n $contextFile = $null\n if ($null -ne $codeNexus -and $null -ne $codeNexus.files -and @($codeNexus.files).Count -gt 0) {\n $contextFile = $codeNexus.files[0]\n }\n\n $targetFile = if ($null -ne $primaryFunction) { [string]$primaryFunction.file } elseif ($null -ne $primaryFile) { [string]$primaryFile.path } else { \"\" }\n $targetName = if ($null -ne $primaryFunction) { [string]$primaryFunction.name } elseif ($null -ne $primaryFile) { [string]$primaryFile.path } else { \"\" }\n $targetAnchor = if ($null -ne $primaryFunction) { Get-SourceAnchorText $primaryFunction.sourceAnchor } elseif ($null -ne $primaryFile) { Get-SourceAnchorText $primaryFile.sourceAnchor } else { \"\" }\n $targetComplexity = if ($null -ne $primaryFunction) { [int]$primaryFunction.complexity } elseif ($null -ne $primaryFile) { [int]$primaryFile.maxComplexity } else { $null }\n $scenarioName = if ($null -ne $primaryScenario) { [string]$primaryScenario.name } else { \"\" }\n $scenarioAction = if ($null -ne $primaryScenario) { [string]$primaryScenario.action } else { \"\" }\n $status = if ([string]$Hospital.triage.next_protocol -eq \"surgery_plan\" -or\n ([string]$Hospital.triage.disposition -eq \"admit\" -and -not [string]::IsNullOrWhiteSpace($targetFile))) {\n \"planned\"\n }\n else {\n \"not_required\"\n }\n\n return [ordered]@{\n schema = \"code-intel-surgery-plan.v1\"\n status = $status\n repo = $RepoPath\n scope = $SentruxTargetPath\n admission = [ordered]@{\n disposition = $Hospital.triage.disposition\n diagnosis = $Hospital.triage.primary_diagnosis\n reason = $Hospital.triage.admission_reason\n }\n primary_target = [ordered]@{\n file = $targetFile\n name = $targetName\n source_anchor = $targetAnchor\n complexity = $targetComplexity\n scenario = $scenarioName\n scenario_action = $scenarioAction\n codenexus_file = if ($null -ne $contextFile) { [string]$contextFile.path } else { \"\" }\n }\n operating_plan = @(\n \"Open the primary target and its CodeNexus context before editing.\",\n \"Reduce the selected hotspot by extraction, boundary clarification, or testable decomposition.\",\n \"Do not raise Sentrux thresholds to make the surgery pass.\",\n \"Add or update the smallest test that proves the behavior stayed intact.\"\n )\n verification = @(\n \"Invoke-SentruxAgentTool.ps1 check_rules `\"$SentruxTargetPath`\"\",\n \"Invoke-SentruxAgentTool.ps1 session_end `\"$SentruxTargetPath`\"\",\n \"scripts/tests/test-code-intel-pipeline.ps1 -RepoPath `\"$RepoPath`\" -SentruxPath `\"$((Get-RelativePathSafe $RepoPath $SentruxTargetPath) -replace '\\\\', '/')`\" -SkipRepowise -Mode normal\"\n )\n discharge_criteria = $Hospital.triage.discharge_criteria\n evidence = [ordered]@{\n hotspots = $HotspotsPath\n what_if = $WhatIfPath\n codenexus = $CodeNexusPath\n failing_scenarios = @($failingScenarios | Select-Object -First 5)\n }\n }\n}\n\nfunction Convert-SurgeryPlanToMarkdown {\n param([object]$Plan)\n\n $lines = @(\n \"# Code Intel Surgery Plan\",\n \"\",\n \"- Status: $($Plan.status)\",\n \"- Repo: $($Plan.repo)\",\n \"- Scope: $($Plan.scope)\",\n \"- Diagnosis: $($Plan.admission.diagnosis)\",\n \"- Admission reason: $($Plan.admission.reason)\",\n \"\",\n \"## Primary Target\",\n \"- File: $($Plan.primary_target.file)\",\n \"- Symbol: $($Plan.primary_target.name)\",\n \"- Anchor: $($Plan.primary_target.source_anchor)\",\n \"- Complexity: $($Plan.primary_target.complexity)\",\n \"- Scenario: $($Plan.primary_target.scenario)\",\n \"- Action: $($Plan.primary_target.scenario_action)\",\n \"- CodeNexus file: $($Plan.primary_target.codenexus_file)\",\n \"\",\n \"## Operating Plan\"\n )\n foreach ($item in @($Plan.operating_plan)) {\n $lines += \"- $item\"\n }\n $lines += \"\"\n $lines += \"## Verification\"\n foreach ($item in @($Plan.verification)) {\n $lines += \"- ``$item``\"\n }\n $lines += \"\"\n $lines += \"## Discharge Criteria\"\n foreach ($item in @($Plan.discharge_criteria)) {\n $lines += \"- $item\"\n }\n return $lines\n}\n\nfunction Get-HospitalDiagnosis {\n param(\n [object]$FailureCounts,\n [bool]$RulesExists,\n [int]$FailingWhatIfCount\n )\n\n $providerQuotaCount = Get-FailureCount $FailureCounts \"providerQuota\"\n $providerUnavailableCount = Get-FailureCount $FailureCounts \"providerUnavailable\"\n $configErrorCount = Get-FailureCount $FailureCounts \"configError\"\n\n if ($FailureCounts.localToolError -gt 0) {\n return [ordered]@{ severity = \"red\"; primaryDiagnosis = \"local tool failure\" }\n }\n if ($providerQuotaCount -gt 0) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"provider quota exhausted\" }\n }\n if ($providerUnavailableCount -gt 0) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"provider unavailable\" }\n }\n if ($configErrorCount -gt 0) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"provider configuration error\" }\n }\n if ($FailureCounts.sentruxFail -gt 0) {\n return [ordered]@{ severity = \"red\"; primaryDiagnosis = \"architecture gate failure\" }\n }\n if ($FailureCounts.graphMissing -gt 0) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"architecture graph missing\" }\n }\n if (-not $RulesExists) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"ungoverned structural scope\" }\n }\n if ($FailingWhatIfCount -gt 0) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"known modernization debt\" }\n }\n\n return [ordered]@{ severity = \"green\"; primaryDiagnosis = \"clean snapshot\" }\n}\n\nfunction Get-HospitalNextProtocol {\n param(\n [object]$FailureCounts,\n [bool]$RulesExists,\n [int]$FailingWhatIfCount,\n [object]$GitHubResearch\n )\n\n $providerQuotaCount = Get-FailureCount $FailureCounts \"providerQuota\"\n $providerUnavailableCount = Get-FailureCount $FailureCounts \"providerUnavailable\"\n $configErrorCount = Get-FailureCount $FailureCounts \"configError\"\n\n if ($FailureCounts.localToolError -gt 0) { return \"triage\" }\n if ($providerQuotaCount -gt 0) { return \"triage\" }\n if ($providerUnavailableCount -gt 0) { return \"triage\" }\n if ($configErrorCount -gt 0) { return \"triage\" }\n if ($null -ne $GitHubResearch -and [bool]$GitHubResearch.required) { return \"github_solution_research\" }\n if ($FailureCounts.graphMissing -gt 0) { return \"diagnose\" }\n if (-not $RulesExists) { return \"govern\" }\n if ($FailingWhatIfCount -gt 0) { return \"surgery_plan\" }\n\n return \"post_op\"\n}\n\nfunction Get-HospitalAdmissionReason {\n param([string]$PrimaryDiagnosis)\n\n switch ($PrimaryDiagnosis) {\n \"clean snapshot\" { return \"No active inpatient issue; ready for discharge after post-op verification.\" }\n \"architecture graph missing\" { return \"Admit for diagnostic imaging: Understand graph is missing or stale.\" }\n \"ungoverned structural scope\" { return \"Admit for governance: rules are missing for the selected scope.\" }\n \"known modernization debt\" { return \"Admit for planned surgery: what-if scenarios show debt that should be scheduled, not ignored.\" }\n \"architecture gate failure\" { return \"Admit for structural treatment: Sentrux gate or rules failed.\" }\n \"provider quota exhausted\" { return \"Admit for triage: provider quota prevented complete evidence collection.\" }\n \"provider unavailable\" { return \"Admit for triage: the configured upstream provider route or model was unavailable.\" }\n \"provider configuration error\" { return \"Admit for triage: provider credentials, endpoint, or model configuration must be corrected.\" }\n \"structural evidence incomplete\" { return \"Admit for diagnosis: required structural summaries are incomplete.\" }\n \"local tool failure\" { return \"Admit for triage: local toolchain failed before diagnosis can be trusted.\" }\n default { return \"Admit until the next protocol clears the diagnosis.\" }\n }\n}\n\nfunction Get-HospitalTreatmentPlan {\n param(\n [object]$FailureCounts,\n [bool]$RulesExists,\n [int]$FailingWhatIfCount,\n [string]$UnderstandCommand,\n [string]$TopContextFile\n )\n\n $providerQuotaCount = Get-FailureCount $FailureCounts \"providerQuota\"\n $providerUnavailableCount = Get-FailureCount $FailureCounts \"providerUnavailable\"\n $configErrorCount = Get-FailureCount $FailureCounts \"configError\"\n\n $treatment = @()\n if ($FailureCounts.localToolError -gt 0) { $treatment += \"Fix local tool errors before interpreting architecture signals.\" }\n if ($providerQuotaCount -gt 0) { $treatment += \"Restore provider quota or use a complete local evidence path before interpreting the result.\" }\n if ($providerUnavailableCount -gt 0) { $treatment += \"Verify the provider model catalog and route availability; keep local index-only evidence available.\" }\n if ($configErrorCount -gt 0) { $treatment += \"Correct provider endpoint, model, or credential configuration before retrying provider-backed docs.\" }\n if ($FailureCounts.graphMissing -gt 0) { $treatment += \"Refresh Understand graph with: $UnderstandCommand\" }\n if (-not $RulesExists) { $treatment += \"Add .sentrux/rules.toml for the chosen scope.\" }\n if ($FailingWhatIfCount -gt 0) { $treatment += \"Use what-if failures as the tightening roadmap; start with the first failing scenario.\" }\n if (-not [string]::IsNullOrWhiteSpace($TopContextFile)) { $treatment += \"Start CodeNexus review at $TopContextFile.\" }\n if ($treatment.Count -eq 0) { $treatment += \"Keep this artifact as the current clean snapshot and compare the next session against it.\" }\n\n return $treatment\n}\n\nfunction New-HospitalDecisionBlock {\n param(\n [object]$FailureCounts,\n [bool]$RulesExists,\n [string]$GateStatus,\n [string]$CheckStatus,\n [int]$FailingWhatIfCount,\n [string]$UnderstandCommand,\n [string]$TopContextFile,\n [bool]$StructuralEvidenceComplete = $false,\n [string]$SurgeryTarget = \"\",\n [string]$CurrentTopHotspot = \"\",\n [object]$GitHubResearch\n )\n\n $diagnosis = Get-HospitalDiagnosis $FailureCounts $RulesExists $FailingWhatIfCount\n $nextProtocol = Get-HospitalNextProtocol $FailureCounts $RulesExists $FailingWhatIfCount $GitHubResearch\n $sentruxVerified = ($RulesExists -and $GateStatus -eq \"passed\" -and $CheckStatus -eq \"passed\")\n if ($diagnosis.severity -eq \"green\" -and -not $sentruxVerified) {\n $hasExplicitFailure = ($GateStatus -eq \"failed\" -or $CheckStatus -eq \"failed\")\n $diagnosis = [ordered]@{\n severity = if ($hasExplicitFailure) { \"red\" } else { \"amber\" }\n primaryDiagnosis = if ($hasExplicitFailure) { \"architecture gate failure\" } else { \"architecture verification incomplete\" }\n }\n $nextProtocol = \"govern\"\n }\n elseif ($diagnosis.severity -eq \"green\" -and -not $StructuralEvidenceComplete) {\n $diagnosis = [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"structural evidence incomplete\" }\n $nextProtocol = \"diagnose\"\n }\n $postOpResolved = (-not [string]::IsNullOrWhiteSpace($SurgeryTarget) -and\n -not [string]::IsNullOrWhiteSpace($CurrentTopHotspot) -and\n $SurgeryTarget -ne $CurrentTopHotspot)\n $disposition = if ($diagnosis.severity -ne \"green\") {\n \"admit\"\n }\n elseif ($sentruxVerified -and $StructuralEvidenceComplete -and $postOpResolved) {\n \"discharge_ready\"\n }\n else {\n \"observe\"\n }\n $admissionReason = Get-HospitalAdmissionReason $diagnosis.primaryDiagnosis\n $dischargeCriteria = @(\n \"failure category counters are zero\",\n \"Sentrux check and gate pass for the governed scope\",\n \"hospital triage status is green or explicitly accepted for observation\",\n \"session_end reports no quality regression after Agent edits\"\n )\n if ($null -ne $GitHubResearch -and [bool]$GitHubResearch.required) {\n $dischargeCriteria += \"GitHub evidence linked or GitHub evidence insufficiency recorded in github-solution-research artifacts\"\n }\n $treatment = Get-HospitalTreatmentPlan $FailureCounts $RulesExists $FailingWhatIfCount $UnderstandCommand $TopContextFile\n if (-not $sentruxVerified) {\n $treatment = @($treatment) + \"Obtain passing Sentrux check and gate evidence before discharge.\"\n }\n\n $stateMachine = New-HospitalStateMachine `\n -FailureCounts $FailureCounts `\n -RulesExists $RulesExists `\n -GateStatus $GateStatus `\n -CheckStatus $CheckStatus `\n -FailingWhatIfCount $FailingWhatIfCount `\n -Disposition $disposition `\n -NextProtocol $nextProtocol `\n -StructuralEvidenceComplete $StructuralEvidenceComplete `\n -SurgeryTarget $SurgeryTarget `\n -CurrentTopHotspot $CurrentTopHotspot\n\n return [ordered]@{\n severity = $diagnosis.severity\n primaryDiagnosis = $diagnosis.primaryDiagnosis\n nextProtocol = $nextProtocol\n disposition = $disposition\n admissionReason = $admissionReason\n dischargeCriteria = $dischargeCriteria\n treatment = $treatment\n stateMachine = $stateMachine\n }\n}\n\nfunction New-HospitalFindings {\n param(\n [int]$InventoryFiles,\n [object]$SentruxFileDetailsSummary,\n [string]$TopFunction,\n [string]$TopModule,\n [object]$ResolvedRatio,\n [int]$ResolvedImports,\n [int]$UnresolvedImports,\n [int]$ExcludedFiles\n )\n\n $findings = @()\n if ($InventoryFiles -gt 0) { $findings += \"X-ray inventory found $InventoryFiles files.\" }\n if ($null -ne $SentruxFileDetailsSummary) { $findings += \"CT structural scan found $($SentruxFileDetailsSummary.files) files and $($SentruxFileDetailsSummary.functions) functions.\" }\n if (-not [string]::IsNullOrWhiteSpace($TopFunction)) { $findings += \"Top surgical hotspot: $TopFunction.\" }\n if (-not [string]::IsNullOrWhiteSpace($TopModule)) { $findings += \"Top module hotspot: $TopModule.\" }\n if ($ResolvedRatio -ne $null) { $findings += \"Import resolution ratio is $ResolvedRatio% ($ResolvedImports resolved, $UnresolvedImports unresolved).\" }\n if ($ExcludedFiles -gt 0) { $findings += \"$ExcludedFiles files were quarantined from governed source metrics.\" }\n return $findings\n}\n\nfunction New-HospitalModalities {\n param(\n [object]$InventoryStep,\n [object]$UnderstandStep,\n [object]$RepowiseStep,\n [object]$SentruxCheckStep,\n [object]$SentruxGateStep,\n [int]$GraphScore,\n [int]$MemoryScore,\n [int]$MriScore,\n [string]$MriStatus,\n [int]$CtScore,\n [string]$CtStatus,\n [int]$PetScore,\n [string]$PetStatus,\n [int]$GovernanceScore,\n [string]$RunDir,\n [string]$RepoPath,\n [int]$InventoryFiles,\n [object]$SentruxDsmSummary,\n [object]$SentruxFileDetailsSummary,\n [object]$CodeNexusContextSummary,\n [object]$SentruxWhatIfSummary,\n [object]$RuntimeCiSummary,\n [string]$GovernanceArtifact,\n [string]$GovernanceFinding\n )\n\n $xrayFinding = if ($InventoryFiles -gt 0) { \"$InventoryFiles files inventoried\" } else { \"no inventory\" }\n $ctArtifact = if ($CtStatus -eq \"available\") { [string]$SentruxDsmSummary.path } else { \"\" }\n $ctFinding = if ($CtStatus -eq \"available\") { \"$($SentruxDsmSummary.modules) modules, $($SentruxFileDetailsSummary.functions) functions\" } else { \"not generated\" }\n $mriArtifact = if ($MriStatus -eq \"available\") { [string]$CodeNexusContextSummary.path } else { \"\" }\n $mriFinding = if ($MriStatus -eq \"available\") { \"$($CodeNexusContextSummary.files) files, $($CodeNexusContextSummary.references) references\" } else { \"not generated\" }\n $petArtifact = if ($null -ne $RuntimeCiSummary) { [string]$RuntimeCiSummary.path } elseif ($PetStatus -eq \"available\") { [string]$SentruxWhatIfSummary.path } else { \"\" }\n $petFinding = if ($null -ne $RuntimeCiSummary) { \"runtime/CI health=$($RuntimeCiSummary.health); freshness=$($RuntimeCiSummary.freshness); completeness=$($RuntimeCiSummary.completeness)\" } elseif ($PetStatus -eq \"available\") { \"$($SentruxWhatIfSummary.failing) failing what-if scenarios\" } else { \"not generated\" }\n $petLimitation = if ($null -ne $RuntimeCiSummary) { \"Provider-neutral runtime/CI evidence is cited; provider logs remain outside this report.\" } else { \"No live runtime trace is captured yet.\" }\n $chartFinding = if ($null -ne $RepowiseStep) { [string]$RepowiseStep.status } else { \"not run\" }\n\n return @(\n (New-Modality \"xray\" \"fast file inventory and repo surface\" $InventoryStep (Get-StepScore $InventoryStep) (Join-Path $RunDir \"files.txt\") $xrayFinding \"Sees files, not semantic impact.\")\n (New-Modality \"anatomy\" \"Understand Anything architecture graph\" $UnderstandStep $GraphScore (Join-Path (Join-Path $RepoPath \".understand-anything\") \"knowledge-graph.json\") (Get-FirstLine ([string]$UnderstandStep.output)) \"Requires a prebuilt graph from the Understand tool.\")\n (New-Modality \"ct\" \"Sentrux DSM, hotspots, and structural slices\" $SentruxGateStep $CtScore $ctArtifact $ctFinding \"Static structure is not runtime truth.\")\n (New-Modality \"mri\" \"CodeNexus context and impact localization\" $null $MriScore $mriArtifact $mriFinding \"Lite mode is local evidence, not a full semantic backend.\")\n (New-Modality \"pet\" \"runtime/CI evidence with test gaps, evolution, and what-if fallback\" $null $PetScore $petArtifact $petFinding $petLimitation)\n (New-Modality \"chart\" \"Repowise long-term project memory\" $RepowiseStep $MemoryScore \"\" $chartFinding \"Provider quota and index freshness can limit semantic memory.\")\n (New-Modality \"governance\" \"rules, gate, and session safety rails\" $SentruxCheckStep $GovernanceScore $GovernanceArtifact $GovernanceFinding \"Rules only protect boundaries that have been encoded.\")\n )\n}\n\nfunction New-HospitalQualityDimensions {\n param(\n [int]$SourceCoverageScore,\n [string]$SourceScopeStatus,\n [int]$InventoryFiles,\n [int]$ScanFiles,\n [int]$GraphScore,\n [object]$UnderstandStep,\n [int]$ResolutionScore,\n [string]$ImportResolutionStatus,\n [int]$ResolvedImports,\n [int]$UnresolvedImports,\n [int]$PollutionScore,\n [string]$PollutionStatus,\n [int]$ExcludedFiles,\n [int]$GovernanceScore,\n [string]$GovernanceStatus,\n [string]$GovernanceEvidence,\n [int]$MriScore,\n [string]$LocalizationStatus,\n [string]$TopContextFile,\n [int]$MemoryScore,\n [string]$MemoryStatus,\n [string]$MemoryEvidence\n )\n\n return @(\n (New-QualityDimension \"source_coverage\" $SourceCoverageScore $SourceScopeStatus \"inventory=$InventoryFiles; sentrux_scan=$ScanFiles\")\n (New-QualityDimension \"graph_freshness\" $GraphScore ([string]$UnderstandStep.status) (Get-FirstLine ([string]$UnderstandStep.output)))\n (New-QualityDimension \"import_resolution\" $ResolutionScore $ImportResolutionStatus \"resolved=$ResolvedImports; unresolved=$UnresolvedImports\")\n (New-QualityDimension \"pollution_control\" $PollutionScore $PollutionStatus \"excluded=$ExcludedFiles\")\n (New-QualityDimension \"governance\" $GovernanceScore $GovernanceStatus $GovernanceEvidence)\n (New-QualityDimension \"localization\" $MriScore $LocalizationStatus \"top_file=$TopContextFile\")\n (New-QualityDimension \"memory\" $MemoryScore $MemoryStatus $MemoryEvidence)\n )\n}\n\nfunction Read-HospitalArtifactFile {\n param([object]$Summary)\n\n if ($null -eq $Summary) { return $null }\n\n $path = [string]$Summary.path\n if ([string]::IsNullOrWhiteSpace($path)) { return $null }\n if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null }\n\n return Read-JsonFileSafe $path\n}\n\nfunction Read-HospitalArtifacts {\n param(\n [object]$SentruxDsmSummary,\n [object]$SentruxFileDetailsSummary,\n [object]$SentruxHotspotsSummary,\n [object]$SentruxEvolutionSummary,\n [object]$SentruxWhatIfSummary,\n [object]$CodeNexusContextSummary\n )\n\n return [ordered]@{\n dsm = Read-HospitalArtifactFile $SentruxDsmSummary\n file_details = Read-HospitalArtifactFile $SentruxFileDetailsSummary\n hotspots = Read-HospitalArtifactFile $SentruxHotspotsSummary\n evolution = Read-HospitalArtifactFile $SentruxEvolutionSummary\n what_if = Read-HospitalArtifactFile $SentruxWhatIfSummary\n codenexus = Read-HospitalArtifactFile $CodeNexusContextSummary\n }\n}\n\nfunction New-HospitalMeasurements {\n param(\n [object]$InventoryStep,\n [object]$SentruxInsight,\n [object]$DsmObject\n )\n\n $inventoryFiles = 0\n $inventoryMatch = [regex]::Match([string]$InventoryStep.output, \"files=([0-9]+)\")\n if ($inventoryMatch.Success) { $inventoryFiles = [int]$inventoryMatch.Groups[1].Value }\n\n $scan = if ($null -ne $SentruxInsight -and $null -ne $SentruxInsight[\"scan\"]) { $SentruxInsight[\"scan\"] } else { @{} }\n $scanFiles = if ($scan.Contains(\"files\")) { [int]$scan[\"files\"] } else { 0 }\n $unresolvedImports = if ($scan.Contains(\"unresolvedImports\")) { [int]$scan[\"unresolvedImports\"] } else { 0 }\n $resolvedImports = if ($scan.Contains(\"resolvedImports\")) { [int]$scan[\"resolvedImports\"] } else { 0 }\n $totalImports = $resolvedImports + $unresolvedImports\n $resolvedRatio = if ($totalImports -gt 0) { [math]::Round(($resolvedImports * 100.0) / $totalImports, 1) } else { $null }\n $dsmScope = $null\n if ($DsmObject -is [System.Collections.IDictionary] -and $DsmObject.Contains(\"scope\")) {\n $dsmScope = $DsmObject[\"scope\"]\n }\n elseif ($null -ne $DsmObject -and $null -ne $DsmObject.PSObject.Properties[\"scope\"]) {\n $dsmScope = $DsmObject.scope\n }\n\n $excludedFilesValue = $null\n $hasPollutionEvidence = $false\n if ($dsmScope -is [System.Collections.IDictionary] -and $dsmScope.Contains(\"excluded_files\")) {\n $excludedFilesValue = $dsmScope[\"excluded_files\"]\n $hasPollutionEvidence = ($null -ne $excludedFilesValue)\n }\n elseif ($null -ne $dsmScope -and $null -ne $dsmScope.PSObject.Properties[\"excluded_files\"]) {\n $excludedFilesValue = $dsmScope.excluded_files\n $hasPollutionEvidence = ($null -ne $excludedFilesValue)\n }\n\n $excludedFiles = if ($hasPollutionEvidence) { [int]$excludedFilesValue } else { 0 }\n $sourceScopeStatus = if ($inventoryFiles -gt 0 -and $scanFiles -gt 0) { \"measured\" } else { \"unknown\" }\n $pollutionStatus = if (-not $hasPollutionEvidence) { \"unknown\" } elseif ($excludedFiles -gt 0) { \"quarantined\" } else { \"clean\" }\n\n return [ordered]@{\n inventory_files = $inventoryFiles\n scan_files = $scanFiles\n unresolved_imports = $unresolvedImports\n resolved_imports = $resolvedImports\n resolved_ratio = $resolvedRatio\n excluded_files = $excludedFiles\n source_scope_status = $sourceScopeStatus\n pollution_status = $pollutionStatus\n }\n}\n\nfunction Get-ImportResolutionScore {\n param([object]$ResolvedRatio)\n\n if ($null -eq $ResolvedRatio) { return 0 }\n if ($ResolvedRatio -ge 75) { return 100 }\n if ($ResolvedRatio -ge 50) { return 75 }\n if ($ResolvedRatio -ge 25) { return 50 }\n\n return 30\n}\n\nfunction Get-SourceCoverageScore {\n param(\n [int]$ScanFiles,\n [int]$InventoryFiles\n )\n\n if ($ScanFiles -le 0 -or $InventoryFiles -le 0) { return 0 }\n\n return [int][math]::Round([math]::Min(100.0, ($ScanFiles * 100.0) / $InventoryFiles))\n}\n\nfunction New-HospitalScoreBlock {\n param(\n [object]$SentruxInsight,\n [object]$Measurements,\n [object]$UnderstandStep,\n [object]$RepowiseStep,\n [object]$SentruxCheckStep,\n [object]$SentruxGateStep,\n [object]$SentruxDsmObject,\n [object]$SentruxFileDetailsObject,\n [object]$SentruxEvolutionObject,\n [object]$SentruxWhatIfObject,\n [object]$CodeNexusContextObject,\n [object]$RuntimeCiSummary\n )\n\n $rulesExists = [bool]$SentruxInsight[\"rulesExists\"]\n $rulesScore = if ($rulesExists) { 100 } else { 45 }\n $gateScore = Get-StepScore $SentruxGateStep\n $checkScore = Get-StepScore $SentruxCheckStep\n $graphScore = Get-StepScore $UnderstandStep\n $memoryScore = Get-StepScore $RepowiseStep\n $mriStatus = if ($null -ne $CodeNexusContextObject) { \"available\" } else { \"missing\" }\n $ctStatus = if ($null -ne $SentruxDsmObject -and $null -ne $SentruxFileDetailsObject) { \"available\" } else { \"missing\" }\n $petStatus = if ($null -ne $RuntimeCiSummary) { if ([string]$RuntimeCiSummary.health -eq \"unknown\") { \"unknown\" } else { \"available\" } } elseif ($null -ne $SentruxWhatIfObject -and $null -ne $SentruxEvolutionObject) { \"available\" } else { \"missing\" }\n $mriScore = if ($mriStatus -eq \"available\") { 100 } else { 0 }\n $ctScore = if ($ctStatus -eq \"available\") { 100 } else { 0 }\n $petScore = if ($null -ne $RuntimeCiSummary) { switch ([string]$RuntimeCiSummary.health) { \"green\" { 100 } \"red\" { 0 } default { 30 } } } elseif ($petStatus -eq \"available\") { 70 } else { 0 }\n $resolutionScore = Get-ImportResolutionScore $Measurements.resolved_ratio\n $pollutionStatus = [string]$Measurements.pollution_status\n $pollutionScore = if ($pollutionStatus -eq \"unknown\") { 0 } elseif ($Measurements.excluded_files -gt 0) { 100 } else { 80 }\n $governanceScore = [int][math]::Round(($rulesScore + $gateScore + $checkScore) / 3.0)\n $diagnosticScore = [int][math]::Round(($ctScore + $mriScore + $graphScore + $memoryScore) / 4.0)\n $overallScore = [int][math]::Round(($diagnosticScore + $governanceScore + $resolutionScore + $pollutionScore) / 4.0)\n $governanceArtifact = if ($rulesExists) { [string]$SentruxInsight[\"rulesPath\"] } else { \"\" }\n $resolvedRatio = $Measurements.resolved_ratio\n\n return [ordered]@{\n rules_exists = $rulesExists\n gate_status = [string]$SentruxInsight[\"gateStatus\"]\n check_status = [string]$SentruxInsight[\"checkStatus\"]\n graph_score = $graphScore\n memory_score = $memoryScore\n mri_score = $mriScore\n mri_status = $mriStatus\n ct_score = $ctScore\n ct_status = $ctStatus\n pet_score = $petScore\n pet_status = $petStatus\n resolution_score = $resolutionScore\n pollution_score = $pollutionScore\n governance_score = $governanceScore\n diagnostic_score = $diagnosticScore\n overall_score = $overallScore\n source_coverage_score = Get-SourceCoverageScore $Measurements.scan_files $Measurements.inventory_files\n import_resolution_status = if ($null -eq $resolvedRatio) { \"unknown\" } else { \"$resolvedRatio%\" }\n pollution_status = $pollutionStatus\n governance_status = if ($rulesExists) { \"rules_present\" } else { \"rules_missing\" }\n governance_artifact = $governanceArtifact\n governance_finding = \"rules=$($SentruxInsight['rulesExists']); gate=$($SentruxInsight['gateStatus']); check=$($SentruxInsight['checkStatus'])\"\n governance_evidence = \"gate=$($SentruxInsight['gateStatus']); check=$($SentruxInsight['checkStatus'])\"\n localization_status = $mriStatus\n memory_status = if ($null -ne $RepowiseStep) { [string]$RepowiseStep.status } else { \"not_run\" }\n memory_evidence = if ($null -ne $RepowiseStep) { Get-FirstLine ([string]$RepowiseStep.output) } else { \"\" }\n }\n}\n\nfunction New-HospitalEvidenceBlock {\n param(\n [object]$HotspotsObject,\n [object]$WhatIfObject,\n [object]$CodeNexusContextSummary\n )\n\n $failingWhatIf = @()\n if ($null -ne $WhatIfObject -and $null -ne $WhatIfObject.scenarios) {\n $failingWhatIf = @($WhatIfObject.scenarios | Where-Object { -not $_.pass })\n }\n\n $topFunction = \"\"\n if ($null -ne $HotspotsObject -and $null -ne $HotspotsObject.functions -and @($HotspotsObject.functions).Count -gt 0) {\n $topFunction = \"{0} in {1} (cc={2})\" -f $HotspotsObject.functions[0].name, $HotspotsObject.functions[0].file, $HotspotsObject.functions[0].complexity\n }\n\n $topModule = \"\"\n if ($null -ne $HotspotsObject -and $null -ne $HotspotsObject.modules -and @($HotspotsObject.modules).Count -gt 0) {\n $topModule = \"{0} (risk={1})\" -f $HotspotsObject.modules[0].name, $HotspotsObject.modules[0].risk\n }\n\n return [ordered]@{\n failing_what_if = $failingWhatIf\n top_function = $topFunction\n top_module = $topModule\n top_context_file = if ($null -ne $CodeNexusContextSummary) { [string]$CodeNexusContextSummary.topFile } else { \"\" }\n }\n}\n\nfunction New-HospitalProtocolBlock {\n param(\n [bool]$RulesExists,\n [int]$FailingWhatIfCount\n )\n\n $governProtocolStatus = if ($RulesExists) { \"active\" } else { \"needs_rules\" }\n $surgeryProtocolStatus = if ($FailingWhatIfCount -gt 0) { \"available\" } else { \"low_risk\" }\n\n return @(\n (New-HospitalProtocol \"triage\" \"available\" \"run-code-intel.ps1 -RepoPath -Mode lite\" \"Classify provider/tool/graph/Sentrux failure bucket and choose next protocol.\")\n (New-HospitalProtocol \"diagnose\" \"available\" \"run-code-intel.ps1 -RepoPath -Mode normal\" \"Produce summary.md, hospital.md, sentrux artifacts, and codenexus context.\")\n (New-HospitalProtocol \"govern\" $governProtocolStatus \"sentrux check ; sentrux gate \" \"Rules pass and gate reports no degradation.\")\n (New-HospitalProtocol \"surgery_plan\" $surgeryProtocolStatus \"read sentrux-what-if.json and codenexus-context.json\" \"Choose one hotspot, one boundary, and one verification command before editing.\")\n (New-HospitalProtocol \"post_op\" \"available\" \"Invoke-SentruxAgentTool.ps1 session_end \" \"Signal does not drop, rules pass, and touched hotspot is lower risk.\")\n )\n}\n\nfunction Get-PreviousSurgeryTarget {\n param([string]$RunDir)\n\n if ([string]::IsNullOrWhiteSpace($RunDir)) { return \"\" }\n $repoArtifactRoot = Split-Path -Parent $RunDir\n if ([string]::IsNullOrWhiteSpace($repoArtifactRoot) -or -not (Test-Path -LiteralPath $repoArtifactRoot -PathType Container)) { return \"\" }\n\n $currentName = Split-Path -Leaf $RunDir\n $previousRun = Get-ChildItem -LiteralPath $repoArtifactRoot -Directory -ErrorAction SilentlyContinue |\n Where-Object { $_.Name -ne $currentName } |\n Sort-Object Name -Descending |\n Select-Object -First 1\n if ($null -eq $previousRun) { return \"\" }\n\n $previousPlanPath = Join-Path $previousRun.FullName \"surgery-plan.json\"\n if (-not (Test-Path -LiteralPath $previousPlanPath -PathType Leaf)) { return \"\" }\n\n $previousPlan = Read-JsonFileSafe $previousPlanPath\n if ($null -eq $previousPlan -or $null -eq $previousPlan.primary_target) { return \"\" }\n if ([string]::IsNullOrWhiteSpace([string]$previousPlan.primary_target.name)) { return \"\" }\n\n return \"$($previousPlan.primary_target.name) in $($previousPlan.primary_target.file)\"\n}\n\nfunction New-CodeIntelHospitalReport {\n param(\n [string]$RepoPath,\n [string]$Mode,\n [string]$RunDir,\n [string]$ReportPath,\n [string]$SummaryPath,\n [string]$UnderstandingPath,\n [object[]]$Steps,\n [object]$FailureCounts,\n [object]$SentruxInsight,\n [object]$SentruxDsmSummary,\n [object]$SentruxFileDetailsSummary,\n [object]$SentruxHotspotsSummary,\n[object]$SentruxEvolutionSummary,\n[object]$SentruxWhatIfSummary,\n[object]$CodeNexusContextSummary,\n[object]$RuntimeCiSummary,\n[string]$UnderstandCommand,\n[object]$ToolState,\n[object]$GitHubResearch\n)\n\n $gitStep = Get-StepMatch $Steps \"git status\"\n $inventoryStep = Get-StepMatch $Steps \"rg file inventory\"\n $understandStep = Get-StepMatch $Steps \"understand graph\"\n $repowiseStep = Get-StepMatch $Steps \"repowise*\" -Last\n $sentruxCheckStep = Get-StepMatch $Steps \"sentrux check\"\n $sentruxGateStep = Get-StepMatch $Steps \"sentrux gate*\" -Last\n\n $artifacts = Read-HospitalArtifacts $SentruxDsmSummary $SentruxFileDetailsSummary $SentruxHotspotsSummary $SentruxEvolutionSummary $SentruxWhatIfSummary $CodeNexusContextSummary\n $structuralEvidenceComplete = ($null -ne $artifacts.dsm -and\n $null -ne $artifacts.file_details -and\n $null -ne $artifacts.hotspots -and\n $null -ne $artifacts.evolution -and\n $null -ne $artifacts.what_if)\n $measurements = New-HospitalMeasurements $inventoryStep $SentruxInsight $artifacts.dsm\n $scores = New-HospitalScoreBlock `\n -SentruxInsight $SentruxInsight `\n -Measurements $measurements `\n -UnderstandStep $understandStep `\n -RepowiseStep $repowiseStep `\n -SentruxCheckStep $sentruxCheckStep `\n -SentruxGateStep $sentruxGateStep `\n -SentruxDsmObject $artifacts.dsm `\n -SentruxFileDetailsObject $artifacts.file_details `\n -SentruxEvolutionObject $artifacts.evolution `\n -SentruxWhatIfObject $artifacts.what_if `\n -CodeNexusContextObject $artifacts.codenexus `\n -RuntimeCiSummary $RuntimeCiSummary\n $evidence = New-HospitalEvidenceBlock $artifacts.hotspots $artifacts.what_if $CodeNexusContextSummary\n\n $currentTopHotspot = \"\"\n if ($null -ne $artifacts.hotspots -and $null -ne $artifacts.hotspots.functions -and @($artifacts.hotspots.functions).Count -gt 0) {\n $topFn = $artifacts.hotspots.functions[0]\n $currentTopHotspot = \"$($topFn.name) in $($topFn.file)\"\n }\n $surgeryTarget = Get-PreviousSurgeryTarget $RunDir\n\n $decision = New-HospitalDecisionBlock `\n -FailureCounts $FailureCounts `\n -RulesExists $scores.rules_exists `\n -GateStatus $scores.gate_status `\n -CheckStatus $scores.check_status `\n -FailingWhatIfCount @($evidence.failing_what_if).Count `\n -UnderstandCommand $UnderstandCommand `\n -TopContextFile $evidence.top_context_file `\n -StructuralEvidenceComplete $structuralEvidenceComplete `\n -SurgeryTarget $surgeryTarget `\n -CurrentTopHotspot $currentTopHotspot `\n -GitHubResearch $GitHubResearch\n\n $findings = New-HospitalFindings `\n -InventoryFiles $measurements.inventory_files `\n -SentruxFileDetailsSummary $SentruxFileDetailsSummary `\n -TopFunction $evidence.top_function `\n -TopModule $evidence.top_module `\n -ResolvedRatio $measurements.resolved_ratio `\n -ResolvedImports $measurements.resolved_imports `\n -UnresolvedImports $measurements.unresolved_imports `\n -ExcludedFiles $measurements.excluded_files\n\n $modalities = New-HospitalModalities `\n -InventoryStep $inventoryStep `\n -UnderstandStep $understandStep `\n -RepowiseStep $repowiseStep `\n -SentruxCheckStep $sentruxCheckStep `\n -SentruxGateStep $sentruxGateStep `\n -GraphScore $scores.graph_score `\n -MemoryScore $scores.memory_score `\n -MriScore $scores.mri_score `\n -MriStatus $scores.mri_status `\n -CtScore $scores.ct_score `\n -CtStatus $scores.ct_status `\n -PetScore $scores.pet_score `\n -PetStatus $scores.pet_status `\n -GovernanceScore $scores.governance_score `\n -RunDir $RunDir `\n -RepoPath $RepoPath `\n -InventoryFiles $measurements.inventory_files `\n -SentruxDsmSummary $SentruxDsmSummary `\n -SentruxFileDetailsSummary $SentruxFileDetailsSummary `\n -CodeNexusContextSummary $CodeNexusContextSummary `\n -SentruxWhatIfSummary $SentruxWhatIfSummary `\n -RuntimeCiSummary $RuntimeCiSummary `\n -GovernanceArtifact $scores.governance_artifact `\n -GovernanceFinding $scores.governance_finding\n\n $quality = New-HospitalQualityDimensions `\n -SourceCoverageScore $scores.source_coverage_score `\n -SourceScopeStatus $measurements.source_scope_status `\n -InventoryFiles $measurements.inventory_files `\n -ScanFiles $measurements.scan_files `\n -GraphScore $scores.graph_score `\n -UnderstandStep $understandStep `\n -ResolutionScore $scores.resolution_score `\n -ImportResolutionStatus $scores.import_resolution_status `\n -ResolvedImports $measurements.resolved_imports `\n -UnresolvedImports $measurements.unresolved_imports `\n -PollutionScore $scores.pollution_score `\n -PollutionStatus $scores.pollution_status `\n -ExcludedFiles $measurements.excluded_files `\n -GovernanceScore $scores.governance_score `\n -GovernanceStatus $scores.governance_status `\n -GovernanceEvidence $scores.governance_evidence `\n -MriScore $scores.mri_score `\n -LocalizationStatus $scores.localization_status `\n -TopContextFile $evidence.top_context_file `\n -MemoryScore $scores.memory_score `\n -MemoryStatus $scores.memory_status `\n -MemoryEvidence $scores.memory_evidence\n\n $protocols = New-HospitalProtocolBlock $scores.rules_exists @($evidence.failing_what_if).Count\n\n return [ordered]@{\n schema = \"code-intel-hospital.v1\"\n generatedAt = (Get-Date).ToString(\"o\")\n repo = $RepoPath\n mode = $Mode\n artifacts = [ordered]@{\n runDir = $RunDir\n report = $ReportPath\n summary = $SummaryPath\n understanding = $UnderstandingPath\n runtime_ci = if ($null -ne $RuntimeCiSummary) { [string]$RuntimeCiSummary.path } else { \"\" }\n github_solution_research = if ($null -ne $GitHubResearch) { [string]$GitHubResearch.path } else { \"\" }\n github_solution_research_markdown = if ($null -ne $GitHubResearch) { [string]$GitHubResearch.markdown } else { \"\" }\n }\n triage = [ordered]@{\n status = $decision.severity\n disposition = $decision.disposition\n primary_diagnosis = $decision.primaryDiagnosis\n overall_score = $scores.overall_score\n next_protocol = $decision.nextProtocol\n research_status = if ($null -ne $GitHubResearch) { [string]$GitHubResearch.status } else { \"not_applicable\" }\n research_required = if ($null -ne $GitHubResearch) { [bool]$GitHubResearch.required } else { $false }\n exit_criteria = if ($null -ne $GitHubResearch) { @($GitHubResearch.exitCriteria) } else { @() }\n admission_reason = $decision.admissionReason\n discharge_criteria = $decision.dischargeCriteria\n }\n state_machine = $decision.stateMachine\n modalities = $modalities\n policies = [ordered]@{\n admission = [ordered]@{\n admit_when = @(\n \"local toolchain fails\",\n \"architecture graph is missing\",\n \"Sentrux rules are missing\",\n \"Sentrux check or gate fails\",\n \"what-if reports planned modernization debt\"\n )\n current_reason = $decision.admissionReason\n }\n discharge = [ordered]@{\n criteria = $decision.dischargeCriteria\n current_state = $decision.stateMachine.current_state\n }\n }\n report_quality = [ordered]@{\n overall_score = $scores.overall_score\n diagnostic_score = $scores.diagnostic_score\n governance_score = $scores.governance_score\n dimensions = $quality\n }\n diagnosis = [ordered]@{\n findings = $findings\n impression = $decision.primaryDiagnosis\n risk = $decision.severity\n evidence = [ordered]@{\n top_function = $evidence.top_function\n top_module = $evidence.top_module\n top_context_file = $evidence.top_context_file\n failing_what_if = @($evidence.failing_what_if | Select-Object -First 5)\n }\n }\n treatment = [ordered]@{\n plan = $decision.treatment\n follow_up = @(\n \"Rerun normal mode after code changes.\",\n \"Compare hospital-report.json overall_score and Sentrux quality signal.\",\n \"Use session_start/session_end around Agent edits.\"\n )\n }\n protocols = $protocols\n tools = $ToolState\n }\n}\n\nfunction Convert-HospitalReportToMarkdown {\n param([object]$Hospital)\n\n $lines = @(\n \"# Code Intel Hospital Report\",\n \"\",\n \"- Repo: $($Hospital.repo)\",\n \"- Mode: $($Hospital.mode)\",\n \"- Status: $($Hospital.triage.status)\",\n \"- Disposition: $($Hospital.triage.disposition)\",\n \"- Primary diagnosis: $($Hospital.triage.primary_diagnosis)\",\n \"- Admission reason: $($Hospital.triage.admission_reason)\",\n\"- Overall score: $($Hospital.triage.overall_score)\",\n\"- Next protocol: $($Hospital.triage.next_protocol)\",\n\"- Research status: $($Hospital.triage.research_status)\",\n\"- Research required: $($Hospital.triage.research_required)\",\n\"- Current state: $($Hospital.state_machine.current_state)\",\n\"\",\n\"## Imaging Modalities\"\n)\nif ($null -ne $Hospital.triage.exit_criteria -and @($Hospital.triage.exit_criteria).Count -gt 0) {\n $lines += \"\"\n $lines += \"## Exit Criteria\"\n foreach ($criterion in @($Hospital.triage.exit_criteria)) {\n $lines += \"- $criterion\"\n }\n}\nforeach ($item in @($Hospital.modalities)) {\n $lines += \"- $($item.name): $($item.status), confidence=$($item.confidence), finding=$($item.finding)\"\n }\n $lines += \"\"\n $lines += \"## Report Quality\"\n foreach ($dimension in @($Hospital.report_quality.dimensions)) {\n $lines += \"- $($dimension.name): $($dimension.score) ($($dimension.status)) - $($dimension.evidence)\"\n }\n $lines += \"\"\n $lines += \"## Diagnosis\"\n foreach ($finding in @($Hospital.diagnosis.findings)) {\n $lines += \"- $finding\"\n }\n $lines += \"\"\n $lines += \"## Treatment\"\n foreach ($item in @($Hospital.treatment.plan)) {\n $lines += \"- $item\"\n }\n if ($null -ne $Hospital.surgery_plan) {\n $lines += \"\"\n $lines += \"## Surgery Plan\"\n $lines += \"- Status: $($Hospital.surgery_plan.status)\"\n $lines += \"- Report: $($Hospital.surgery_plan.path)\"\n $lines += \"- Markdown: $($Hospital.surgery_plan.markdown)\"\n $lines += \"- Primary target: $($Hospital.surgery_plan.primary_target)\"\n }\n $lines += \"\"\n $lines += \"## Discharge Criteria\"\n foreach ($item in @($Hospital.triage.discharge_criteria)) {\n $lines += \"- $item\"\n }\n $lines += \"\"\n $lines += \"## State Machine\"\n foreach ($transition in @($Hospital.state_machine.transitions)) {\n $lines += \"- $($transition.from) -> $($transition.to): pass=$($transition.pass), guard=$($transition.guard)\"\n }\n $lines += \"\"\n $lines += \"## Protocols\"\n foreach ($protocol in @($Hospital.protocols)) {\n $lines += \"- $($protocol.name): $($protocol.status) - $($protocol.exit_criteria)\"\n }\n return $lines\n}\n\nfunction Get-CodeIntelSentruxStep {\n param(\n [object[]]$Steps,\n [string]$NamePattern,\n [switch]$Last\n )\n\n $matches = @($Steps | Where-Object { [string]$_.name -like $NamePattern })\n if ($matches.Count -eq 0) { return $null }\n if ($Last) { return $matches[-1] }\n return $matches[0]\n}\n\nfunction Get-CodeIntelBoundedExcerpt {\n param(\n [string]$Text,\n [int]$MaxLength = 500\n )\n\n if ([string]::IsNullOrWhiteSpace($Text)) { return \"\" }\n $singleLine = (($Text -split \"`r?`n\") | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 8) -join \" | \"\n if ($singleLine.Length -le $MaxLength) { return $singleLine }\n return $singleLine.Substring(0, $MaxLength)\n}\n\nfunction New-CodeIntelSentruxTarget {\n param(\n [ValidateSet(\"resolved\", \"unresolved\", \"aggregate\", \"not_applicable\")]\n [string]$Status,\n [string]$File = \"\",\n [string]$Symbol = \"\"\n )\n\n $target = [ordered]@{ status = $Status }\n if (-not [string]::IsNullOrWhiteSpace($File)) { $target[\"file\"] = $File }\n if (-not [string]::IsNullOrWhiteSpace($Symbol)) { $target[\"symbol\"] = $Symbol }\n return $target\n}\n\nfunction New-CodeIntelSentruxRecord {\n param(\n [string]$Id,\n [string]$Kind,\n [string]$Source,\n [string]$SourceStep,\n [string]$RawOutputPath,\n [string]$Stdout,\n [object]$Target,\n [string]$Metric = \"\",\n [Nullable[int]]$Value = $null,\n [Nullable[int]]$Threshold = $null,\n [Nullable[int]]$Before = $null,\n [Nullable[int]]$After = $null\n )\n\n $record = [ordered]@{\n id = $Id\n kind = $Kind\n source = $Source\n source_step = $SourceStep\n provenance = \"stdout\"\n raw_output_path = $RawOutputPath\n stdout_excerpt = Get-CodeIntelBoundedExcerpt $Stdout\n parsed_at = (Get-Date).ToString(\"o\")\n target = $Target\n }\n if (-not [string]::IsNullOrWhiteSpace($Metric)) { $record[\"metric\"] = $Metric }\n if ($null -ne $Value) { $record[\"value\"] = [int]$Value }\n if ($null -ne $Threshold) { $record[\"threshold\"] = [int]$Threshold }\n if ($null -ne $Before) { $record[\"before\"] = [int]$Before }\n if ($null -ne $After) { $record[\"after\"] = [int]$After }\n return $record\n}\n\nfunction Get-CodeIntelObjectValue {\n param(\n [object]$Object,\n [string]$Name\n )\n\n if ($null -eq $Object) { return $null }\n if ($Object -is [System.Collections.IDictionary] -and $Object.Contains($Name)) {\n return $Object[$Name]\n }\n return Get-JsonProperty $Object $Name\n}\n\nfunction New-CodeIntelSentruxConflict {\n param(\n [object]$Authoritative,\n [object]$Conflicting,\n [string]$ConflictingSource,\n [string]$RawPointer\n )\n\n if ($null -eq $Authoritative -or $null -eq $Conflicting) { return $null }\n $authoritativeValue = ConvertTo-NullableDouble (Get-CodeIntelObjectValue $Authoritative \"value\")\n $conflictingValue = ConvertTo-NullableDouble (Get-CodeIntelObjectValue $Conflicting \"complexity\")\n if ($null -eq $authoritativeValue -or $null -eq $conflictingValue) { return $null }\n if ([int]$authoritativeValue -eq [int]$conflictingValue) { return $null }\n\n $conflictingId = \"{0}:max_cc:{1}:{2}\" -f $ConflictingSource, [string](Get-CodeIntelObjectValue $Conflicting \"file\"), [string](Get-CodeIntelObjectValue $Conflicting \"name\")\n return [ordered]@{\n kind = \"metric_conflict\"\n authoritative_record_id = [string](Get-CodeIntelObjectValue $Authoritative \"id\")\n conflicting_record_id = $conflictingId\n metric = \"cyclomatic_complexity\"\n authoritative_value = [int]$authoritativeValue\n conflicting_value = [int]$conflictingValue\n authoritative_source = [string](Get-CodeIntelObjectValue $Authoritative \"source\")\n conflicting_source = $ConflictingSource\n raw_output_path = $RawPointer\n stdout_excerpt = Get-CodeIntelBoundedExcerpt (\"{0} {1} (cc={2})\" -f [string](Get-CodeIntelObjectValue $Conflicting \"name\"), [string](Get-CodeIntelObjectValue $Conflicting \"file\"), [string](Get-CodeIntelObjectValue $Conflicting \"complexity\"))\n parsed_at = (Get-Date).ToString(\"o\")\n resolution = \"authoritative_stdout_wins\"\n }\n}\n\nfunction New-CodeIntelSentruxFailures {\n param(\n [object[]]$Steps,\n [string]$OutputPath = \"\",\n [string]$HotspotsPath = \"\",\n [string]$FileDetailsPath = \"\"\n )\n\n $checkStep = Get-CodeIntelSentruxStep -Steps $Steps -NamePattern \"sentrux check\"\n $gateStep = Get-CodeIntelSentruxStep -Steps $Steps -NamePattern \"sentrux gate*\" -Last\n $records = [System.Collections.Generic.List[object]]::new()\n $parserNotes = [System.Collections.Generic.List[string]]::new()\n $parserErrors = [System.Collections.Generic.List[string]]::new()\n\n if ($null -ne $checkStep) {\n $checkStatus = [string]$checkStep.status\n $checkText = (([string]$checkStep.output) + \"`n\" + ([string]$checkStep.error)).Trim()\n if ($checkStatus -eq \"failed\" -or $checkStatus -eq \"manual_required\") {\n $namedMatches = @([regex]::Matches($checkText, \"(?im)(?[^\\s:()]+(?:\\.ps1|\\.psm1|\\.ts|\\.tsx|\\.js|\\.jsx|\\.py|\\.rs|\\.go|\\.cs|\\.java|\\.kt|\\.v)):(?[A-Za-z_][A-Za-z0-9_.:-]*)\\s*\\(cc=(?\\d+)\\)\"))\n if ($namedMatches.Count -gt 0) {\n foreach ($match in $namedMatches) {\n $file = [string]$match.Groups[\"file\"].Value\n $symbol = [string]$match.Groups[\"symbol\"].Value\n $value = [int]$match.Groups[\"cc\"].Value\n $records.Add((New-CodeIntelSentruxRecord `\n -Id (\"check:max_cc:{0}:{1}\" -f $file, $symbol) `\n -Kind \"max_cc\" `\n -Source \"sentrux check\" `\n -SourceStep \"sentrux check\" `\n -RawOutputPath \"report.json#/steps/sentrux check/output\" `\n -Stdout $checkText `\n -Metric \"cyclomatic_complexity\" `\n -Value $value `\n -Threshold 70 `\n -Target (New-CodeIntelSentruxTarget -Status \"resolved\" -File $file -Symbol $symbol)))\n }\n }\n elseif ($checkText -match \"(?i)max[_ -]?cc|cyclomatic|complex\") {\n $value = $null\n $valueMatch = [regex]::Match($checkText, \"(?i)(?:max[_ -]?cc|cc|cyclomatic[^0-9]*)(?:\\D+)(?\\d+)\")\n if ($valueMatch.Success) { $value = [int]$valueMatch.Groups[\"cc\"].Value }\n $records.Add((New-CodeIntelSentruxRecord `\n -Id \"check:max_cc:unresolved\" `\n -Kind \"max_cc\" `\n -Source \"sentrux check\" `\n -SourceStep \"sentrux check\" `\n -RawOutputPath \"report.json#/steps/sentrux check/output\" `\n -Stdout $checkText `\n -Metric \"cyclomatic_complexity\" `\n -Value $value `\n -Threshold 70 `\n -Target (New-CodeIntelSentruxTarget -Status \"unresolved\")))\n }\n else {\n $parserErrors.Add(\"sentrux check failed but stdout did not match known max_cc formats.\")\n }\n }\n }\n\n if ($null -ne $gateStep) {\n $gateStatus = [string]$gateStep.status\n $gateText = (([string]$gateStep.output) + \"`n\" + ([string]$gateStep.error)).Trim()\n if ($gateStatus -eq \"failed\" -or $gateStatus -eq \"manual_required\") {\n $gateMatches = @([regex]::Matches($gateText, \"(?im)(?