extensions/specrew-speckit/scripts/resolve-quality-profile.ps1

[CmdletBinding()]
param(
    [string]$ProjectPath = (Get-Location).Path,
    [string]$FeaturePath,
    [string]$SpecPath,
    [ValidateSet('Object', 'Json', 'Markdown')]
    [string]$OutputFormat = 'Object'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$sharedGovernancePath = Join-Path $PSScriptRoot 'shared-governance.ps1'
if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) {
    throw "Shared governance helper not found at '$sharedGovernancePath'."
}
. $sharedGovernancePath

function Test-AnyPattern {
    param(
        [AllowEmptyString()]
        [string]$Text,

        [string[]]$Patterns
    )

    if ([string]::IsNullOrWhiteSpace($Text)) {
        return $false
    }

    foreach ($pattern in $Patterns) {
        if ($Text -match $pattern) {
            return $true
        }
    }

    return $false
}

function Add-UniqueItem {
    param(
        [System.Collections.Generic.List[string]]$List,
        [AllowEmptyString()]
        [string]$Value
    )

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return
    }

    if (-not $List.Contains($Value)) {
        $null = $List.Add($Value)
    }
}

function Add-UniqueItems {
    param(
        [System.Collections.Generic.List[string]]$List,
        [string[]]$Values
    )

    foreach ($value in $Values) {
        Add-UniqueItem -List $List -Value $value
    }
}

function Get-DependencyNames {
    param([string]$PackageJsonPath)

    if (-not (Test-Path -LiteralPath $PackageJsonPath -PathType Leaf)) {
        return @()
    }

    try {
        $packageJson = Get-Content -LiteralPath $PackageJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json -AsHashtable
    }
    catch {
        return @()
    }

    $dependencies = [System.Collections.Generic.List[string]]::new()
    foreach ($propertyName in @('dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies')) {
        if (-not $packageJson.ContainsKey($propertyName)) {
            continue
        }

        $propertyValue = $packageJson[$propertyName]
        if ($propertyValue -is [System.Collections.IDictionary]) {
            foreach ($name in $propertyValue.Keys) {
                Add-UniqueItem -List $dependencies -Value ([string]$name).ToLowerInvariant()
            }
        }
    }

    return $dependencies.ToArray()
}

function Get-TextFileContent {
    param([string]$Path)

    if (-not [string]::IsNullOrWhiteSpace($Path) -and (Test-Path -LiteralPath $Path -PathType Leaf)) {
        return Get-Content -LiteralPath $Path -Raw -Encoding UTF8
    }

    return ''
}

function New-QualityGate {
    param(
        [string]$GateId,
        [string]$Category,
        [string[]]$RequirementRefs,
        [string]$EvidenceRef,
        [string]$Description
    )

    return [pscustomobject]@{
        gate_id          = $GateId
        category         = $Category
        requirement_refs = @($RequirementRefs)
        status           = 'planned'
        evidence_ref     = $EvidenceRef
        exception_ref    = $null
        description      = $Description
    }
}

function New-StackSurface {
    param(
        [string]$SurfaceId,
        [string[]]$PathGlobs,
        [string]$Language,
        [string]$RuntimeShape,
        [string]$RecognizedStack,
        [string[]]$MatchedSignals
    )

    return [pscustomobject]@{
        surface_id       = $SurfaceId
        path_globs       = @($PathGlobs)
        language         = $Language
        runtime_shape    = $RuntimeShape
        recognized_stack = $RecognizedStack
        matched_signals  = @($MatchedSignals)
    }
}

function New-RiskDimension {
    param(
        [string]$Id,
        [string]$Status,
        [string]$Rationale
    )

    return [pscustomobject]@{
        id        = $Id
        status    = $Status
        rationale = $Rationale
    }
}

function New-HardeningFocusArea {
    param(
        [string]$FocusArea,
        [string]$WhyItMatters,
        [string]$PlannedArtifactOrEvidence,
        [string]$Status
    )

    return [pscustomobject]@{
        focus_area                   = $FocusArea
        why_it_matters              = $WhyItMatters
        planned_artifact_or_evidence = $PlannedArtifactOrEvidence
        status                      = $Status
    }
}

function New-LensActivationPlanEntry {
    param(
        [string]$LensRef,
        [string]$Activation,
        [string]$Rationale,
        [string]$PlannedEvidencePath,
        [string]$RequestedReviewClass
    )

    return [pscustomobject]@{
        lens_ref               = $LensRef
        activation             = $Activation
        rationale              = $Rationale
        planned_evidence_path  = $PlannedEvidencePath
        requested_review_class = $RequestedReviewClass
    }
}

function New-RoutingPolicyEntry {
    param(
        [string]$LensScope,
        [string]$RequestedReviewClass,
        [string]$EffectiveClass,
        [string]$OverrideApprovalRecord,
        [string]$Notes
    )

    return [pscustomobject]@{
        lens_scope               = $LensScope
        requested_review_class   = $RequestedReviewClass
        effective_class          = $EffectiveClass
        override_approval_record = $OverrideApprovalRecord
        notes                    = $Notes
    }
}

function Get-BaselineRiskDimensions {
    return @(
        'code-quality',
        'design-quality-and-separation-of-concerns',
        'verification-confidence',
        'maintainability',
        'security',
        'robustness'
    )
}

function Get-DefaultLensRefs {
    return @(
        'security-baseline@v1.0.0',
        'robustness-baseline@v1.0.0',
        'test-integrity@v1.0.0'
    )
}

function Get-PhaseOneDeferrals {
    return @(
        'Pre-implementation hardening gate sign-off and blocking semantics remain deferred to Phase 2+.',
        'Dedicated bug-hunter lens execution and strongest-class routing remain deferred to Phase 2+.',
        'Quality-drift logic, mixed-stack override routing, and reference-implementation comparison remain deferred to Phase 2+.'
    )
}

function Convert-ToRepoMarkdownPath {
    param(
        [string]$ResolvedProjectPath,
        [string]$Path
    )

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return $null
    }

    try {
        $relativePath = [System.IO.Path]::GetRelativePath($ResolvedProjectPath, $Path)
        return ($relativePath -replace '\\', '/')
    }
    catch {
        return ($Path -replace '\\', '/')
    }
}

function Get-QualityPlanningDefaults {
    param([string]$ResolvedProjectPath)

    $defaults = [ordered]@{
        known_traps_path          = '.specrew/quality/known-traps.md'
        routing_default_policy    = 'strongest-available'
        allow_lower_tier_override = $true
        approval_required         = $true
    }

    $configPath = Join-Path $ResolvedProjectPath '.specrew\config.yml'
    $configContent = Get-TextFileContent -Path $configPath
    if ([string]::IsNullOrWhiteSpace($configContent)) {
        return [pscustomobject]$defaults
    }

    $qualityBlockMatch = [regex]::Match($configContent, '(?ms)^\s*quality:\s*(?<body>(?:\r?\n\s+.+)+)')
    if (-not $qualityBlockMatch.Success) {
        return [pscustomobject]$defaults
    }

    $qualityBlock = $qualityBlockMatch.Groups['body'].Value
    $knownTrapsMatch = [regex]::Match($qualityBlock, '(?m)^\s*known_traps_path:\s*"?(?<value>[^"\r\n]+)"?\s*$')
    if ($knownTrapsMatch.Success) {
        $defaults.known_traps_path = ($knownTrapsMatch.Groups['value'].Value.Trim() -replace '\\', '/')
    }

    $routingBlockMatch = [regex]::Match($qualityBlock, '(?ms)^\s*routing:\s*(?<body>(?:\r?\n\s{4,}.+)+)')
    if ($routingBlockMatch.Success) {
        $routingBlock = $routingBlockMatch.Groups['body'].Value
        $defaultPolicyMatch = [regex]::Match($routingBlock, '(?m)^\s*default_policy:\s*"?(?<value>[^"\r\n]+)"?\s*$')
        if ($defaultPolicyMatch.Success) {
            $defaults.routing_default_policy = $defaultPolicyMatch.Groups['value'].Value.Trim()
        }

        $allowOverrideMatch = [regex]::Match($routingBlock, '(?m)^\s*allow_lower_tier_override:\s*(?<value>true|false)\s*$')
        if ($allowOverrideMatch.Success) {
            $defaults.allow_lower_tier_override = [System.Convert]::ToBoolean($allowOverrideMatch.Groups['value'].Value)
        }

        $approvalRequiredMatch = [regex]::Match($routingBlock, '(?m)^\s*approval_required:\s*(?<value>true|false)\s*$')
        if ($approvalRequiredMatch.Success) {
            $defaults.approval_required = [System.Convert]::ToBoolean($approvalRequiredMatch.Groups['value'].Value)
        }
    }

    return [pscustomobject]$defaults
}

function Get-PhaseTwoArtifactRefs {
    param(
        [string]$ResolvedProjectPath,
        [string]$ResolvedFeaturePath,
        [pscustomobject]$QualityDefaults
    )

    $featureRoot = if ([string]::IsNullOrWhiteSpace($ResolvedFeaturePath)) {
        'specs/<feature>'
    }
    else {
        Convert-ToRepoMarkdownPath -ResolvedProjectPath $ResolvedProjectPath -Path $ResolvedFeaturePath
    }

    $iterationQualityRoot = '{0}/iterations/<NNN>/quality' -f $featureRoot
    return [pscustomobject]@{
        hardening_gate_artifact      = '{0}/hardening-gate.md' -f $iterationQualityRoot
        known_traps_corpus_location  = [string]$QualityDefaults.known_traps_path
        trap_reapplication_artifact  = '{0}/trap-reapplication.md' -f $iterationQualityRoot
        lens_evidence_directory      = '{0}/lenses' -f $iterationQualityRoot
    }
}

function Get-PhaseTwoHardeningFocusAreas {
    param(
        [pscustomobject]$RiskResolution,
        [pscustomobject]$ArtifactRefs
    )

    $requiredDimensions = @($RiskResolution.required.id)
    $retryStatus = if ($requiredDimensions -contains 'retry-idempotency-and-recovery') { 'required' } else { 'not-applicable' }
    $retryRationale = if ($retryStatus -eq 'required') {
        'Retry, idempotency, or recovery behavior is materially relevant for this slice, so the hardening gate must capture the explicit guardrails before implementation starts.'
    }
    else {
        'The hardening gate still records why retry and idempotency do not materially apply in this slice so omissions stay reviewable before implementation begins.'
    }
    $qualityEvidencePath = '{0}/quality-evidence.md' -f (($ArtifactRefs.hardening_gate_artifact -replace '/hardening-gate\.md$', ''))

    return @(
        (New-HardeningFocusArea -FocusArea 'Security surface analysis' -WhyItMatters 'The hardening gate must capture planning-time security analysis, expected controls, and any explicit non-applicable reasoning before coding begins; runtime proof can remain pending only until later closure.' -PlannedArtifactOrEvidence $ArtifactRefs.hardening_gate_artifact -Status 'required'),
        (New-HardeningFocusArea -FocusArea 'Error handling and failure semantics' -WhyItMatters 'Silent failure paths, expected controls, and fallback expectations must be made explicit in the hardening gate so implementation does not invent them later or bypass runtime follow-through.' -PlannedArtifactOrEvidence $ArtifactRefs.hardening_gate_artifact -Status 'required'),
        (New-HardeningFocusArea -FocusArea 'Retry and idempotency expectations' -WhyItMatters $retryRationale -PlannedArtifactOrEvidence $ArtifactRefs.hardening_gate_artifact -Status $retryStatus),
        (New-HardeningFocusArea -FocusArea 'Test-integrity targets' -WhyItMatters 'The hardening gate must name the planned validation evidence and expected controls for this slice so implementation readiness does not rely on smoke-only success while runtime/test proof remains visibly pending until later closure.' -PlannedArtifactOrEvidence ('feature plan Phase 2 quality planning section plus {0}' -f $qualityEvidencePath) -Status 'required')
    )
}

function Get-LensIdFromRef {
    param([string]$LensRef)

    if ([string]::IsNullOrWhiteSpace($LensRef)) {
        return ''
    }

    return ($LensRef -split '@', 2)[0]
}

function Get-PhaseTwoLensActivationPlan {
    param(
        [pscustomobject]$Profile,
        [pscustomobject]$ArtifactRefs,
        [pscustomobject]$QualityDefaults
    )

    $lensRefs = [System.Collections.Generic.List[string]]::new()
    Add-UniqueItems -List $lensRefs -Values @($Profile.required_lens_refs)
    Add-UniqueItems -List $lensRefs -Values @($Profile.custom_lens_refs)

    $entries = [System.Collections.Generic.List[object]]::new()
    foreach ($lensRef in $lensRefs) {
        $lensId = Get-LensIdFromRef -LensRef $lensRef
        $evidencePath = '{0}/{1}.md' -f $ArtifactRefs.lens_evidence_directory, $lensId
        $activation = 'optional'
        $rationale = 'The lens remains available for later Phase 2 execution, but the current slice only publishes bounded planning metadata.'

        switch ($lensId) {
            'security-baseline' {
                $activation = 'required'
                $rationale = 'Security is always a materially reviewed baseline dimension, so the security lens stays pre-activated in planning even though row-level execution remains deferred.'
            }
            'robustness-baseline' {
                $activation = 'required'
                $rationale = 'Robustness, failure semantics, and retry-related concerns feed the hardening gate directly, so the robustness lens must be visible as required planning metadata.'
            }
            'test-integrity' {
                $activation = 'required'
                $rationale = 'Test-integrity targets are part of the pre-implementation hardening review, so this lens stays explicitly required in the bounded plan.'
            }
        }

        $null = $entries.Add((New-LensActivationPlanEntry -LensRef $lensRef -Activation $activation -Rationale $rationale -PlannedEvidencePath $evidencePath -RequestedReviewClass $QualityDefaults.routing_default_policy))
    }

    return @($entries)
}

function Get-PhaseTwoRoutingPolicy {
    param([pscustomobject]$QualityDefaults)

    $overrideRecord = if ($QualityDefaults.allow_lower_tier_override) {
        if ($QualityDefaults.approval_required) {
            'Explicit approved lower-tier override required before any downgrade takes effect.'
        }
        else {
            'Lower-tier overrides are allowed by config without a separate approval gate.'
        }
    }
    else {
        'No lower-tier override path is enabled for required hardening or specialist review work.'
    }

    return @(
        (New-RoutingPolicyEntry -LensScope 'Required hardening and bug-hunter lenses' -RequestedReviewClass $QualityDefaults.routing_default_policy -EffectiveClass 'Record when execution happens' -OverrideApprovalRecord $overrideRecord -Notes 'Planning publishes the requested routing baseline only; effective-class evidence stays deferred until the execution path exists.')
    )
}

function Get-PhaseTwoLaterDeferrals {
    return @(
        'Full line-by-line lens execution evidence remains deferred until the approved implementation/review slice authorizes it.',
        'Known-traps corpus seeding, approved additions, and trap reapplication remain deferred until the dedicated known-traps slice is in scope.',
        'Strongest-class routing enforcement details and requested-versus-effective execution evidence remain deferred until the routed lens execution path exists.',
        'Quality-drift comparison, mixed-stack override workflows, and reference-implementation checks remain deferred unless the approved slice explicitly includes them.'
    )
}

function Get-QualitySignals {
    param(
        [string]$ResolvedProjectPath,
        [string]$ResolvedFeaturePath,
        [string]$ResolvedSpecPath
    )

    $packageJsonPath = Join-Path $ResolvedProjectPath 'package.json'
    $pyprojectPath = Join-Path $ResolvedProjectPath 'pyproject.toml'
    $requirementsPath = Join-Path $ResolvedProjectPath 'requirements.txt'
    $projectFiles = Get-ChildItem -LiteralPath $ResolvedProjectPath -File -Recurse -ErrorAction SilentlyContinue
    $csprojFiles = @($projectFiles | Where-Object { $_.Extension -ieq '.csproj' })
    $slnFiles = @($projectFiles | Where-Object { $_.Extension -ieq '.sln' })

    $planPath = if ([string]::IsNullOrWhiteSpace($ResolvedFeaturePath)) { $null } else { Join-Path $ResolvedFeaturePath 'plan.md' }
    $tasksPath = if ([string]::IsNullOrWhiteSpace($ResolvedFeaturePath)) { $null } else { Join-Path $ResolvedFeaturePath 'tasks.md' }
    $quickstartPath = if ([string]::IsNullOrWhiteSpace($ResolvedFeaturePath)) { $null } else { Join-Path $ResolvedFeaturePath 'quickstart.md' }
    $contextText = @(
        Get-TextFileContent -Path $ResolvedSpecPath
        Get-TextFileContent -Path $planPath
        Get-TextFileContent -Path $tasksPath
        Get-TextFileContent -Path $quickstartPath
    ) -join "`n"
    $normalizedContext = $contextText.ToLowerInvariant()
    $dependencies = Get-DependencyNames -PackageJsonPath $packageJsonPath

    return [pscustomobject]@{
        package_json_path = $packageJsonPath
        has_package_json  = Test-Path -LiteralPath $packageJsonPath -PathType Leaf
        dependencies      = @($dependencies)
        has_pyproject     = Test-Path -LiteralPath $pyprojectPath -PathType Leaf
        has_requirements  = Test-Path -LiteralPath $requirementsPath -PathType Leaf
        csproj_files      = @($csprojFiles | Select-Object -ExpandProperty FullName)
        sln_files         = @($slnFiles | Select-Object -ExpandProperty FullName)
        context_text      = $contextText
        normalized_text   = $normalizedContext
    }
}

function Get-PresetCandidates {
    param([pscustomobject]$Signals)

    $candidates = [System.Collections.Generic.List[object]]::new()
    $dependencies = @($Signals.dependencies)
    $normalizedText = [string]$Signals.normalized_text

    $hasNodeApiDependency = [bool]($dependencies | Where-Object { $_ -in @('express', 'fastify', 'koa', 'hapi', '@nestjs/core', '@nestjs/platform-express') } | Select-Object -First 1)
    $hasWebsocketDependency = [bool]($dependencies | Where-Object { $_ -in @('ws', 'socket.io', 'socket.io-client', '@fastify/websocket', 'uwebsockets.js') } | Select-Object -First 1)
    $hasReactDependency = [bool]($dependencies | Where-Object { $_ -in @('react', 'react-dom', 'next') } | Select-Object -First 1)
    $hasPostgresDependency = [bool]($dependencies | Where-Object { $_ -in @('pg', 'postgres', 'knex', 'typeorm', 'sequelize', 'prisma') } | Select-Object -First 1)
    $hasFastApiSignal = $Signals.has_pyproject -or $Signals.has_requirements -or (Test-AnyPattern -Text $normalizedText -Patterns @('\bfastapi\b'))
    $hasAspNetSignal = ($Signals.csproj_files.Count -gt 0) -or ($Signals.sln_files.Count -gt 0) -or (Test-AnyPattern -Text $normalizedText -Patterns @('\basp\.net\b', '\baspnet\b', '\bcontroller\b', '\bminimal api\b'))

    $nodeWebsocketScore = 0
    $nodeWebsocketSignals = [System.Collections.Generic.List[string]]::new()
    if ($Signals.has_package_json) {
        $nodeWebsocketScore += 20
        Add-UniqueItem -List $nodeWebsocketSignals -Value 'package.json'
    }
    if ($hasWebsocketDependency) {
        $nodeWebsocketScore += 35
        Add-UniqueItem -List $nodeWebsocketSignals -Value 'websocket transport dependency'
    }
    if (Test-AnyPattern -Text $normalizedText -Patterns @('\bwebsocket\b', '\brealtime\b', 'socket', '/ws\b', 'long-lived connection')) {
        $nodeWebsocketScore += 35
        Add-UniqueItem -List $nodeWebsocketSignals -Value 'websocket feature scope'
    }
    if (Test-AnyPattern -Text $normalizedText -Patterns @('\bpublic\b', '\binternet-facing\b', '\bclient\b')) {
        $nodeWebsocketScore += 10
        Add-UniqueItem -List $nodeWebsocketSignals -Value 'public connection boundary'
    }
    if ($nodeWebsocketScore -ge 70) {
        $null = $candidates.Add([pscustomobject]@{
                preset_id       = 'node-public-ws-service'
                score           = $nodeWebsocketScore
                matched_signals = $nodeWebsocketSignals.ToArray()
            })
    }

    $reactScore = 0
    $reactSignals = [System.Collections.Generic.List[string]]::new()
    if ($Signals.has_package_json) {
        $reactScore += 20
        Add-UniqueItem -List $reactSignals -Value 'package.json'
    }
    if ($hasReactDependency) {
        $reactScore += 35
        Add-UniqueItem -List $reactSignals -Value 'react dependency'
    }
    if (Test-AnyPattern -Text $normalizedText -Patterns @('\breact\b', '\bspa\b', '\bfrontend\b', '\bbrowser\b', '\bcomponent\b', 'single-page')) {
        $reactScore += 25
        Add-UniqueItem -List $reactSignals -Value 'browser UI feature scope'
    }
    if (Test-Path -LiteralPath (Join-Path $ProjectPath 'src\components') -PathType Container -ErrorAction SilentlyContinue) {
        $reactScore += 10
        Add-UniqueItem -List $reactSignals -Value 'component-oriented source layout'
    }
    if ($reactScore -ge 70) {
        $null = $candidates.Add([pscustomobject]@{
                preset_id       = 'react-spa-public'
                score           = $reactScore
                matched_signals = $reactSignals.ToArray()
            })
    }

    $nodeRestScore = 0
    $nodeRestSignals = [System.Collections.Generic.List[string]]::new()
    if ($Signals.has_package_json) {
        $nodeRestScore += 20
        Add-UniqueItem -List $nodeRestSignals -Value 'package.json'
    }
    if ($hasNodeApiDependency) {
        $nodeRestScore += 20
        Add-UniqueItem -List $nodeRestSignals -Value 'node API dependency'
    }
    if ($hasPostgresDependency) {
        $nodeRestScore += 25
        Add-UniqueItem -List $nodeRestSignals -Value 'postgres dependency'
    }
    if (Test-AnyPattern -Text $normalizedText -Patterns @('\brest\b', '\bhttp\b', '\bapi\b', '\broute\b', '\bendpoint\b', '\bpostgres\b', '\bdatabase\b')) {
        $nodeRestScore += 25
        Add-UniqueItem -List $nodeRestSignals -Value 'API or persistence feature scope'
    }
    if ($nodeRestScore -ge 75) {
        $null = $candidates.Add([pscustomobject]@{
                preset_id       = 'node-rest-with-postgres'
                score           = $nodeRestScore
                matched_signals = $nodeRestSignals.ToArray()
            })
    }

    $fastApiScore = 0
    $fastApiSignals = [System.Collections.Generic.List[string]]::new()
    if ($Signals.has_pyproject -or $Signals.has_requirements) {
        $fastApiScore += 25
        Add-UniqueItem -List $fastApiSignals -Value 'python project manifest'
    }
    if (Test-AnyPattern -Text $normalizedText -Patterns @('\bfastapi\b', '\brouter\b', '\bendpoint\b', '\basync api\b')) {
        $fastApiScore += 35
        Add-UniqueItem -List $fastApiSignals -Value 'fastapi feature scope'
    }
    if (Test-AnyPattern -Text $normalizedText -Patterns @('\bservice\b', '\bhttp\b', '\bpublic\b')) {
        $fastApiScore += 15
        Add-UniqueItem -List $fastApiSignals -Value 'public service surface'
    }
    if ($fastApiScore -ge 70 -and $hasFastApiSignal) {
        $null = $candidates.Add([pscustomobject]@{
                preset_id       = 'python-fastapi-service'
                score           = $fastApiScore
                matched_signals = $fastApiSignals.ToArray()
            })
    }

    $aspNetScore = 0
    $aspNetSignals = [System.Collections.Generic.List[string]]::new()
    if ($Signals.csproj_files.Count -gt 0 -or $Signals.sln_files.Count -gt 0) {
        $aspNetScore += 25
        Add-UniqueItem -List $aspNetSignals -Value '*.csproj or solution file'
    }
    if (Test-AnyPattern -Text $normalizedText -Patterns @('\basp\.net\b', '\baspnet\b', '\bcontroller\b', '\bminimal api\b', '\bmiddleware\b')) {
        $aspNetScore += 35
        Add-UniqueItem -List $aspNetSignals -Value 'ASP.NET API surface'
    }
    if (Test-AnyPattern -Text $normalizedText -Patterns @('\bapi\b', '\bpublic\b', '\bservice\b')) {
        $aspNetScore += 15
        Add-UniqueItem -List $aspNetSignals -Value 'hosted .NET service scope'
    }
    if ($aspNetScore -ge 70 -and $hasAspNetSignal) {
        $null = $candidates.Add([pscustomobject]@{
                preset_id       = 'dotnet-aspnet-api'
                score           = $aspNetScore
                matched_signals = $aspNetSignals.ToArray()
            })
    }

    return @(
        $candidates |
            Sort-Object -Property @(
                @{ Expression = 'score'; Descending = $true }
                @{ Expression = 'preset_id'; Descending = $false }
            )
    )
}

function Get-PresetProfile {
    param(
        [string]$PresetId,
        [string[]]$MatchedSignals
    )

    switch ($PresetId) {
        'node-public-ws-service' {
            return [pscustomobject]@{
                preset_ref           = 'node-public-ws-service@v1.0.0'
                profile_id           = 'quality-profile.node-public-ws-service.v1'
                bundle_id            = 'node-websocket-phase1'
                stack_surfaces       = @(
                    (New-StackSurface -SurfaceId 'service-runtime' -PathGlobs @('package.json', 'src/**/*.js', 'src/**/*.ts') -Language 'Node.js' -RuntimeShape 'service-runtime' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals),
                    (New-StackSurface -SurfaceId 'websocket-boundary' -PathGlobs @('src/**/*ws*', 'src/**/*socket*', 'src/**/*gateway*') -Language 'Node.js' -RuntimeShape 'websocket-boundary' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals),
                    (New-StackSurface -SurfaceId 'session-state' -PathGlobs @('src/**/*session*', 'src/**/*connection*') -Language 'Node.js' -RuntimeShape 'session-state' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals)
                )
                preset_dimensions    = @('verification-confidence', 'security', 'robustness', 'concurrency-correctness', 'resiliency')
                ecosystem_tools      = @('npm test', 'repo-standard Node lint/static-analysis command', 'deterministic websocket integration checks')
                mechanical_checks    = @('dead-field', 'anti-pattern', 'test-integrity')
                required_lens_refs   = Get-DefaultLensRefs
                custom_lens_refs     = @()
            }
        }
        'react-spa-public' {
            return [pscustomobject]@{
                preset_ref           = 'react-spa-public@v1.0.0'
                profile_id           = 'quality-profile.react-spa-public.v1'
                bundle_id            = 'react-spa-phase1'
                stack_surfaces       = @(
                    (New-StackSurface -SurfaceId 'browser-ui' -PathGlobs @('package.json', 'src/**/*.jsx', 'src/**/*.tsx') -Language 'TypeScript/JavaScript' -RuntimeShape 'browser-spa' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals),
                    (New-StackSurface -SurfaceId 'component-state' -PathGlobs @('src/**/*component*', 'src/**/*hook*', 'src/**/*state*') -Language 'TypeScript/JavaScript' -RuntimeShape 'component-state' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals)
                )
                preset_dimensions    = @('verification-confidence', 'maintainability', 'security')
                ecosystem_tools      = @('npm test', 'repo-standard frontend lint/static-analysis command', 'browser-oriented component/integration tests')
                mechanical_checks    = @('dead-field', 'anti-pattern', 'test-integrity')
                required_lens_refs   = Get-DefaultLensRefs
                custom_lens_refs     = @()
            }
        }
        'node-rest-with-postgres' {
            return [pscustomobject]@{
                preset_ref           = 'node-rest-with-postgres@v1.0.0'
                profile_id           = 'quality-profile.node-rest-with-postgres.v1'
                bundle_id            = 'node-rest-postgres-phase1'
                stack_surfaces       = @(
                    (New-StackSurface -SurfaceId 'api-runtime' -PathGlobs @('package.json', 'src/**/*.js', 'src/**/*.ts') -Language 'Node.js' -RuntimeShape 'http-api' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals),
                    (New-StackSurface -SurfaceId 'persistence-boundary' -PathGlobs @('src/**/*repository*', 'src/**/*db*', 'src/**/*postgres*') -Language 'Node.js' -RuntimeShape 'postgres-persistence' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals)
                )
                preset_dimensions    = @('maintainability', 'security', 'robustness', 'resiliency')
                ecosystem_tools      = @('npm test', 'repo-standard Node lint/static-analysis command', 'Postgres-backed integration coverage')
                mechanical_checks    = @('dead-field', 'anti-pattern', 'test-integrity')
                required_lens_refs   = Get-DefaultLensRefs
                custom_lens_refs     = @()
            }
        }
        'python-fastapi-service' {
            return [pscustomobject]@{
                preset_ref           = 'python-fastapi-service@v1.0.0'
                profile_id           = 'quality-profile.python-fastapi-service.v1'
                bundle_id            = 'python-fastapi-phase1'
                stack_surfaces       = @(
                    (New-StackSurface -SurfaceId 'api-runtime' -PathGlobs @('pyproject.toml', 'requirements.txt', '**/*.py') -Language 'Python' -RuntimeShape 'http-api' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals),
                    (New-StackSurface -SurfaceId 'request-models' -PathGlobs @('**/*schema*.py', '**/*model*.py') -Language 'Python' -RuntimeShape 'typed-request-models' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals)
                )
                preset_dimensions    = @('verification-confidence', 'security', 'robustness', 'resiliency')
                ecosystem_tools      = @('pytest', 'repo-standard Python lint/type-analysis command', 'FastAPI route/integration tests')
                mechanical_checks    = @('dead-field', 'anti-pattern', 'test-integrity')
                required_lens_refs   = Get-DefaultLensRefs
                custom_lens_refs     = @()
            }
        }
        'dotnet-aspnet-api' {
            return [pscustomobject]@{
                preset_ref           = 'dotnet-aspnet-api@v1.0.0'
                profile_id           = 'quality-profile.dotnet-aspnet-api.v1'
                bundle_id            = 'dotnet-aspnet-phase1'
                stack_surfaces       = @(
                    (New-StackSurface -SurfaceId 'api-runtime' -PathGlobs @('**/*.csproj', '**/*.cs', '**/*.sln') -Language '.NET / C#' -RuntimeShape 'http-api' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals),
                    (New-StackSurface -SurfaceId 'request-pipeline' -PathGlobs @('**/*Controller.cs', '**/*Middleware*.cs', '**/Program.cs') -Language '.NET / C#' -RuntimeShape 'request-pipeline' -RecognizedStack $PresetId -MatchedSignals $MatchedSignals)
                )
                preset_dimensions    = @('verification-confidence', 'security', 'robustness', 'resiliency')
                ecosystem_tools      = @('dotnet test', 'repo-standard .NET analyzer/lint lane', 'integration or host-level API tests')
                mechanical_checks    = @('dead-field', 'anti-pattern', 'test-integrity')
                required_lens_refs   = Get-DefaultLensRefs
                custom_lens_refs     = @()
            }
        }
        default {
            throw "Unsupported preset '$PresetId'."
        }
    }
}

function Get-CustomCompositionProfile {
    param(
        [pscustomobject]$Signals,
        [object[]]$Candidates
    )

    $stackSurfaces = [System.Collections.Generic.List[object]]::new()
    foreach ($candidate in $Candidates | Select-Object -First 2) {
        $matchedSignals = @($candidate.matched_signals)
        switch ($candidate.preset_id) {
            'react-spa-public' {
                $null = $stackSurfaces.Add((New-StackSurface -SurfaceId 'browser-ui' -PathGlobs @('package.json', 'src/**/*.jsx', 'src/**/*.tsx') -Language 'TypeScript/JavaScript' -RuntimeShape 'browser-spa' -RecognizedStack 'custom' -MatchedSignals $matchedSignals))
            }
            'node-rest-with-postgres' {
                $null = $stackSurfaces.Add((New-StackSurface -SurfaceId 'api-runtime' -PathGlobs @('package.json', 'src/**/*.js', 'src/**/*.ts') -Language 'Node.js' -RuntimeShape 'http-api' -RecognizedStack 'custom' -MatchedSignals $matchedSignals))
            }
            'node-public-ws-service' {
                $null = $stackSurfaces.Add((New-StackSurface -SurfaceId 'realtime-runtime' -PathGlobs @('package.json', 'src/**/*ws*', 'src/**/*socket*') -Language 'Node.js' -RuntimeShape 'websocket-boundary' -RecognizedStack 'custom' -MatchedSignals $matchedSignals))
            }
            'python-fastapi-service' {
                $null = $stackSurfaces.Add((New-StackSurface -SurfaceId 'python-service' -PathGlobs @('pyproject.toml', 'requirements.txt', '**/*.py') -Language 'Python' -RuntimeShape 'http-api' -RecognizedStack 'custom' -MatchedSignals $matchedSignals))
            }
            'dotnet-aspnet-api' {
                $null = $stackSurfaces.Add((New-StackSurface -SurfaceId 'dotnet-service' -PathGlobs @('**/*.csproj', '**/*.cs', '**/*.sln') -Language '.NET / C#' -RuntimeShape 'http-api' -RecognizedStack 'custom' -MatchedSignals $matchedSignals))
            }
        }
    }

    if ($stackSurfaces.Count -eq 0) {
        $genericSignals = [System.Collections.Generic.List[string]]::new()
        if ($Signals.has_package_json) {
            Add-UniqueItem -List $genericSignals -Value 'package.json'
        }
        if ($Signals.has_pyproject -or $Signals.has_requirements) {
            Add-UniqueItem -List $genericSignals -Value 'python project manifest'
        }
        if ($Signals.csproj_files.Count -gt 0 -or $Signals.sln_files.Count -gt 0) {
            Add-UniqueItem -List $genericSignals -Value '.NET project file'
        }
        if ($genericSignals.Count -eq 0) {
            Add-UniqueItem -List $genericSignals -Value 'feature specification only'
        }

        $null = $stackSurfaces.Add((New-StackSurface -SurfaceId 'custom-phase1-surface' -PathGlobs @('**/*') -Language 'mixed-or-unknown' -RuntimeShape 'custom-surface' -RecognizedStack 'custom' -MatchedSignals $genericSignals.ToArray()))
    }

    $ecosystemTools = [System.Collections.Generic.List[string]]::new()
    if ($Signals.has_package_json) {
        Add-UniqueItems -List $ecosystemTools -Values @('repo-standard stack-specific lint/static-analysis command', 'repo-standard verification command')
    }
    if ($Signals.has_pyproject -or $Signals.has_requirements) {
        Add-UniqueItems -List $ecosystemTools -Values @('pytest', 'repo-standard Python lint/type-analysis command')
    }
    if ($Signals.csproj_files.Count -gt 0 -or $Signals.sln_files.Count -gt 0) {
        Add-UniqueItems -List $ecosystemTools -Values @('dotnet test', 'repo-standard .NET analyzer/lint lane')
    }
    if ($ecosystemTools.Count -eq 0) {
        Add-UniqueItems -List $ecosystemTools -Values @('repo-standard stack-specific lint/static-analysis command', 'manual review evidence recorded in quality-evidence.md')
    }

    $reason = if ($Candidates.Count -gt 1) {
        'Repository and feature signals map to more than one Phase 1 preset, so this slice stays bounded by composing from the approved baseline lenses and mechanical gates instead of claiming a single recognized preset.'
    }
    else {
        'Repository and feature signals are weak or unsupported for a confident Phase 1 preset match, so this slice falls back to a bounded custom composition with explicit manual review expectations.'
    }

    return [pscustomobject]@{
        preset_ref           = $null
        profile_id           = 'quality-profile.custom-composition.v1'
        bundle_id            = 'phase1-custom-quality-bundle'
        stack_surfaces       = @($stackSurfaces)
        preset_dimensions    = @('verification-confidence', 'security', 'robustness')
        ecosystem_tools      = $ecosystemTools.ToArray()
        mechanical_checks    = @('dead-field', 'anti-pattern', 'test-integrity')
        required_lens_refs   = @()
        custom_lens_refs     = Get-DefaultLensRefs
        custom_reason        = $reason
        unknowns             = @(
            'Confirm the stack-specific lint or analyzer command for the active surface.',
            'Confirm whether any additional stack-specific evidence source is needed beyond the Phase 1 baseline mechanical gates and checklist references.'
        )
    }
}

function Get-RiskResolution {
    param(
        [pscustomobject]$Profile,
        [pscustomobject]$Signals
    )

    $required = [System.Collections.Generic.List[object]]::new()
    $notApplicable = [System.Collections.Generic.List[object]]::new()
    $baselineDimensions = Get-BaselineRiskDimensions

    foreach ($dimension in $baselineDimensions) {
        $rationale = switch ($dimension) {
            'code-quality' { 'Phase 1 always evaluates code-quality expectations because the quality tool bundle must remain explicit and reviewable.' }
            'design-quality-and-separation-of-concerns' { 'Phase 1 always evaluates design quality and separation of concerns so the plan does not hide layering or coupling risks.' }
            'verification-confidence' { 'Phase 1 always requires verification confidence so tests and evidence prove observable behavior instead of smoke-only success.' }
            'maintainability' { 'Phase 1 always evaluates maintainability because the quality bar must remain stack-aware and reviewable for later iterations.' }
            'security' { 'Phase 1 always evaluates security because every active feature can expose boundary, configuration, or data-handling concerns.' }
            'robustness' { 'Phase 1 always evaluates robustness so degraded behavior and failure semantics are explicit before implementation continues.' }
            default { 'Phase 1 baseline quality dimension.' }
        }

        $null = $required.Add((New-RiskDimension -Id $dimension -Status 'required' -Rationale $rationale))
    }

    $normalizedText = [string]$Signals.normalized_text
    $surfaceRuntimeShapes = @($Profile.stack_surfaces | ForEach-Object { $_.runtime_shape })
    $isRecognizedPreset = -not [string]::IsNullOrWhiteSpace([string]$Profile.preset_ref)

    $requiresConcurrency = if ($isRecognizedPreset) {
        ($Profile.profile_id -eq 'quality-profile.node-public-ws-service.v1') -or
        (Test-AnyPattern -Text $normalizedText -Patterns @('\bconcurr', '\brace\b', '\bparallel\b', '\bshared state\b', '\bwebsocket\b', '\brealtime\b', '\bsession\b'))
    }
    else {
        $surfaceRuntimeShapes -contains 'websocket-boundary'
    }

    $requiresResiliency = if ($isRecognizedPreset) {
        ($Profile.profile_id -in @('quality-profile.node-public-ws-service.v1', 'quality-profile.node-rest-with-postgres.v1', 'quality-profile.python-fastapi-service.v1', 'quality-profile.dotnet-aspnet-api.v1')) -or
        (Test-AnyPattern -Text $normalizedText -Patterns @('\bretry\b', '\bidempot', '\brecover', '\btimeout\b', '\bbackoff\b', '\breconnect\b', '\bdegraded\b', '\bfailure\b'))
    }
    else {
        $surfaceRuntimeShapes -contains 'websocket-boundary'
    }

    if ($requiresConcurrency) {
        $null = $required.Add((New-RiskDimension -Id 'concurrency-correctness' -Status 'required' -Rationale 'The feature shape materially touches realtime, session, or shared-state behavior, so concurrency correctness must be planned explicitly in Phase 1.'))
    }
    else {
        $null = $notApplicable.Add([pscustomobject]@{
                id               = 'concurrency-correctness'
                rationale        = 'No repository or feature signal shows material shared-state, parallel, or realtime concurrency behavior for this Phase 1 slice.'
                omitted_gate_ids = @('concurrency-correctness-review')
            })
    }

    if ($requiresResiliency) {
        $null = $required.Add((New-RiskDimension -Id 'resiliency' -Status 'required' -Rationale 'The feature shape includes failure-handling, reconnect, async service, or persistence semantics that need an explicit resiliency expectation in Phase 1.'))
        $null = $required.Add((New-RiskDimension -Id 'retry-idempotency-and-recovery' -Status 'required' -Rationale 'Retry, idempotency, and recovery concerns are materially relevant for the active surface, so the plan must call them out even though later-phase hardening workflows stay deferred.'))
    }
    else {
        $null = $notApplicable.Add([pscustomobject]@{
                id               = 'resiliency'
                rationale        = 'The current Phase 1 slice does not materially depend on retries, reconnect, or degraded recovery behavior beyond the baseline robustness expectation.'
                omitted_gate_ids = @('resiliency-semantics-review')
            })
        $null = $notApplicable.Add([pscustomobject]@{
                id               = 'retry-idempotency-and-recovery'
                rationale        = 'Retry, idempotency, and recovery-specific gates are not required because the active feature shape does not present a material retry or recovery workflow in this slice.'
                omitted_gate_ids = @('retry-idempotency-review')
            })
    }

    return [pscustomobject]@{
        required       = @($required)
        not_applicable = @($notApplicable)
    }
}

function Get-RequiredQualityGates {
    param(
        [pscustomobject]$Profile,
        [pscustomobject]$RiskResolution
    )

    $gates = [System.Collections.Generic.List[object]]::new()
    $evidenceDirectory = 'specs/<feature>/iterations/<NNN>/quality/'
    $findingsPath = $evidenceDirectory + 'mechanical-findings.json'
    $evidencePath = $evidenceDirectory + 'quality-evidence.md'

    $null = $gates.Add((New-QualityGate -GateId 'dead-field' -Category 'mechanical' -RequirementRefs @('FR-004', 'FR-027', 'FR-030') -EvidenceRef $findingsPath -Description 'Inspect declared fields, DTOs, and config members for unused state.'))
    $null = $gates.Add((New-QualityGate -GateId 'anti-pattern' -Category 'mechanical' -RequirementRefs @('FR-004', 'FR-028', 'FR-030') -EvidenceRef $findingsPath -Description 'Flag deterministic anti-patterns before model-based review.'))
    $null = $gates.Add((New-QualityGate -GateId 'test-integrity' -Category 'mechanical' -RequirementRefs @('FR-004', 'FR-029', 'FR-030') -EvidenceRef $findingsPath -Description 'Require assertion-driven tests with meaningful negative-path evidence.'))
    $null = $gates.Add((New-QualityGate -GateId 'stack-tooling-evidence' -Category 'tooling' -RequirementRefs @('FR-004', 'FR-010', 'FR-011') -EvidenceRef $evidencePath -Description 'Record the stack-aware lint, static-analysis, and verification command(s) selected for the active surface.'))
    $null = $gates.Add((New-QualityGate -GateId 'quality-lens-review' -Category 'manual-evidence' -RequirementRefs @('FR-010', 'FR-011', 'FR-015') -EvidenceRef $evidencePath -Description 'Record checklist-backed quality reasoning for the selected preset or bounded custom composition.'))

    if ($RiskResolution.required.id -contains 'concurrency-correctness') {
        $null = $gates.Add((New-QualityGate -GateId 'concurrency-correctness-review' -Category 'manual-evidence' -RequirementRefs @('FR-003', 'FR-015') -EvidenceRef $evidencePath -Description 'Record how the plan addresses materially relevant concurrency-correctness concerns in Phase 1.'))
    }

    if ($RiskResolution.required.id -contains 'resiliency') {
        $null = $gates.Add((New-QualityGate -GateId 'resiliency-semantics-review' -Category 'manual-evidence' -RequirementRefs @('FR-003', 'FR-015') -EvidenceRef $evidencePath -Description 'Record how the plan addresses materially relevant resiliency and degraded-behavior concerns in Phase 1.'))
    }

    if ($RiskResolution.required.id -contains 'retry-idempotency-and-recovery') {
        $null = $gates.Add((New-QualityGate -GateId 'retry-idempotency-review' -Category 'manual-evidence' -RequirementRefs @('FR-015') -EvidenceRef $evidencePath -Description 'Record retry, idempotency, and recovery expectations only when they materially apply in this Phase 1 slice.'))
    }

    return @($gates)
}

function Convert-QualityProfileToMarkdown {
    param([pscustomobject]$Resolution)

    $lines = [System.Collections.Generic.List[string]]::new()
    $selectedPresetText = if ($Resolution.preset_refs.Count -gt 0) { $Resolution.preset_refs -join ', ' } else { 'None - using bounded custom composition' }
    $customCompositionText = if ($Resolution.custom_composition) { $Resolution.custom_composition.reason } else { 'Not required for this recognized stack.' }

    $null = $lines.Add('## Phase 1 Quality Planning')
    $null = $lines.Add('')
    $null = $lines.Add('**Phase Scope**: `phase-1-first-slice`')
    $null = $lines.Add(('**Inferred Quality Profile**: `{0}`' -f $Resolution.profile_id))
    $null = $lines.Add(('**Selected preset ref or explicit custom composition**: {0}' -f $selectedPresetText))
    $null = $lines.Add(('**Bounded custom composition**: {0}' -f $customCompositionText))
    $null = $lines.Add('')
    $null = $lines.Add('### Stack Surfaces in Scope')
    $null = $lines.Add('| Stack Surface | Recognized Stack | Path Globs | Matched Signals |')
    $null = $lines.Add('| --- | --- | --- | --- |')
    foreach ($surface in $Resolution.stack_surfaces) {
        $null = $lines.Add(('| `{0}` | `{1}` | `{2}` | {3} |' -f $surface.surface_id, $surface.recognized_stack, (($surface.path_globs -join ', ') -replace '\|', '\|'), ($surface.matched_signals -join '; ')))
    }
    $null = $lines.Add('')
    $null = $lines.Add('### Risk Dimensions')
    $null = $lines.Add('| Risk Dimension | Status | Rationale |')
    $null = $lines.Add('| --- | --- | --- |')
    foreach ($dimension in $Resolution.risk_dimensions) {
        $null = $lines.Add(('| `{0}` | `{1}` | {2} |' -f $dimension.id, $dimension.status, $dimension.rationale))
    }
    $null = $lines.Add('')
    $null = $lines.Add('### Quality Tool Bundle')
    $null = $lines.Add('| Area | Selection |')
    $null = $lines.Add('| --- | --- |')
    $null = $lines.Add(('| Bundle ID | `{0}` |' -f $Resolution.tool_bundle.bundle_id))
    $null = $lines.Add(('| Mechanical Checks | {0} |' -f ($Resolution.tool_bundle.mechanical_checks -join ', ')))
    $null = $lines.Add(('| Ecosystem Tools | {0} |' -f ($Resolution.tool_bundle.ecosystem_tools -join ', ')))
    $null = $lines.Add(('| Manual Evidence | {0} |' -f ($Resolution.tool_bundle.manual_evidence -join ', ')))
    $null = $lines.Add('')
    $null = $lines.Add('### Required Quality Gates')
    $null = $lines.Add('| Required Quality Gate | Category | Evidence Source |')
    $null = $lines.Add('| --- | --- | --- |')
    foreach ($gate in $Resolution.required_quality_gates) {
        $null = $lines.Add(('| `{0}` | `{1}` | `{2}` |' -f $gate.gate_id, $gate.category, $gate.evidence_ref))
    }
    $null = $lines.Add('')
    $null = $lines.Add('### Not-Applicable Dimensions and Rationale')
    $null = $lines.Add('| Dimension | Rationale | Omitted Gates |')
    $null = $lines.Add('| --- | --- | --- |')
    foreach ($dimension in $Resolution.not_applicable_dimensions) {
        $null = $lines.Add(('| `{0}` | {1} | `{2}` |' -f $dimension.id, $dimension.rationale, ($dimension.omitted_gate_ids -join ', ')))
    }
    $null = $lines.Add('')
    $null = $lines.Add('### Explicit Phase 2+ Deferrals')
    foreach ($deferral in $Resolution.phase2_deferrals) {
        $null = $lines.Add(("- {0}" -f $deferral))
    }
    $null = $lines.Add('')
    $null = $lines.Add('## Phase 2 Hardening and Specialist Review Planning')
    $null = $lines.Add('')
    $null = $lines.Add(('**Phase 2 Slice Scope**: `{0}`' -f $Resolution.phase2_slice_scope))
    $null = $lines.Add(('**Hardening Gate Artifact**: `{0}`' -f $Resolution.phase2_hardening_gate_artifact))
    $null = $lines.Add(('**Known-Traps Corpus Location**: `{0}`' -f $Resolution.phase2_known_traps_corpus_location))
    $null = $lines.Add(('**Trap Reapplication Artifact**: `{0}`' -f $Resolution.phase2_trap_reapplication_artifact))
    $null = $lines.Add('')
    $null = $lines.Add('### Hardening Focus Areas')
    $null = $lines.Add('| Focus Area | Why It Matters in This Slice | Planned Artifact / Evidence | Status |')
    $null = $lines.Add('| --- | --- | --- | --- |')
    foreach ($focusArea in $Resolution.phase2_hardening_focus_areas) {
        $null = $lines.Add(('| {0} | {1} | `{2}` | `{3}` |' -f $focusArea.focus_area, $focusArea.why_it_matters, $focusArea.planned_artifact_or_evidence, $focusArea.status))
    }
    $null = $lines.Add('')
    $null = $lines.Add('### Lens Activation Plan')
    $null = $lines.Add('| Lens / Checklist Ref | Activation | Why Activated or Omitted | Planned Evidence / Artifact Path |')
    $null = $lines.Add('| --- | --- | --- | --- |')
    foreach ($lensPlan in $Resolution.phase2_lens_activation_plan) {
        $null = $lines.Add(('| `{0}` | `{1}` | {2} | `{3}` |' -f $lensPlan.lens_ref, $lensPlan.activation, $lensPlan.rationale, $lensPlan.planned_evidence_path))
    }
    $null = $lines.Add('')
    $null = $lines.Add('### Routing Policy')
    $null = $lines.Add('| Lens Scope | Requested Reasoning / Review Class | Effective Class (when run) | Override / Approval Record | Notes |')
    $null = $lines.Add('| --- | --- | --- | --- | --- |')
    foreach ($routingRow in $Resolution.phase2_routing_policy) {
        $null = $lines.Add(('| {0} | `{1}` | {2} | {3} | {4} |' -f $routingRow.lens_scope, $routingRow.requested_review_class, $routingRow.effective_class, $routingRow.override_approval_record, $routingRow.notes))
    }
    $null = $lines.Add('')
    $null = $lines.Add('### Explicit Later Deferrals')
    foreach ($deferral in $Resolution.phase2_explicit_later_deferrals) {
        $null = $lines.Add(("- {0}" -f $deferral))
    }

    return $lines -join "`n"
}

$resolvedProjectPath = Resolve-ProjectPath -Path $ProjectPath
if (-not (Test-Path -LiteralPath $resolvedProjectPath -PathType Container)) {
    throw "Project path '$resolvedProjectPath' does not exist."
}

$resolvedFeaturePath = $null
if (-not [string]::IsNullOrWhiteSpace($FeaturePath)) {
    $resolvedFeaturePath = Resolve-ProjectPath -Path $FeaturePath
}
elseif (-not [string]::IsNullOrWhiteSpace($SpecPath)) {
    $resolvedFeaturePath = Split-Path -Parent (Resolve-ProjectPath -Path $SpecPath)
}

$resolvedSpecPath = $null
if (-not [string]::IsNullOrWhiteSpace($SpecPath)) {
    $resolvedSpecPath = Resolve-ProjectPath -Path $SpecPath
}
elseif (-not [string]::IsNullOrWhiteSpace($resolvedFeaturePath)) {
    $candidateSpecPath = Join-Path $resolvedFeaturePath 'spec.md'
    if (Test-Path -LiteralPath $candidateSpecPath -PathType Leaf) {
        $resolvedSpecPath = $candidateSpecPath
    }
}

$signals = Get-QualitySignals -ResolvedProjectPath $resolvedProjectPath -ResolvedFeaturePath $resolvedFeaturePath -ResolvedSpecPath $resolvedSpecPath
$candidates = @(Get-PresetCandidates -Signals $signals)
$selectedCandidate = $null
if ($candidates.Count -gt 0) {
    $selectedCandidate = $candidates[0]
}

$useRecognizedPreset = $false
if ($candidates.Count -eq 1) {
    $useRecognizedPreset = $true
}
elseif ($candidates.Count -gt 1) {
    $scoreGap = [int]$candidates[0].score - [int]$candidates[1].score
    $useRecognizedPreset = ($candidates[0].score -ge 85) -and ($scoreGap -ge 15)
}

$profile = if ($useRecognizedPreset -and $null -ne $selectedCandidate) {
    Get-PresetProfile -PresetId $selectedCandidate.preset_id -MatchedSignals @($selectedCandidate.matched_signals)
}
else {
    Get-CustomCompositionProfile -Signals $signals -Candidates $candidates
}

$riskResolution = Get-RiskResolution -Profile $profile -Signals $signals
$requiredQualityGates = Get-RequiredQualityGates -Profile $profile -RiskResolution $riskResolution
$qualityPlanningDefaults = Get-QualityPlanningDefaults -ResolvedProjectPath $resolvedProjectPath
$phaseTwoArtifactRefs = Get-PhaseTwoArtifactRefs -ResolvedProjectPath $resolvedProjectPath -ResolvedFeaturePath $resolvedFeaturePath -QualityDefaults $qualityPlanningDefaults
$presetRefs = [System.Collections.Generic.List[string]]::new()
if ($profile.preset_ref) {
    Add-UniqueItem -List $presetRefs -Value $profile.preset_ref
}

$manualEvidence = [System.Collections.Generic.List[string]]::new()
Add-UniqueItem -List $manualEvidence -Value 'feature plan Phase 1 quality planning section'
Add-UniqueItem -List $manualEvidence -Value 'specs/<feature>/iterations/<NNN>/quality/quality-evidence.md'

$resolution = [pscustomobject]@{
    schema_version            = 'v1'
    phase_scope               = 'phase-1-first-slice'
    project_path              = $resolvedProjectPath
    feature_path              = $resolvedFeaturePath
    spec_path                 = $resolvedSpecPath
    profile_id                = $profile.profile_id
    resolution_mode           = $(if ($profile.preset_ref) { 'preset' } else { 'bounded-custom-composition' })
    preset_refs               = $presetRefs.ToArray()
    custom_composition        = $(if ($profile.preset_ref) { $null } else { [pscustomobject]@{
                reason        = $profile.custom_reason
                lens_refs     = @($profile.custom_lens_refs)
                unknowns      = @($profile.unknowns)
                phase_boundary = 'Phase 1 only; no hardening gate, bug-hunter execution, strongest-class routing, or quality-drift logic is implied.'
            } })
    stack_signals             = @($candidates | ForEach-Object {
            [pscustomobject]@{
                preset_id       = $_.preset_id
                score           = $_.score
                matched_signals = @($_.matched_signals)
            }
        })
    stack_surfaces            = @($profile.stack_surfaces)
    risk_dimensions           = @($riskResolution.required)
    not_applicable_dimensions = @($riskResolution.not_applicable)
    required_lens_refs        = @($profile.required_lens_refs)
    custom_lens_refs          = @($profile.custom_lens_refs)
    tool_bundle               = [pscustomobject]@{
        bundle_id         = $profile.bundle_id
        mechanical_checks = @($profile.mechanical_checks)
        ecosystem_tools   = @($profile.ecosystem_tools)
        manual_evidence   = $manualEvidence.ToArray()
    }
    required_quality_gates    = @($requiredQualityGates)
    phase2_slice_scope        = 'US-2 hardening-gate planning only; pre-implementation readiness must accept planning-time analysis, expected controls, rationale, and explicit non-applicable reasoning, while runtime-only final proof stays pending until later closure or approved runtime-only deferment.'
    phase2_hardening_gate_artifact = $phaseTwoArtifactRefs.hardening_gate_artifact
    phase2_known_traps_corpus_location = $phaseTwoArtifactRefs.known_traps_corpus_location
    phase2_trap_reapplication_artifact = $phaseTwoArtifactRefs.trap_reapplication_artifact
    phase2_hardening_focus_areas = @(Get-PhaseTwoHardeningFocusAreas -RiskResolution $riskResolution -ArtifactRefs $phaseTwoArtifactRefs)
    phase2_lens_activation_plan = @(Get-PhaseTwoLensActivationPlan -Profile $profile -ArtifactRefs $phaseTwoArtifactRefs -QualityDefaults $qualityPlanningDefaults)
    phase2_routing_policy     = @(Get-PhaseTwoRoutingPolicy -QualityDefaults $qualityPlanningDefaults)
    phase2_explicit_later_deferrals = @(Get-PhaseTwoLaterDeferrals)
    phase2_deferrals          = Get-PhaseOneDeferrals
}
$resolution | Add-Member -NotePropertyName markdown_summary -NotePropertyValue (Convert-QualityProfileToMarkdown -Resolution $resolution)

switch ($OutputFormat) {
    'Json' {
        $resolution | ConvertTo-Json -Depth 10
    }
    'Markdown' {
        $resolution.markdown_summary
    }
    default {
        $resolution
    }
}