Closing the intent-to-code chasm - specification-driven development with BDD verification chain
Overall
score
96%
Does it follow best practices?
Validation for skill structure
param( [Parameter(Position = 0)] [string]$Command,
[Parameter(Position = 1)]
[string]$FilePath,
[Parameter(Position = 2)]
[string]$ContextFile,
[Parameter(Position = 3)]
[string]$ConstitutionFile)
$ErrorActionPreference = "Stop"
function Get-TddAssessment { param([string]$ConstitutionFile)
if (-not (Test-Path $ConstitutionFile)) {
return @{
error = "Constitution file not found"
} | ConvertTo-Json
}
$content = Get-Content $ConstitutionFile -Raw
# Initialize assessment
$determination = "optional"
$confidence = "high"
$evidence = ""
$reasoning = "No TDD indicators found in constitution"
# Check for strong TDD indicators with MUST/REQUIRED
if ($content -match "MUST.*(TDD|test-first|red-green-refactor|write tests before)") {
$determination = "mandatory"
$confidence = "high"
$evidence = $Matches[0]
$reasoning = "Strong TDD indicator found with MUST modifier"
}
elseif ($content -match "(TDD|test-first|red-green-refactor|write tests before).*MUST") {
$determination = "mandatory"
$confidence = "high"
$evidence = $Matches[0]
$reasoning = "Strong TDD indicator found with MUST modifier"
}
# Check for moderate indicators
elseif ($content -match "MUST.*(test-driven|tests.*before.*code|tests.*before.*implementation)") {
$determination = "mandatory"
$confidence = "medium"
$evidence = $Matches[0]
$reasoning = "Moderate TDD indicator found with MUST modifier"
}
# Check for prohibition indicators (both word orders)
elseif ($content -match "MUST.*(test-after|integration tests only|no unit tests)") {
$determination = "forbidden"
$confidence = "high"
$evidence = $Matches[0]
$reasoning = "TDD prohibition indicator found"
}
elseif ($content -match "(test-after|integration tests only|no unit tests).*MUST") {
$determination = "forbidden"
$confidence = "high"
$evidence = $Matches[0]
$reasoning = "TDD prohibition indicator found"
}
# Check for implicit indicators (SHOULD)
elseif ($content -match "SHOULD.*(quality gates|coverage|test)") {
$determination = "optional"
$confidence = "low"
$evidence = $Matches[0]
$reasoning = "Implicit testing indicator found with SHOULD modifier"
}
return @{
determination = $determination
confidence = $confidence
evidence = $evidence
reasoning = $reasoning
} | ConvertTo-Json}
function Get-AssertionContent { param([string]$InputPath)
if (Test-Path -Path $InputPath -PathType Container) {
# Directory input: glob all .feature files, sorted by name
$files = Get-ChildItem "$InputPath/*.feature" -ErrorAction SilentlyContinue | Sort-Object Name
if (-not $files -or $files.Count -eq 0) {
return ""
}
# Extract step lines in document order per file, normalize whitespace
$allLines = @()
foreach ($f in $files) {
$lines = Get-Content $f.FullName | Where-Object { $_ -match '^\s*(Given|When|Then|And|But) ' }
foreach ($line in $lines) {
# Normalize: strip leading whitespace, collapse internal whitespace, trim trailing
$normalized = $line.Trim() -replace '\s{2,}', ' '
$allLines += $normalized
}
}
return ($allLines -join "`n")
}
elseif (Test-Path -Path $InputPath -PathType Leaf) {
if ($InputPath -like "*.feature") {
# Single .feature file input
$content = Get-Content $InputPath
$allLines = @()
$lines = $content | Where-Object { $_ -match '^\s*(Given|When|Then|And|But) ' }
foreach ($line in $lines) {
$normalized = $line.Trim() -replace '\s{2,}', ' '
$allLines += $normalized
}
return ($allLines -join "`n")
}
else {
# Legacy test-specs.md input: extract **Given**:/**When**:/**Then**: lines
$content = Get-Content $InputPath
$assertions = $content | Where-Object { $_ -match '^\*\*(Given|When|Then)\*\*:' } |
ForEach-Object { $_.TrimEnd() } |
Sort-Object
return ($assertions -join "`n")
}
}
else {
return ""
}}
function Get-AssertionHash { param([string]$InputPath)
$assertions = Get-AssertionContent -InputPath $InputPath
if ([string]::IsNullOrEmpty($assertions)) {
return "NO_ASSERTIONS"
}
# Compute SHA256 hash
$bytes = [System.Text.Encoding]::UTF8.GetBytes($assertions)
$sha256 = [System.Security.Cryptography.SHA256]::Create()
$hashBytes = $sha256.ComputeHash($bytes)
$hash = [BitConverter]::ToString($hashBytes) -replace '-', ''
return $hash.ToLower()}
function Get-ContextPath { param([string]$InputPath)
if (Test-Path -Path $InputPath -PathType Container) {
# Directory input: tests/features/ -> go up 2 levels
$parentDir = Split-Path $InputPath -Parent # tests/
$featureDir = Split-Path $parentDir -Parent # specs/NNN-feature/
return Join-Path $featureDir "context.json"
}
elseif ($InputPath -like "*.feature") {
# Single .feature file: tests/features/x.feature -> go up 3 levels
$featuresDir = Split-Path $InputPath -Parent # tests/features/
$testsDir = Split-Path $featuresDir -Parent # tests/
$featureDir = Split-Path $testsDir -Parent # specs/NNN-feature/
return Join-Path $featureDir "context.json"
}
else {
# Legacy: tests/test-specs.md -> go up 2 levels
$testsDir = Split-Path $InputPath -Parent # tests/
$featureDir = Split-Path $testsDir -Parent # specs/NNN-feature/
return Join-Path $featureDir "context.json"
}}
function Set-AssertionHash { param( [string]$InputPath, [string]$ContextFile # Legacy param — ignored, path is derived ) $ContextFile = Get-ContextPath -InputPath $InputPath
$hash = Get-AssertionHash -InputPath $InputPath
$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
# Create context file if it doesn't exist
if (-not (Test-Path $ContextFile)) {
@{} | ConvertTo-Json | Set-Content $ContextFile
}
# Read existing context
$context = Get-Content $ContextFile -Raw | ConvertFrom-Json
# Build testify data based on input type
if (Test-Path -Path $InputPath -PathType Container) {
# Directory input: store features_dir and file_count
$fileCount = (Get-ChildItem "$InputPath/*.feature" -ErrorAction SilentlyContinue).Count
$testifyData = @{
assertion_hash = $hash
generated_at = $timestamp
features_dir = $InputPath
file_count = $fileCount
}
}
else {
# Legacy file input: store test_specs_file
$testifyData = @{
assertion_hash = $hash
generated_at = $timestamp
test_specs_file = $InputPath
}
}
# Handle PSCustomObject conversion
if ($context -is [PSCustomObject]) {
$context | Add-Member -NotePropertyName "testify" -NotePropertyValue $testifyData -Force
} else {
$context = @{ testify = $testifyData }
}
$context | ConvertTo-Json -Depth 10 | Set-Content $ContextFile
return $hash}
function Test-AssertionHash { param( [string]$InputPath, [string]$ContextFile # Legacy param — ignored, path is derived ) $ContextFile = Get-ContextPath -InputPath $InputPath
# Check if context file exists
if (-not (Test-Path $ContextFile)) {
return "missing"
}
# Read context
$context = Get-Content $ContextFile -Raw | ConvertFrom-Json
# Check if testify section exists
if (-not $context.testify -or -not $context.testify.assertion_hash) {
return "missing"
}
$storedHash = $context.testify.assertion_hash
# Compute current hash
$currentHash = Get-AssertionHash -InputPath $InputPath
if ($storedHash -eq $currentHash) {
return "valid"
} else {
return "invalid"
}}
$GIT_NOTES_REF = "refs/notes/testify"
function Test-GitRepo { try { $null = git rev-parse --git-dir 2>$null return $LASTEXITCODE -eq 0 } catch { return $false } }
function Set-GitNote { param([string]$InputPath)
if (-not (Test-GitRepo)) {
return "ERROR:NOT_GIT_REPO"
}
$hash = Get-AssertionHash -InputPath $InputPath
$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
# Create note content
$noteContent = @"testify-hash: $hash generated-at: $timestamp test-specs-file: $InputPath "@
# Store as git note on HEAD
try {
$noteContent | git notes --ref=$GIT_NOTES_REF add -f -F - HEAD 2>$null
if ($LASTEXITCODE -eq 0) {
return $hash
} else {
return "ERROR:GIT_NOTE_FAILED"
}
} catch {
return "ERROR:GIT_NOTE_FAILED"
}}
function Test-GitNote { param([string]$InputPath)
if (-not (Test-GitRepo)) {
return "ERROR:NOT_GIT_REPO"
}
# Get the git note for HEAD
try {
$noteContent = git notes --ref=$GIT_NOTES_REF show HEAD 2>$null
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrEmpty($noteContent)) {
return "missing"
}
} catch {
return "missing"
}
# Extract hash from note
$storedHash = ($noteContent | Where-Object { $_ -match '^testify-hash:' }) -replace '^testify-hash:\s*', ''
if ([string]::IsNullOrEmpty($storedHash)) {
return "missing"
}
# Compute current hash
$currentHash = Get-AssertionHash -InputPath $InputPath
if ($storedHash -eq $currentHash) {
return "valid"
} else {
return "invalid"
}}
function Test-GitDiff { param([string]$InputPath)
if (-not (Test-GitRepo)) {
return "ERROR:NOT_GIT_REPO"
}
if (-not (Test-Path $InputPath)) {
return "ERROR:FILE_NOT_FOUND"
}
# Check if file is tracked by git
try {
$null = git ls-files --error-unmatch $InputPath 2>$null
if ($LASTEXITCODE -ne 0) {
return "untracked"
}
} catch {
return "untracked"
}
# Get diff of the file against HEAD
try {
$diffOutput = git diff HEAD -- $InputPath 2>$null
if ($LASTEXITCODE -ne 0) {
return "ERROR:GIT_DIFF_FAILED"
}
} catch {
return "ERROR:GIT_DIFF_FAILED"
}
# If no diff at all, file is clean
if ([string]::IsNullOrEmpty($diffOutput)) {
return "clean"
}
# Check if any Given/When/Then lines were modified
if ($diffOutput -match '^[+-]\*\*(Given|When|Then)\*\*:') {
return "modified"
} else {
return "clean"
}}
function Get-ComprehensiveIntegrityCheck { param( [string]$InputPath, [string]$ContextFile, # Legacy param — ignored, path is derived [string]$ConstitutionFile ) $ContextFile = Get-ContextPath -InputPath $InputPath
$hashResult = "skipped"
$gitNoteResult = "skipped"
$gitDiffResult = "skipped"
$tddDetermination = "unknown"
$overallStatus = "unknown"
$blockReason = ""
# Get TDD determination from constitution
if (Test-Path $ConstitutionFile) {
$tddJson = Get-TddAssessment -ConstitutionFile $ConstitutionFile | ConvertFrom-Json
$tddDetermination = $tddJson.determination
if ([string]::IsNullOrEmpty($tddDetermination)) {
$tddDetermination = "unknown"
}
}
# Check context.json hash
if (Test-Path $ContextFile) {
$hashResult = Test-AssertionHash -InputPath $InputPath -ContextFile $ContextFile
} else {
$hashResult = "missing"
}
# Check git-based integrity (if in git repo and input is a file)
# Git note/diff checks only apply to individual files, not directories
if (Test-GitRepo) {
if (Test-Path -Path $InputPath -PathType Leaf) {
$gitNoteResult = Test-GitNote -InputPath $InputPath
$gitDiffResult = Test-GitDiff -InputPath $InputPath
} else {
# For directory input, git note/diff not applicable (multiple files)
$gitNoteResult = "skipped"
$gitDiffResult = "skipped"
}
}
# Determine overall status
if ($hashResult -eq "invalid" -or $gitNoteResult -eq "invalid") {
$overallStatus = "BLOCKED"
$blockReason = "Assertions were modified since testify ran"
} elseif ($gitDiffResult -eq "modified") {
$overallStatus = "BLOCKED"
$blockReason = "Uncommitted changes to assertions detected"
} elseif ($tddDetermination -eq "mandatory") {
if ($hashResult -eq "missing" -and $gitNoteResult -ne "valid") {
$overallStatus = "BLOCKED"
$blockReason = "TDD is mandatory but no integrity hash found"
} else {
$overallStatus = "PASS"
}
} else {
if ($hashResult -eq "valid" -or $gitNoteResult -eq "valid") {
$overallStatus = "PASS"
} elseif ($hashResult -eq "missing" -and $gitNoteResult -ne "valid") {
$overallStatus = "WARN"
$blockReason = "No integrity hash found (TDD is optional)"
} else {
$overallStatus = "PASS"
}
}
return @{
overall_status = $overallStatus
block_reason = $blockReason
tdd_determination = $tddDetermination
checks = @{
context_hash = $hashResult
git_note = $gitNoteResult
git_diff = $gitDiffResult
}
} | ConvertTo-Json -Depth 3}
function Get-TddDetermination { param([string]$ConstitutionFile)
if (-not (Test-Path $ConstitutionFile)) {
return "unknown"
}
$tddJson = Get-TddAssessment -ConstitutionFile $ConstitutionFile | ConvertFrom-Json
$determination = $tddJson.determination
if ([string]::IsNullOrEmpty($determination)) {
return "unknown"
}
return $determination}
function Get-AcceptanceScenarioCount { param([string]$SpecFile)
if (-not (Test-Path $SpecFile)) {
return 0
}
$content = Get-Content $SpecFile -Raw
# Remove HTML comments before counting
$content = $content -replace '<!--.*?-->', ''
$patterns = @(
"\*\*Given\*\*",
"\*\*When\*\*"
)
$count = 0
foreach ($pattern in $patterns) {
$matches = [regex]::Matches($content, $pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$count += $matches.Count
}
return $count}
function Test-HasAcceptanceScenarios { param([string]$SpecFile)
$count = Get-AcceptanceScenarioCount -SpecFile $SpecFile
return ($count -gt 0).ToString().ToLower()}
switch ($Command) { "assess-tdd" { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 assess-tdd <constitution-file>" exit 1 } Get-TddAssessment -ConstitutionFile $FilePath } "get-tdd-determination" { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 get-tdd-determination <constitution-file>" exit 1 } Get-TddDetermination -ConstitutionFile $FilePath } "count-scenarios" { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 count-scenarios <spec-file>" exit 1 } Get-AcceptanceScenarioCount -SpecFile $FilePath } "has-scenarios" { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 has-scenarios <spec-file>" exit 1 } Test-HasAcceptanceScenarios -SpecFile $FilePath } "extract-assertions" { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 extract-assertions <features-dir-or-file>" exit 1 } Get-AssertionContent -InputPath $FilePath } "compute-hash" { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 compute-hash <features-dir-or-file>" exit 1 } Get-AssertionHash -InputPath $FilePath } { $_ -in "store-hash", "rehash" } { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 store-hash <features-dir-or-file>" exit 1 } Set-AssertionHash -InputPath $FilePath } "verify-hash" { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 verify-hash <features-dir-or-file>" exit 1 } Test-AssertionHash -InputPath $FilePath } "store-git-note" { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 store-git-note <test-specs-file>" exit 1 } Set-GitNote -InputPath $FilePath } "verify-git-note" { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 verify-git-note <test-specs-file>" exit 1 } Test-GitNote -InputPath $FilePath } "check-git-diff" { if (-not $FilePath) { Write-Error "Usage: testify-tdd.ps1 check-git-diff <test-specs-file>" exit 1 } Test-GitDiff -InputPath $FilePath } "comprehensive-check" { if (-not $FilePath -or -not $ContextFile) { Write-Error "Usage: testify-tdd.ps1 comprehensive-check <features-dir-or-file> <constitution-file>" exit 1 } Get-ComprehensiveIntegrityCheck -InputPath $FilePath -ConstitutionFile $ContextFile } default { Write-Host "Unknown command: $Command" Write-Host "" Write-Host "Available commands:" Write-Host " TDD Assessment:" Write-Host " assess-tdd <constitution-file> - Full TDD assessment (JSON)" Write-Host " get-tdd-determination <constitution> - Just the determination" Write-Host " Scenario Counting:" Write-Host " count-scenarios <spec-file> - Count acceptance scenarios" Write-Host " has-scenarios <spec-file> - Check if scenarios exist" Write-Host " Hash-based Integrity (context.json auto-derived from input path):" Write-Host " extract-assertions <dir-or-file> - Extract step lines (.feature dir/file or legacy .md)" Write-Host " compute-hash <dir-or-file> - Compute SHA256 hash" Write-Host " store-hash|rehash <dir-or-file> - Atomic compute + store hash in feature's context.json" Write-Host " verify-hash <dir-or-file> - Verify against feature's context.json" Write-Host " Git-based Integrity (tamper-resistant):" Write-Host " store-git-note <test-specs-file> - Store hash as git note" Write-Host " verify-git-note <test-specs-file> - Verify against git note" Write-Host " check-git-diff <test-specs-file> - Check uncommitted changes" Write-Host " Comprehensive:" Write-Host " comprehensive-check <dir-or-file> <constitution-file>" exit 1 } }
Install with Tessl CLI
npx tessl i tessl-labs/intent-integrity-kit@2.3.5rules
skills
iikit-00-constitution
scripts
iikit-01-specify
iikit-02-clarify
iikit-03-plan
iikit-04-checklist
scripts
dashboard
iikit-05-testify
iikit-06-tasks
iikit-07-analyze
iikit-08-implement
iikit-09-taskstoissues
iikit-bugfix
scripts
iikit-core
scripts
bash
dashboard
powershell