scripts/internal/specrew-consumer-language.ps1
|
$ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest # T010 / FR-015, FR-016 / SC-009 - THE CONSUMER LANGUAGE LAYER. # # The acceptance bar names the failure directly: a consumer completes their first feature without # hitting "a sentence they cannot understand". Three instances were measured in this feature, and they # are one defect in different clothes - a surface that is TRUE and still unusable: # # - the stale block named a run and a snapshot, every word accurate, and omitted the one fact that # made it dismissible; the reader investigated and found nothing # - its only remediation was addressed to a role the reader might not hold, so the block could never # be cleared and re-fired at every stop - and a block correctly declined every time teaches people # to stop reading blocks, which is how the one that matters gets missed # - a bare `T006` or `FR-013` is the same failure at sentence scale: an identifier the reader must go # and look up before the sentence means anything # # These helpers are deliberately PURE text functions with no lifecycle knowledge, so packet templates, # stop messages, skill instructions and the banner can all consult the same rules. function Format-SpecrewIdGloss { # An identifier never travels alone. REFUSING a missing description is the point: a helper that # silently passed the id through would let every caller emit exactly the defect it exists to # prevent, and the failure would surface as a confusing sentence rather than a broken build. [OutputType([string])] [CmdletBinding()] param( [Parameter(Mandatory)][AllowEmptyString()][string]$Id, [Parameter(Mandatory)][AllowEmptyString()][string]$Title ) if ([string]::IsNullOrWhiteSpace($Id)) { throw 'specrew-id-gloss-requires-an-id' } if ([string]::IsNullOrWhiteSpace($Title)) { throw ("specrew-id-gloss-requires-a-description: '{0}' must be written with a short description a reader can act on, e.g. '{0} (what it does)'" -f $Id.Trim()) } return ('{0} ({1})' -f $Id.Trim(), $Title.Trim()) } # The identifier shapes this project actually uses in consumer-facing prose. Deliberately NARROW: a # detector that also flagged version strings, commit hashes or ordinary words would be switched off # within a week, and a switched-off detector guards nothing. $script:SpecrewConsumerIdPattern = '\b(?<id>(?:T\d{3}|(?:FR|SC|NFR|US)-\d{3}))\b' function Get-SpecrewUnglossedId { # Returns the identifiers that appear WITHOUT a description. Only the FIRST use must be glossed: # requiring every occurrence would push authors toward dropping the id entirely, and the reader # would lose the handle they need to search the records. [OutputType([string[]])] [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyString()][string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return @() } $seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) $unglossed = [Collections.Generic.List[string]]::new() foreach ($match in [regex]::Matches($Text, $script:SpecrewConsumerIdPattern)) { $id = [string]$match.Groups['id'].Value if (-not $seen.Add($id)) { continue } # specrew-dedup-not-a-path # A gloss is a parenthesised description immediately after the id. An EMPTY pair of brackets is # not a description - it is the same bare id with decoration. $rest = $Text.Substring($match.Index + $match.Length) if ($rest -match '^\s*\((?<body>[^)]*)\)' -and -not [string]::IsNullOrWhiteSpace($Matches['body'])) { continue } $unglossed.Add($id) | Out-Null } return @($unglossed) } # Machinery vocabulary that must never reach a consumer surface. LIFECYCLE STAGE NAMES and approval # phrases are deliberately ABSENT: they are the words the human is asked to USE, and banning them would # make the approval instruction unspeakable. $script:SpecrewBannedConsumerNouns = @( 'crossing', 'mint', 'marker', 'digest', 'boundary sync', 'verdict capture', 'controller truth', 'workshop controller', 'controller plumbing', 'governed controller state', 'lens-applicability.json', 'ratchet', 'claim-ordered', 'terminalize' ) function Get-SpecrewBannedConsumerNoun { # Case-insensitive on purpose: a capitalised "Digest" is the same word to a reader. # # INFLECTIONS MATTER, and this was found by running the detector over its own release notes. The # first version matched `\bmint\b`, which reports CLEAN on "a fresh authorization was minted" - the # exact sentence the ban exists to catch. A detector that passes the inflected form of a banned word # is worse than none, because it certifies the text as checked. # # Suffixes are bounded deliberately (s / ed / ing / e / es). A looser rule would start matching # unrelated words and get the detector switched off, which guards nothing. [OutputType([string[]])] [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyString()][string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return @() } $found = [Collections.Generic.List[string]]::new() foreach ($noun in $script:SpecrewBannedConsumerNouns) { $pattern = '\b' + [regex]::Escape($noun) + '(?:e?s|ed|ing)?\b' if ([regex]::IsMatch($Text, $pattern, 'IgnoreCase')) { $found.Add($noun) | Out-Null } } return @($found) } function Get-SpecrewUnprovenFaultAttribution { # A refusal may state what the agent could not complete. It may not diagnose Specrew as broken or # at fault in the message shown to the human: the emitting agent does not have enough evidence to # distinguish its own missed step from inconsistent project records or a product defect. Diagnosis # belongs in the drift record, where evidence can support it. [OutputType([string[]])] [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyString()][string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return @() } $patterns = @( '\bSpecrew\b.{0,100}\b(?:broken|buggy|at fault|has (?:a )?problem|failed)\b', '\b(?:broken|bug|fault|problem|failure)\b.{0,100}\b(?:in|with)\s+Specrew\b' ) $found = [Collections.Generic.List[string]]::new() foreach ($pattern in $patterns) { foreach ($match in [regex]::Matches($Text, $pattern, 'IgnoreCase')) { $found.Add($match.Value) | Out-Null } } return @($found) } |