scripts/internal/code-implementation-lens.ps1
|
<#
.SYNOPSIS Code & Implementation lens manifest writer/validator + catalog/overlay helpers (Feature 177). The code-implementation lens captures implementation-craft decisions at design time into a per-feature manifest (implementation-rules.yml) that references the canonical catalog (code-rules.yml) by stable id. An implement-time guidance skill (specrew-code-rules) reads the manifest and composes baseline + overlay. YAML note: PowerShell 7 has no native YAML parser and Specrew deliberately avoids powershell-yaml. The manifest uses a CONSTRAINED YAML subset whose emitter (ConvertTo-SpecrewImplementationRulesYaml) and reader (ConvertFrom-SpecrewImplementationRulesYaml) are co-designed + round-trip-tested. Schema validation projects the parsed object to JSON and uses Test-Json -SchemaFile against implementation-rules.schema.json. Catalog/overlay ids are extracted by regex (no full YAML parse). Graceful: fail-open reads; the gate decides fail-open vs fail-closed. UTF-8 no-BOM. #> Set-StrictMode -Version Latest $script:SpecrewCodeRuleGroups = @('baseline-default', 'decision-prompt', 'applicability-filtered', 'enforcement-mode') $script:SpecrewCodeContextScopes = @('feature_standalone', 'product_baseline', 'feature_delta') $script:SpecrewCodeConfirmations = @('human-confirmed', 'human-delegated', 'human-skipped') $script:SpecrewCodeConfirmationScopes = @{ 'human-confirmed' = 'lens-question' 'human-delegated' = 'explicit-delegation' 'human-skipped' = 'explicit-skip' } $script:SpecrewCodeCustomProvenance = @('free-text', 'pasted-doc', 'from-guideline', 'from-example-project') $script:SpecrewCodeDependencyStances = @('use-existing-no-new-dependency', 'approved-new-dependencies') $script:SpecrewCodeDependencyFields = @('name', 'version', 'license', 'source_org', 'canonical_url', 'maintenance_signal', 'security_advisory_status', 'compatibility', 'cost_or_quota', 'coupling_weight', 'replaceability', 'test_implications') function Get-SpecrewCodeManifestPath { param([Parameter(Mandatory = $true)][string]$FeatureDir) return (Join-Path $FeatureDir 'implementation-rules.yml') } function Get-SpecrewCodeRecordPath { param([Parameter(Mandatory = $true)][string]$FeatureDir) return (Join-Path (Join-Path $FeatureDir 'workshop') 'code-implementation.md') } function Get-SpecrewCodeRuleEscape { param([AllowNull()][string]$Value) if ($null -eq $Value) { return '' } return (($Value -replace '\\', '\\' -replace '"', '\"') -replace '\r?\n', ' ') } function Get-SpecrewCodeRuleUnescape { param([AllowNull()][string]$Value) if ($null -eq $Value) { return '' } return ($Value -replace '\\"', '"' -replace '\\\\', '\') } function Get-SpecrewCodeMember { param([AllowNull()]$Object, [Parameter(Mandatory = $true)][string]$Key) if ($null -eq $Object) { return $null } if ($Object -is [System.Collections.IDictionary]) { if ($Object.Contains($Key)) { return $Object[$Key] } else { return $null } } $p = $Object.PSObject.Properties[$Key]; if ($p) { return $p.Value } else { return $null } } function ConvertTo-SpecrewCodeScalar { param([AllowNull()]$Value) if ($null -eq $Value) { return 'null' } if ($Value -is [bool]) { if ($Value) { return 'true' } else { return 'false' } } return ('"{0}"' -f (Get-SpecrewCodeRuleEscape -Value ([string]$Value))) } function ConvertTo-SpecrewImplementationRulesYaml { # Emit the constrained YAML for an implementation-rules manifest object. Deterministic key order, # 2-space indent, strings double-quoted, bool true/false, null -> null, enforcement as an inline list. [CmdletBinding()] param([Parameter(Mandatory = $true)][AllowNull()]$Manifest) if ($null -eq $Manifest) { return '' } $sb = [System.Text.StringBuilder]::new() foreach ($k in @('schema_version', 'context_scope', 'resolved_stack', 'product_id', 'product_context_ref')) { [void]$sb.AppendLine(('{0}: {1}' -f $k, (ConvertTo-SpecrewCodeScalar (Get-SpecrewCodeMember $Manifest $k)))) } [void]$sb.AppendLine('selections:') foreach ($s in @(Get-SpecrewCodeMember $Manifest 'selections')) { if ($null -eq $s) { continue } [void]$sb.AppendLine((' - id: {0}' -f (ConvertTo-SpecrewCodeScalar (Get-SpecrewCodeMember $s 'id')))) [void]$sb.AppendLine((' checked: {0}' -f (ConvertTo-SpecrewCodeScalar (Get-SpecrewCodeMember $s 'checked')))) $dec = Get-SpecrewCodeMember $s 'decision' if ($null -ne $dec) { [void]$sb.AppendLine((' decision: {0}' -f (ConvertTo-SpecrewCodeScalar $dec))) } $enf = @(Get-SpecrewCodeMember $s 'enforcement') if ($enf.Count -gt 0) { $items = ($enf | ForEach-Object { ConvertTo-SpecrewCodeScalar $_ }) -join ', ' [void]$sb.AppendLine((' enforcement: [{0}]' -f $items)) } } [void]$sb.AppendLine('custom_rules:') foreach ($c in @(Get-SpecrewCodeMember $Manifest 'custom_rules')) { if ($null -eq $c) { continue } [void]$sb.AppendLine((' - id: {0}' -f (ConvertTo-SpecrewCodeScalar (Get-SpecrewCodeMember $c 'id')))) [void]$sb.AppendLine((' text: {0}' -f (ConvertTo-SpecrewCodeScalar (Get-SpecrewCodeMember $c 'text')))) $sc = Get-SpecrewCodeMember $c 'scope' if ($null -ne $sc) { [void]$sb.AppendLine((' scope: {0}' -f (ConvertTo-SpecrewCodeScalar $sc))) } [void]$sb.AppendLine((' provenance: {0}' -f (ConvertTo-SpecrewCodeScalar (Get-SpecrewCodeMember $c 'provenance')))) } $dep = Get-SpecrewCodeMember $Manifest 'dependency_policy' if ($null -ne $dep) { [void]$sb.AppendLine('dependency_policy:') [void]$sb.AppendLine((' stance: {0}' -f (ConvertTo-SpecrewCodeScalar (Get-SpecrewCodeMember $dep 'stance')))) [void]$sb.AppendLine(' selected:') foreach ($d in @(Get-SpecrewCodeMember $dep 'selected')) { if ($null -eq $d) { continue } $first = $true foreach ($f in $script:SpecrewCodeDependencyFields) { $val = Get-SpecrewCodeMember $d $f if ($f -eq 'name' -or $null -ne $val) { $prefix = if ($first) { ' - ' } else { ' ' } [void]$sb.AppendLine(('{0}{1}: {2}' -f $prefix, $f, (ConvertTo-SpecrewCodeScalar $val))) $first = $false } } } } $prov = Get-SpecrewCodeMember $Manifest 'provenance' [void]$sb.AppendLine('provenance:') [void]$sb.AppendLine((' confirmation: {0}' -f (ConvertTo-SpecrewCodeScalar (Get-SpecrewCodeMember $prov 'confirmation')))) [void]$sb.AppendLine((' confirmation_scope: {0}' -f (ConvertTo-SpecrewCodeScalar (Get-SpecrewCodeMember $prov 'confirmation_scope')))) return $sb.ToString() } function ConvertFrom-SpecrewCodeScalar { param([AllowNull()][string]$Raw) if ($null -eq $Raw) { return $null } $t = $Raw.Trim() if ($t -eq 'null' -or $t -eq '') { return $null } if ($t -eq 'true') { return $true } if ($t -eq 'false') { return $false } if ($t.StartsWith('"') -and $t.EndsWith('"') -and $t.Length -ge 2) { return (Get-SpecrewCodeRuleUnescape -Value $t.Substring(1, $t.Length - 2)) } return $t } function ConvertFrom-SpecrewCodeInlineList { # Always returns an array. The leading-comma idiom (return ,$items) prevents PowerShell from # unwrapping a SINGLE-element array on function return -- otherwise enforcement: [review] is read # back as the scalar "review" and fails the schema's array type at the JSON projection. (Found by # the F-177 deployed-module dogfood; the unit round-trip only exercised a two-element list.) param([AllowNull()][string]$Raw) if ([string]::IsNullOrWhiteSpace($Raw)) { return ,@() } $t = $Raw.Trim() if ($t.StartsWith('[') -and $t.EndsWith(']')) { $t = $t.Substring(1, $t.Length - 2) } if ([string]::IsNullOrWhiteSpace($t)) { return ,@() } $items = @($t -split ',' | ForEach-Object { ConvertFrom-SpecrewCodeScalar -Raw $_ } | Where-Object { $null -ne $_ }) return ,$items } function ConvertFrom-SpecrewImplementationRulesYaml { # Matched reader for the constrained manifest YAML. Returns an ordered hashtable, or $null on a # structurally unreadable document (graceful -- the caller fails closed). [CmdletBinding()] param([Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return $null } $lines = $Text -split '\r?\n' $rec = [ordered]@{ selections = @(); custom_rules = @(); provenance = [ordered]@{} } $selections = [System.Collections.Generic.List[object]]::new() $customs = [System.Collections.Generic.List[object]]::new() $depSelected = [System.Collections.Generic.List[object]]::new() $dep = $null $section = 'top' $cur = $null foreach ($line in $lines) { if ($line -match '^\s*$') { continue } # Top-level key if ($line -match '^(?<k>[a-z_]+):\s*(?<v>.*)$') { $k = $Matches['k']; $v = $Matches['v'] switch ($k) { 'selections' { $section = 'selections'; $cur = $null; continue } 'custom_rules' { $section = 'customs'; $cur = $null; continue } 'dependency_policy' { $section = 'dep'; $dep = [ordered]@{}; $cur = $null; continue } 'provenance' { $section = 'provenance'; $cur = $null; continue } default { $section = 'top'; $rec[$k] = ConvertFrom-SpecrewCodeScalar -Raw $v; continue } } } if ($section -eq 'selections') { if ($line -match '^\s{2}-\s+id:\s*(?<v>.*)$') { $cur = [ordered]@{ id = ConvertFrom-SpecrewCodeScalar -Raw $Matches['v'] } $selections.Add($cur) | Out-Null; continue } if ($null -ne $cur -and $line -match '^\s{4}enforcement:\s*(?<v>.*)$') { $cur['enforcement'] = ConvertFrom-SpecrewCodeInlineList -Raw $Matches['v']; continue } if ($null -ne $cur -and $line -match '^\s{4}(?<k>[a-z_]+):\s*(?<v>.*)$') { $cur[$Matches['k']] = ConvertFrom-SpecrewCodeScalar -Raw $Matches['v']; continue } } if ($section -eq 'customs') { if ($line -match '^\s{2}-\s+id:\s*(?<v>.*)$') { $cur = [ordered]@{ id = ConvertFrom-SpecrewCodeScalar -Raw $Matches['v'] } $customs.Add($cur) | Out-Null; continue } if ($null -ne $cur -and $line -match '^\s{4}(?<k>[a-z_]+):\s*(?<v>.*)$') { $cur[$Matches['k']] = ConvertFrom-SpecrewCodeScalar -Raw $Matches['v']; continue } } if ($section -eq 'dep') { if ($line -match '^\s{2}stance:\s*(?<v>.*)$') { $dep['stance'] = ConvertFrom-SpecrewCodeScalar -Raw $Matches['v']; continue } if ($line -match '^\s{2}selected:\s*$') { $cur = $null; continue } if ($line -match '^\s{4}-\s+(?<k>[a-z_]+):\s*(?<v>.*)$') { $cur = [ordered]@{}; $cur[$Matches['k']] = ConvertFrom-SpecrewCodeScalar -Raw $Matches['v'] $depSelected.Add($cur) | Out-Null; continue } if ($null -ne $cur -and $line -match '^\s{6}(?<k>[a-z_]+):\s*(?<v>.*)$') { $cur[$Matches['k']] = ConvertFrom-SpecrewCodeScalar -Raw $Matches['v']; continue } } if ($section -eq 'provenance' -and $line -match '^\s{2}(?<k>[a-z_]+):\s*(?<v>.*)$') { $rec['provenance'][$Matches['k']] = ConvertFrom-SpecrewCodeScalar -Raw $Matches['v']; continue } } $rec['selections'] = $selections.ToArray() $rec['custom_rules'] = $customs.ToArray() if ($null -ne $dep) { $dep['selected'] = $depSelected.ToArray(); $rec['dependency_policy'] = $dep } return $rec } function Format-SpecrewCodeImplementationMarkdown { # Human-readable record (workshop/code-implementation.md) from a manifest object. [CmdletBinding()] param([Parameter(Mandatory = $true)][AllowNull()]$Manifest) $sb = [System.Text.StringBuilder]::new() [void]$sb.AppendLine('# Code & Implementation Record') [void]$sb.AppendLine('') [void]$sb.AppendLine(('**Resolved stack**: {0}' -f (Get-SpecrewCodeMember $Manifest 'resolved_stack'))) [void]$sb.AppendLine(('**Context scope**: {0}' -f (Get-SpecrewCodeMember $Manifest 'context_scope'))) [void]$sb.AppendLine('') [void]$sb.AppendLine('## Selected rules') [void]$sb.AppendLine('') foreach ($s in @(Get-SpecrewCodeMember $Manifest 'selections')) { if ($null -eq $s) { continue } $mark = if ([bool](Get-SpecrewCodeMember $s 'checked')) { 'x' } else { ' ' } $dec = Get-SpecrewCodeMember $s 'decision' $decTxt = if ($null -ne $dec) { (' -- {0}' -f $dec) } else { '' } [void]$sb.AppendLine(('- [{0}] {1}{2}' -f $mark, (Get-SpecrewCodeMember $s 'id'), $decTxt)) } $customs = @(Get-SpecrewCodeMember $Manifest 'custom_rules') if ($customs.Count -gt 0) { [void]$sb.AppendLine('') [void]$sb.AppendLine('## Custom rules') [void]$sb.AppendLine('') foreach ($c in $customs) { if ($null -ne $c) { [void]$sb.AppendLine(('- {0} ({1}): {2}' -f (Get-SpecrewCodeMember $c 'id'), (Get-SpecrewCodeMember $c 'provenance'), (Get-SpecrewCodeMember $c 'text'))) } } } $dep = Get-SpecrewCodeMember $Manifest 'dependency_policy' if ($null -ne $dep) { [void]$sb.AppendLine('') [void]$sb.AppendLine('## Dependency policy') [void]$sb.AppendLine('') [void]$sb.AppendLine(('- **Stance**: {0}' -f (Get-SpecrewCodeMember $dep 'stance'))) foreach ($d in @(Get-SpecrewCodeMember $dep 'selected')) { if ($null -ne $d) { [void]$sb.AppendLine(('- {0} {1} ({2})' -f (Get-SpecrewCodeMember $d 'name'), (Get-SpecrewCodeMember $d 'version'), (Get-SpecrewCodeMember $d 'license'))) } } } [void]$sb.AppendLine('') $prov = Get-SpecrewCodeMember $Manifest 'provenance' [void]$sb.AppendLine(('**Confirmation**: {0} / {1}' -f (Get-SpecrewCodeMember $prov 'confirmation'), (Get-SpecrewCodeMember $prov 'confirmation_scope'))) return $sb.ToString() } function New-SpecrewImplementationRulesManifest { # FR-004: persist the manifest (.yml) + the human-readable record (.md). Idempotent; UTF-8 no-BOM. [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$FeatureDir, [Parameter(Mandatory = $true)][AllowNull()]$Manifest, [switch]$Force ) if ($null -eq $Manifest) { throw 'New-SpecrewImplementationRulesManifest: -Manifest is required.' } $ymlPath = Get-SpecrewCodeManifestPath -FeatureDir $FeatureDir $mdPath = Get-SpecrewCodeRecordPath -FeatureDir $FeatureDir $mdDir = Split-Path -Parent $mdPath if (-not (Test-Path -LiteralPath $mdDir -PathType Container)) { $null = New-Item -ItemType Directory -Path $mdDir -Force } $utf8 = [System.Text.UTF8Encoding]::new($false) if ((Test-Path -LiteralPath $ymlPath -PathType Leaf) -and -not $Force) { if (-not (Test-Path -LiteralPath $mdPath -PathType Leaf)) { [System.IO.File]::WriteAllText($mdPath, (Format-SpecrewCodeImplementationMarkdown -Manifest $Manifest), $utf8) } return $ymlPath } [System.IO.File]::WriteAllText($ymlPath, (ConvertTo-SpecrewImplementationRulesYaml -Manifest $Manifest), $utf8) [System.IO.File]::WriteAllText($mdPath, (Format-SpecrewCodeImplementationMarkdown -Manifest $Manifest), $utf8) return $ymlPath } function Get-SpecrewCodeRuleIds { # Regex-extract the rule ids from a catalog/overlay YAML file (no full YAML parse). Graceful @(). [CmdletBinding()] param([Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][string]$Path) if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) { return @() } $ids = [System.Collections.Generic.List[string]]::new() foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8)) { if ($line -match '^\s*-\s+id:\s*(?<v>\S.*)$') { $ids.Add(($Matches['v'].Trim().Trim('"'))) | Out-Null } } return $ids.ToArray() } function Merge-SpecrewCodeRuleCatalog { # FR-012 overlay merge at the id level: shipped ids + overlay added ids, overlay overrides applied, # a shipped id is NEVER dropped. Returns @{ merged = string[]; dropped = string[]; added = string[] }. [CmdletBinding()] param( [Parameter(Mandatory = $true)][AllowNull()][string]$CatalogPath, [AllowNull()][string]$OverlayPath ) $shipped = @(Get-SpecrewCodeRuleIds -Path $CatalogPath) $overlay = @(Get-SpecrewCodeRuleIds -Path $OverlayPath) $merged = [System.Collections.Generic.List[string]]::new() foreach ($id in $shipped) { if (-not $merged.Contains($id)) { $merged.Add($id) | Out-Null } } $added = [System.Collections.Generic.List[string]]::new() foreach ($id in $overlay) { if (-not $merged.Contains($id)) { $merged.Add($id) | Out-Null; $added.Add($id) | Out-Null } } # A shipped id is never dropped (additive + override only): dropped is always empty by construction. $dropped = @($shipped | Where-Object { -not $merged.Contains($_) }) return @{ merged = $merged.ToArray(); dropped = @($dropped); added = $added.ToArray() } } function Test-SpecrewImplementationRulesManifest { # FR-004 / FR-013 / SC-002: validate the persisted manifest. Reads + parses the constrained YAML, # projects to JSON, validates against the schema (Test-Json) when present, and enforces invariants # (selections reference known ids when a catalog is given; provenance pairing; dependency-stance). # Returns string[] of errors (empty = OK). Missing manifest -> single 'missing' error (caller decides). [CmdletBinding()] param( [Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][string]$Path, [AllowNull()][AllowEmptyString()][string]$SchemaPath, [AllowNull()][AllowEmptyString()][string]$CatalogPath, [AllowNull()][AllowEmptyString()][string]$OverlayPath ) $errors = [System.Collections.Generic.List[string]]::new() if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) { $errors.Add(("implementation-rules manifest is missing: {0}" -f $Path)) | Out-Null return $errors.ToArray() } $text = '' try { $text = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 } catch { $errors.Add('implementation-rules.yml is unreadable.') | Out-Null; return $errors.ToArray() } $rec = ConvertFrom-SpecrewImplementationRulesYaml -Text $text if ($null -eq $rec) { $errors.Add('implementation-rules.yml could not be parsed.') | Out-Null; return $errors.ToArray() } # Schema validation via JSON projection (SC-002), when a schema is available. if (-not [string]::IsNullOrWhiteSpace($SchemaPath) -and (Test-Path -LiteralPath $SchemaPath -PathType Leaf)) { $json = $null try { $json = ($rec | ConvertTo-Json -Depth 10) } catch { $json = $null } if ($null -ne $json) { $schemaErrors = $null; $ok = $true try { $ok = Test-Json -Json $json -SchemaFile $SchemaPath -ErrorVariable schemaErrors -ErrorAction SilentlyContinue } catch { $ok = $false } if (-not $ok) { $detail = if ($schemaErrors) { ($schemaErrors | ForEach-Object { $_.ToString() }) -join '; ' } else { 'schema mismatch' } $errors.Add(("implementation-rules.yml fails the schema: {0}" -f $detail)) | Out-Null } } } # Invariant backstops (also in the schema; checked here so the gate is robust without the schema). $scope = if ($rec.Contains('context_scope')) { [string]$rec['context_scope'] } else { '' } if ($scope -notin $script:SpecrewCodeContextScopes) { $errors.Add(("context_scope must be one of {0} (got '{1}')." -f ($script:SpecrewCodeContextScopes -join ' | '), $scope)) | Out-Null } if ([string]::IsNullOrWhiteSpace([string]$rec['resolved_stack'])) { $errors.Add('resolved_stack is required.') | Out-Null } $prov = $rec['provenance'] $conf = [string](Get-SpecrewCodeMember $prov 'confirmation') if ($conf -notin $script:SpecrewCodeConfirmations) { $errors.Add(("provenance.confirmation must be one of {0} (got '{1}'); a batch 'confirm all' is NOT valid provenance." -f ($script:SpecrewCodeConfirmations -join ' | '), $conf)) | Out-Null } else { $expected = $script:SpecrewCodeConfirmationScopes[$conf] $scopeVal = [string](Get-SpecrewCodeMember $prov 'confirmation_scope') if ($scopeVal -ne $expected) { $errors.Add(("provenance.confirmation_scope must be '{0}' when confirmation is '{1}' (got '{2}')." -f $expected, $conf, $scopeVal)) | Out-Null } } foreach ($c in @($rec['custom_rules'])) { if ($null -eq $c) { continue } $cp = [string](Get-SpecrewCodeMember $c 'provenance') if ($cp -notin $script:SpecrewCodeCustomProvenance) { $errors.Add(("a custom rule has an invalid provenance '{0}' (must be {1})." -f $cp, ($script:SpecrewCodeCustomProvenance -join ' | '))) | Out-Null } } $dep = $rec['dependency_policy'] if ($null -ne $dep) { $stance = [string](Get-SpecrewCodeMember $dep 'stance') if ($stance -notin $script:SpecrewCodeDependencyStances) { $errors.Add(("dependency_policy.stance must be one of {0} (got '{1}')." -f ($script:SpecrewCodeDependencyStances -join ' | '), $stance)) | Out-Null } foreach ($d in @(Get-SpecrewCodeMember $dep 'selected')) { if ($null -ne $d -and [string]::IsNullOrWhiteSpace([string](Get-SpecrewCodeMember $d 'name'))) { $errors.Add('a dependency_policy.selected entry is missing its name.') | Out-Null } } } # Selections reference known catalog/overlay ids OR a declared custom rule id (when a catalog is given). if (-not [string]::IsNullOrWhiteSpace($CatalogPath) -and (Test-Path -LiteralPath $CatalogPath -PathType Leaf)) { $merge = Merge-SpecrewCodeRuleCatalog -CatalogPath $CatalogPath -OverlayPath $OverlayPath $known = [System.Collections.Generic.HashSet[string]]::new() foreach ($id in @($merge.merged)) { [void]$known.Add([string]$id) } foreach ($c in @($rec['custom_rules'])) { if ($null -ne $c) { [void]$known.Add([string](Get-SpecrewCodeMember $c 'id')) } } foreach ($s in @($rec['selections'])) { if ($null -eq $s) { continue } $sid = [string](Get-SpecrewCodeMember $s 'id') if (-not $known.Contains($sid)) { $errors.Add(("selection references unknown rule id '{0}' (not in the catalog/overlay/custom set)." -f $sid)) | Out-Null } } } return $errors.ToArray() } |