diff --git a/README.md b/README.md index ed7f9998..f797b70b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Install all skills for any supported agent: The release inventory is the tracked `skills/assistant-*` set. `skills/unity-*` directories are local-only and ignored by git; they are not installed or validated as framework release skills. -Install a single skill: +Install one root skill with its bundled hard dependencies: ```bash ./install.sh --agent claude --skill assistant-thinking ``` @@ -65,7 +65,7 @@ Install the complete release inventory for one agent: .\install.ps1 -Agent gemini ``` -The same entry point supports a single skill and a non-mutating preview: +The same entry point supports one root skill with its bundled hard dependencies and a non-mutating preview: ```powershell .\install.ps1 -Agent claude -Skill assistant-thinking @@ -138,6 +138,17 @@ makes quality claims falsifiable with workload/budget/measurement, and travels through the plan, task handoff, and independent review. It does not add a permanent architect agent or force architecture ceremony onto local work. +### Shared-change impact + +Behavior-bearing work records a compact local/shared/unresolved assessment before +choosing verification scope. Triggered shared-impact work uses one installed +Node 22 common checker and carries the same artifact identity through workflow, +debugging, and review. Discovery may report gaps; completion also needs actual +current verification and the existing assistant-review manifest/coverage result. +The checker validates closure against supplied bounded evidence, not a universal +runtime dependency graph. Shared fanout alone does not add Architecture Decision +Pack, strict-lane, or QA requirements. + For multi-slice work, Assistant Workflow uses descriptive outcome-oriented slice identifiers rather than ordinal-only labels, explicit dependencies, and native subagents only for independent packets. diff --git a/docs/evals/variants/workflow-kernel-v1/manifest.json b/docs/evals/variants/workflow-kernel-v1/manifest.json index f00df2c5..a34735fa 100644 --- a/docs/evals/variants/workflow-kernel-v1/manifest.json +++ b/docs/evals/variants/workflow-kernel-v1/manifest.json @@ -22,14 +22,14 @@ "raw_response_retained": false }, "static_measurement": { - "baseline_selected_initial_words": 2183, - "candidate_selected_initial_words": 455, - "selected_initial_word_delta": -1728, - "baseline_total_initial_words": 2772, - "candidate_total_initial_words": 1044, - "baseline_selected_entry_words": 3788, - "candidate_selected_entry_words": 2060, - "selected_entry_word_delta": -1728, + "baseline_selected_initial_words": 2318, + "candidate_selected_initial_words": 488, + "selected_initial_word_delta": -1830, + "baseline_total_initial_words": 2907, + "candidate_total_initial_words": 1077, + "baseline_selected_entry_words": 3930, + "candidate_selected_entry_words": 2100, + "selected_entry_word_delta": -1830, "standing_context_growth": 0, "measured_with": "tools/context-budget-report.sh --skill-overlay" }, diff --git a/docs/skill-contract-design-guide.md b/docs/skill-contract-design-guide.md index 08977c58..7456267a 100644 --- a/docs/skill-contract-design-guide.md +++ b/docs/skill-contract-design-guide.md @@ -322,11 +322,12 @@ gates: checkpoint_start: "--- PHASE: NAME ---" checkpoint_end: "--- PHASE: NAME COMPLETE ---" condition: "when this phase runs" # optional + entry_assertions: [] # Optional; same schema, checked before any mutation or mutation dispatch exit_assertions: - - id: XX1 # unique ID for referencing - check: "what must be true" # plain-English assertion - condition: "when applicable" # optional — scopes the assertion - on_fail: "corrective action" # what to do if assertion fails + - id: XX1 # unique + check: "what must be true" + condition: "when applicable" # optional + on_fail: "corrective action" invariants: - id: INV1 @@ -512,3 +513,7 @@ encode arbitrary structures. Recipe, typed refs, separate code-review/QA handoffs, pivot/restart decisions, and the max 10 terminal cap when the Process skill has long-running review, QA, or fix-verify loops +15. **Shared-impact evidence** — preserve identity. Discovery reports gaps; + mutation requires current valid pre-build entry evidence; completion needs + verification/review. Missing loaded-path runtimes block. Fanout alone + does not escalate architecture, QA, or lane. diff --git a/install.ps1 b/install.ps1 index e6f76b3d..87950a37 100644 --- a/install.ps1 +++ b/install.ps1 @@ -12,7 +12,7 @@ configuration is preserved while installer-owned sections are refreshed. Target agent: claude, codex, or gemini. Agent names are case-insensitive. .PARAMETER Skill -Install one root assistant-* skill instead of the complete inventory. +Install one root assistant-* skill and its bundled hard dependencies instead of the complete inventory. .PARAMETER DryRun Validate and report the planned work without changing the filesystem. @@ -84,7 +84,7 @@ function Show-Usage { Usage: .\install.ps1 -Agent [options] Options: - -Skill Install one assistant-* skill + -Skill Install one root assistant-* skill plus bundled hard dependencies -DryRun Validate and show work without changing files -NoHooks Deprecated no-op; installs are hookless -Help Show this help @@ -1839,28 +1839,63 @@ function Install-Skills { } } +function Get-CanonicalSkillRequirements { + param([string]$SkillFile) + $inFrontmatter = $false + $inRequires = $false + foreach ($line in Get-Content -LiteralPath $SkillFile) { + if ($line -eq '---') { + if ($inFrontmatter) { break } + $inFrontmatter = $true + continue + } + if (-not $inFrontmatter) { continue } + if ($line -eq 'requires:') { $inRequires = $true; continue } + if ($inRequires -and $line -match '^\s*-\s*(.+?)\s*$') { + $Matches[1] + continue + } + $inRequires = $false + } +} + +function Resolve-BundledSkillClosure { + param( + [string[]]$RootSkills, + [string[]]$Inventory, + [string]$SourceRoot, + [string]$TargetRoot + ) + $pending = New-Object 'System.Collections.Generic.List[string]' + $selected = New-Object 'System.Collections.Generic.List[string]' + $preflighted = New-Object 'System.Collections.Generic.List[string]' + $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal) + foreach ($rootSkill in $RootSkills) { $pending.Add($rootSkill) } + + for ($index = 0; $index -lt $pending.Count; $index += 1) { + $skillName = $pending[$index] + if (-not $seen.Add($skillName)) { continue } + $source = Join-Path $SourceRoot $skillName + $target = Join-Path $TargetRoot $skillName + [void](Assert-ManagedDirectoryCopySafe -Source $source -Target $target -ManagedRoot $TargetRoot -Label $skillName) + $preflighted.Add($skillName) + $selected.Add($skillName) + foreach ($dependency in @(Get-CanonicalSkillRequirements -SkillFile (Join-Path $source 'SKILL.md'))) { + if (($Inventory -ccontains $dependency) -and -not $seen.Contains($dependency) -and -not $pending.Contains($dependency)) { + $pending.Add($dependency) + } + } + } + return (New-Object PSObject -Property @{ SkillNames = @($selected); PreflightedNames = @($preflighted) }) +} + function Write-DependencyNotes { param([string[]]$SkillNames, [string]$SourceRoot, [string]$TargetRoot) foreach ($skillName in $SkillNames) { $skillFile = Join-Path (Join-Path $SourceRoot $skillName) 'SKILL.md' - $inFrontmatter = $false - $inRequires = $false - foreach ($line in Get-Content -LiteralPath $skillFile) { - if ($line -eq '---') { - if ($inFrontmatter) { break } - $inFrontmatter = $true - continue - } - if (-not $inFrontmatter) { continue } - if ($line -eq 'requires:') { $inRequires = $true; continue } - if ($inRequires -and $line -match '^\s*-\s*(.+?)\s*$') { - $dependency = $Matches[1] - if (($SkillNames -notcontains $dependency) -and -not (Test-Path -LiteralPath (Join-Path $TargetRoot $dependency) -PathType Container)) { - Write-Info "NOTE: $skillName requires '$dependency', which is not selected or installed." - } - } - elseif ($inRequires) { - $inRequires = $false + foreach ($dependency in @(Get-CanonicalSkillRequirements -SkillFile $skillFile)) { + if (($SkillNames -notcontains $dependency) -and -not (Test-Path -LiteralPath (Join-Path $TargetRoot $dependency) -PathType Container)) { + Write-Info "NOTE: $skillName requires '$dependency', which is not selected or installed." } } } @@ -2223,7 +2258,7 @@ function Invoke-AssistantFrameworkInstall { $selectedSkills = @($inventory) if (-not [string]::IsNullOrWhiteSpace($Skill)) { if ($inventory -notcontains $Skill) { throw "Unknown skill '$Skill'. Available: $($inventory -join ', ')" } - $selectedSkills = @($Skill) + $selectedSkills = @($inventory | Where-Object { $_ -ieq $Skill }) } if ($agentName -eq 'codex' -and -not [string]::IsNullOrWhiteSpace($env:CODEX_HOME)) { @@ -2251,13 +2286,18 @@ function Invoke-AssistantFrameworkInstall { if ($agentName -eq 'codex') { $agentsRoot = Get-FullPath -LiteralPath (Join-Path $script:UserHome '.agents') $skillsTarget = Join-Path $agentsRoot 'skills' + $codexCommonToolsTarget = Join-Path $agentsRoot 'tools' [void](Assert-SafeManagedChild -LiteralPath $skillsTarget -ManagedRoot $agentsRoot -Purpose 'Codex skills root') + [void](Assert-SafeManagedChild -LiteralPath $codexCommonToolsTarget -ManagedRoot $agentsRoot -Purpose 'Codex common tools root') Assert-NoSourceTargetOverlap -Source $agentHome -Target $skillsTarget -Purpose 'Codex agent home and shared skills root' } else { $skillsTarget = Join-Path $agentHome 'skills' [void](Assert-SafeManagedChild -LiteralPath $skillsTarget -ManagedRoot $agentHome -Purpose "$agentName skills root") } + $skillSelection = Resolve-BundledSkillClosure -RootSkills $selectedSkills -Inventory $inventory -SourceRoot $skillsSource -TargetRoot $skillsTarget + $selectedSkills = @($skillSelection.SkillNames) + $preflightedSkills = @($skillSelection.PreflightedNames) $toolsTarget = Join-Path $agentHome 'tools' $evalDocsTarget = Join-Path (Join-Path $agentHome 'docs') 'evals' $hooksRoot = Join-Path $agentHome 'hooks' @@ -2282,6 +2322,7 @@ function Invoke-AssistantFrameworkInstall { Write-Info "Skills target: $skillsTarget" $toolsSource = Join-Path $script:FrameworkDir 'tools' + $changeImpactToolsSource = Join-Path $toolsSource 'change-impact' $toolExclusions = @('.DS_Store', '.publish', 'bin', 'obj') $evalDocsSource = Join-Path (Join-Path $script:FrameworkDir 'docs') 'evals' $sourceOnlyFiles = @( @@ -2309,11 +2350,16 @@ function Invoke-AssistantFrameworkInstall { Assert-JsonFilePropertyIdentitySafe -LiteralPath $legacySettings foreach ($skillName in $selectedSkills) { - [void](Assert-ManagedDirectoryCopySafe -Source (Join-Path $skillsSource $skillName) -Target (Join-Path $skillsTarget $skillName) -ManagedRoot $skillsTarget -Label $skillName) + if ($preflightedSkills -notcontains $skillName) { + [void](Assert-ManagedDirectoryCopySafe -Source (Join-Path $skillsSource $skillName) -Target (Join-Path $skillsTarget $skillName) -ManagedRoot $skillsTarget -Label $skillName) + } } if (Test-Path -LiteralPath $toolsSource -PathType Container) { Assert-ManagedTopLevelEntriesSafe -Source $toolsSource -Target $toolsTarget -ManagedRoot $agentHome -ExcludedNames $toolExclusions -ExcludedExactPaths $sourceOnly -Label 'Tools' } + if ($agentName -eq 'codex' -and (Test-Path -LiteralPath $changeImpactToolsSource -PathType Container)) { + [void](Assert-ManagedDirectoryCopySafe -Source $changeImpactToolsSource -Target (Join-Path $codexCommonToolsTarget 'change-impact') -ManagedRoot $agentsRoot -Label 'Codex common change-impact checker') + } if (Test-Path -LiteralPath $evalDocsSource -PathType Container) { Assert-ManagedTopLevelEntriesSafe -Source $evalDocsSource -Target $evalDocsTarget -ManagedRoot $agentHome -Label 'Eval docs' } @@ -2341,6 +2387,9 @@ function Invoke-AssistantFrameworkInstall { if (Test-Path -LiteralPath $toolsSource -PathType Container) { Sync-ManagedTopLevelEntries -Source $toolsSource -Target $toolsTarget -ManagedRoot $agentHome -ExcludedNames $toolExclusions -ExcludedExactPaths $sourceOnly -Label 'Tools' } + if ($agentName -eq 'codex' -and (Test-Path -LiteralPath $changeImpactToolsSource -PathType Container)) { + Sync-ManagedDirectory -Source $changeImpactToolsSource -Target (Join-Path $codexCommonToolsTarget 'change-impact') -ManagedRoot $agentsRoot -Label 'Codex common change-impact checker' + } if (Test-Path -LiteralPath $evalDocsSource -PathType Container) { Sync-ManagedTopLevelEntries -Source $evalDocsSource -Target $evalDocsTarget -ManagedRoot $agentHome -Label 'Eval docs' } diff --git a/install.sh b/install.sh index 8bacdc22..74adf40f 100755 --- a/install.sh +++ b/install.sh @@ -9,7 +9,7 @@ # ./install.sh --agent codex # → ~/.codex/skills/assistant-*/ # ./install.sh --agent gemini # → ~/.gemini/skills/assistant-*/ # ./install.sh --agent claude --dry-run -# ./install.sh --agent claude --skill assistant-workflow # single skill only +# ./install.sh --agent claude --skill assistant-workflow # root skill plus bundled hard dependencies # ./install.sh --agent codex # native, hookless behavior # ./install.sh --agent claude --no-hooks # deprecated compatibility no-op # @@ -37,7 +37,7 @@ Installs the Assistant Framework skills for an AI agent. Options: --agent NAME Target agent: claude, codex, gemini (required) - --skill NAME Install only one skill (default: all) + --skill NAME Install one root skill plus bundled hard dependencies (default: all) --no-hooks Deprecated compatibility no-op; all installs are hookless --dry-run Show what would be done without doing it -h, --help Show this help @@ -75,6 +75,150 @@ info() { echo " $1"; } ok() { echo " OK: $1"; } dry() { echo " [dry-run] $1"; } +array_contains() { + local needle="$1" + shift + local value + for value in "$@"; do + [[ "$value" == "$needle" ]] && return 0 + done + return 1 +} + +validate_managed_skill_name() { + local skill="$1" + + [[ -n "$skill" && "$skill" != "." && "$skill" != ".." && "$skill" != */* && "$skill" != *\\* ]] \ + || fail "Unsafe skill name: $skill" +} + +assert_existing_directory_or_absent() { + local path="$1" + local label="$2" + + [[ ! -L "$path" ]] || fail "Refusing $label symlink: $path" + [[ ! -e "$path" || -d "$path" ]] || fail "Refusing $label that is not a directory: $path" +} + +assert_directory_tree_safe() { + local directory="$1" + local label="$2" + local unsafe_node="" + + unsafe_node="$(find "$directory" \( -type l -o \( ! -type d -a ! -type f \) \) -print -quit)" \ + || fail "Could not inspect $label: $directory" + [[ -z "$unsafe_node" ]] || fail "Refusing $label containing a symlink or unsupported node: $unsafe_node" +} + +assert_managed_skill_target_root_safe() { + assert_existing_directory_or_absent "$AGENT_HOME" "agent home" + assert_existing_directory_or_absent "$SKILLS_TARGET" "managed skills root" +} + +assert_managed_skill_copy_safe() { + local skill="$1" + local source_dir="" + local source_skill_md="" + local target_dir="" + + validate_managed_skill_name "$skill" + assert_managed_skill_target_root_safe + + source_dir="$SKILLS_SOURCE/$skill" + source_skill_md="$source_dir/SKILL.md" + target_dir="$SKILLS_TARGET/$skill" + + [[ ! -L "$SKILLS_SOURCE" && -d "$SKILLS_SOURCE" ]] \ + || fail "Refusing managed skills source symlink or non-directory: $SKILLS_SOURCE" + [[ ! -L "$source_dir" && -d "$source_dir" ]] \ + || fail "Refusing $skill source symlink or non-directory: $source_dir" + [[ ! -L "$source_skill_md" && -f "$source_skill_md" ]] \ + || fail "Refusing $skill metadata symlink or non-regular file: $source_skill_md" + assert_directory_tree_safe "$source_dir" "$skill source" + + [[ ! -L "$target_dir" ]] || fail "Refusing $skill managed target symlink: $target_dir" + [[ ! -e "$target_dir" || -d "$target_dir" ]] \ + || fail "Refusing $skill managed target that is not a directory: $target_dir" + if [[ -d "$target_dir" ]]; then + assert_directory_tree_safe "$target_dir" "$skill managed target" + fi +} + +preflight_managed_skill_closure() { + local skill="" + + for skill in "$@"; do + assert_managed_skill_copy_safe "$skill" + done +} + +read_canonical_skill_requires() { + local skill_md="$1" + local line="" + local dependency="" + local in_frontmatter=false + local in_requires=false + + [[ -f "$skill_md" ]] || return 0 + while IFS= read -r line; do + if [[ "$line" == "---" ]]; then + if $in_frontmatter; then + break + fi + in_frontmatter=true + continue + fi + $in_frontmatter || continue + if [[ "$line" == "requires:" ]]; then + in_requires=true + continue + fi + if $in_requires && [[ "$line" =~ ^[[:space:]]*-[[:space:]]*(.*)$ ]]; then + dependency="${BASH_REMATCH[1]}" + dependency="${dependency#"${dependency%%[![:space:]]*}"}" + dependency="${dependency%"${dependency##*[![:space:]]}"}" + [[ -n "$dependency" ]] && printf '%s\n' "$dependency" + else + in_requires=false + fi + done < "$skill_md" +} + +resolve_bundled_skill_closure() { + local -a inventory=() + local -a queue=("$@") + local -a resolved=() + local queue_index=0 + local skill="" + local dependency="" + + if [[ ${#SKILLS[@]} -gt 0 ]]; then + inventory=("${SKILLS[@]}") + fi + + while [[ $queue_index -lt ${#queue[@]} ]]; do + skill="${queue[$queue_index]}" + queue_index=$((queue_index + 1)) + validate_managed_skill_name "$skill" + if [[ ${#resolved[@]} -gt 0 ]] && array_contains "$skill" "${resolved[@]}"; then + continue + fi + assert_managed_skill_copy_safe "$skill" + resolved+=("$skill") + while IFS= read -r dependency; do + [[ -n "$dependency" ]] || continue + if [[ ${#inventory[@]} -gt 0 ]] && + array_contains "$dependency" "${inventory[@]}" && + ! { [[ ${#resolved[@]} -gt 0 ]] && array_contains "$dependency" "${resolved[@]}"; } && + ! array_contains "$dependency" "${queue[@]}"; then + queue+=("$dependency") + fi + done < <(read_canonical_skill_requires "$SKILLS_SOURCE/$skill/SKILL.md") + done + + SKILLS=("${resolved[@]}") +} + metadata_preserving_temp() { local source_file="$1" local temp_file @@ -496,9 +640,10 @@ retire_installed_plugin_tool() { [[ -n "$AGENT" ]] || fail "Missing --agent. Supported: claude, codex, gemini" [[ "$AGENT" =~ ^(claude|codex|gemini)$ ]] || fail "Unknown agent: $AGENT. Supported: claude, codex, gemini" -FRAMEWORK_DIR="$(cd "$(dirname "$0")" && pwd)" +FRAMEWORK_DIR="$(cd -P "$(dirname "$0")" && pwd -P)" SKILLS_SOURCE="$FRAMEWORK_DIR/skills" -[[ -d "$SKILLS_SOURCE" ]] || fail "Skills directory not found at $SKILLS_SOURCE" +[[ ! -L "$SKILLS_SOURCE" && -d "$SKILLS_SOURCE" ]] \ + || fail "Skills directory not found or unsafe at $SKILLS_SOURCE" # Auto-discover first-class release skills: assistant-* directories containing SKILL.md. while IFS= read -r skill_md; do @@ -527,12 +672,19 @@ if [[ -d "$TOOLS_SOURCE" ]]; then validate_installed_plugin_tool_cleanup "$TOOLS_TARGET" fi -# Filter to single skill if requested +# Select one root skill and its bundled hard-dependency closure if requested. if [[ -n "$SINGLE_SKILL" ]]; then + validate_managed_skill_name "$SINGLE_SKILL" + assert_managed_skill_copy_safe "$SINGLE_SKILL" [[ -f "$SKILLS_SOURCE/$SINGLE_SKILL/SKILL.md" ]] || fail "Unknown skill: $SINGLE_SKILL. Available: ${SKILLS[*]}" - SKILLS=("$SINGLE_SKILL") + resolve_bundled_skill_closure "$SINGLE_SKILL" fi +# Validate the complete selected set before any installer write. The closure +# resolver preflights each member before it reads that member's metadata; this +# second pass also covers full installs and catches changes before copying. +preflight_managed_skill_closure "${SKILLS[@]}" + SETTINGS_FILE="$AGENT_HOME/settings.json" HOOKS_TARGET="$AGENT_HOME/hooks/assistant" @@ -544,14 +696,11 @@ echo "" # ── Install skills ──────────────────────────────────────────────────────────── for skill in "${SKILLS[@]}"; do + # Recheck immediately before rsync so a changed managed path is never used. + assert_managed_skill_copy_safe "$skill" source_dir="$SKILLS_SOURCE/$skill" target_dir="$SKILLS_TARGET/$skill" - if [[ ! -d "$source_dir" ]]; then - info "SKIP: $skill (source not found)" - continue - fi - if $DRY_RUN; then dry "rsync $source_dir/ -> $target_dir/" dry "Substitute agent state path placeholders in copied $skill instruction/config files" @@ -583,39 +732,11 @@ for skill in "${SKILLS[@]}"; do skill_md="$SKILLS_SOURCE/$skill/SKILL.md" [[ -f "$skill_md" ]] || continue - # Parse requires: from YAML frontmatter (simple grep, no YAML parser needed) - in_frontmatter=false - in_requires=false - while IFS= read -r line; do - # Track frontmatter boundaries (opening and closing ---) - if [[ "$line" == "---" ]]; then - if $in_frontmatter; then - break # closing delimiter — done - else - in_frontmatter=true - continue # opening delimiter — skip - fi - fi - $in_frontmatter || continue - if [[ "$line" == "requires:" ]]; then - in_requires=true - continue + while IFS= read -r dep; do + if ! array_contains "$dep" "${SKILLS[@]}" && [[ ! -d "$SKILLS_TARGET/$dep" ]]; then + info "NOTE: $skill requires '$dep' which is not being installed and not found at $SKILLS_TARGET/$dep" fi - if $in_requires; then - if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*(.+) ]]; then - dep="${BASH_REMATCH[1]}" - dep_installed=false - for s in "${SKILLS[@]}"; do - [[ "$s" == "$dep" ]] && dep_installed=true - done - if ! $dep_installed && [[ ! -d "$SKILLS_TARGET/$dep" ]]; then - info "NOTE: $skill requires '$dep' which is not being installed and not found at $SKILLS_TARGET/$dep" - fi - else - in_requires=false - fi - fi - done < "$skill_md" + done < <(read_canonical_skill_requires "$skill_md") done # ── Create ~/.agents symlink for Codex (agentskills.io standard) ──────────── @@ -889,5 +1010,5 @@ echo "" if [[ -n "$SINGLE_SKILL" ]]; then echo "To install all skills: ./install.sh --agent $AGENT" else - echo "To install a single skill: ./install.sh --agent $AGENT --skill " + echo "To install one root skill and its bundled hard dependencies: ./install.sh --agent $AGENT --skill " fi diff --git a/skills/assistant-debugging/SKILL.md b/skills/assistant-debugging/SKILL.md index 1d4309bf..bbf34c89 100644 --- a/skills/assistant-debugging/SKILL.md +++ b/skills/assistant-debugging/SKILL.md @@ -1,6 +1,8 @@ --- name: assistant-debugging description: "Diagnose an unknown failure by reproducing and isolating before fixing. Use for debugging, root causes, flaky tests, or unexplained breakage." +requires: + - assistant-review --- # Evidence-First Debugging @@ -17,6 +19,10 @@ evidence that explains the symptom, cause, fix, and verification. - Competing hypotheses are tested and disconfirming evidence is retained. - Root cause is supported by code, config, or runtime evidence. - Verification covers the original failure path and a relevant regression check. +- When a repair can affect another consumer, record compact change-impact + applicability evidence. Only shared, materially unresolved, or explicitly + carried expanded repairs use the bounded artifact through verification; + source fixes do not relabel a symptom-only assessment as completion evidence. ## Constraints @@ -35,6 +41,8 @@ load only the boundary currently being enforced: - `entry` for symptom, scope, reproduction target, safety, and edit permission; - `current_phase` for SCOPE, REPRODUCE, HYPOTHESIZE, ISOLATE, FIX, or VERIFY; +- `change_impact` when a diagnosis, repair, cosmetic, or locality claim affects consumer + verification; and - `selected_handoff` only when investigation or fix delegation is selected; and - `completion` only for the artifact being returned. @@ -62,6 +70,10 @@ delegation, and review, but specialist gates are authoritative. 6. **VERIFY** — rerun the reproduction, focused test, relevant regressions, and normal project checks. Skipped checks remain residual risk. +When change impact is triggered, load `references/change-impact.md`. The +common checker is resolved from the active installation, so standalone +debugging does not depend on assistant-workflow being installed. + Use `assistant-debugging` before `assistant-tdd` when a meaningful RED test cannot yet be written. Once the mechanism is understood, hand the regression test and evidence into the TDD cycle. Use `assistant-review` after non-trivial or diff --git a/skills/assistant-debugging/contracts/handoffs.yaml b/skills/assistant-debugging/contracts/handoffs.yaml index 9057afaa..1f0b3a7f 100644 --- a/skills/assistant-debugging/contracts/handoffs.yaml +++ b/skills/assistant-debugging/contracts/handoffs.yaml @@ -1,7 +1,7 @@ # Debugging handoff contracts # Handoffs are optional; direct execution is valid when subagents are unavailable or policy-disallowed. -schema_version: "1.0" +schema_version: "1.1" contract: handoffs skill: assistant-debugging @@ -49,6 +49,25 @@ handoffs: to: Fixer phase: FIX context_fields: + - name: change_impact_applicability + type: object + required: conditional + condition: "the diagnosis has a behavior-bearing or locality/shared/unresolved impact decision" + validation: "impact_scope and applicability_reason are carried unchanged. local has causal evidence without mandatory expanded refs; shared/unresolved or explicit expansion carries the matching artifact." + - name: change_impact_evidence + type: object + required: conditional + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + validation: "The common checker was freshly run, or was rerun with --receipt validator_result_ref against the referenced capture_ref, independent expected_context_ref, and assessment_ref for a current valid pre_build result that resolves artifact_identity before Fixer dispatch; impact_scope and status remain bound unchanged. A status or result ref alone is insufficient, and discovery or completion evidence does not authorize a fix mutation." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: phase, type: enum, required: true, enum_values: [pre_build]} + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: capture_ref, type: string, required: true} + - {name: expected_context_ref, type: string, required: true} + - {name: assessment_ref, type: string, required: true} + - {name: validator_result_ref, type: string, required: true} + - {name: status, type: enum, required: true, enum_values: [valid]} - name: root_cause type: string required: true diff --git a/skills/assistant-debugging/contracts/index.yaml b/skills/assistant-debugging/contracts/index.yaml index 0e6885bd..39f309eb 100644 --- a/skills/assistant-debugging/contracts/index.yaml +++ b/skills/assistant-debugging/contracts/index.yaml @@ -1,4 +1,4 @@ -schema_version: "1.0" +schema_version: "1.1" contract: index skill: assistant-debugging @@ -21,6 +21,20 @@ load_sets: key: name names: [symptom, expected_behavior, scope, reproduction_target, severity, constraints, can_modify_code] budget_words: 1500 + change_impact: + references: [references/change-impact.md] + selectors: + - id: debugging-change-impact-input + path: contracts/input.yaml + section: fields + key: name + names: [change_impact_applicability, change_impact_context] + - id: debugging-change-impact-output + path: contracts/output.yaml + section: artifacts + key: name + names: [change_impact_applicability, change_impact_evidence] + budget_words: 1800 current_phase: selectors: - id: debugging-current-phase @@ -33,7 +47,7 @@ load_sets: path: contracts/phase-gates.yaml section: invariants key: id - names: [INV1, INV2, INV3] + names: [INV1, INV2, INV3, INV_CHANGE_IMPACT] budget_words: 4000 selected_handoff: selectors: @@ -51,7 +65,7 @@ load_sets: section: artifacts key: name name_from: applicable_completion_artifact - allowed_names: [status, symptom_summary, reproduction, hypotheses, fewer_hypotheses_reason, root_cause, confidence, fix_summary, verification, residual_risks] + allowed_names: [status, symptom_summary, reproduction, hypotheses, fewer_hypotheses_reason, root_cause, confidence, fix_summary, verification, residual_risks, change_impact_applicability, change_impact_evidence] budget_words: 5000 fallback: diff --git a/skills/assistant-debugging/contracts/input.yaml b/skills/assistant-debugging/contracts/input.yaml index 5fd1a8da..7d7b8867 100644 --- a/skills/assistant-debugging/contracts/input.yaml +++ b/skills/assistant-debugging/contracts/input.yaml @@ -1,4 +1,4 @@ -schema_version: "1.0" +schema_version: "1.1" contract: input skill: assistant-debugging @@ -61,3 +61,34 @@ fields: default: true on_missing: skip validation: "False when user asked only for investigation, explanation, or audit" + + - name: change_impact_applicability + type: object + required: conditional + condition: "a diagnosis or proposed repair has a behavior-bearing, cosmetic, or locality/shared/unresolved impact decision" + description: "Compact impact applicability evidence before selecting local or expanded repair verification." + validation: "Records impact_scope and applicability_reason. not_applicable is cosmetic; local requires causal_evidence_ref; shared/unresolved or explicit expansion requires expanded_artifact_carried=true." + object_fields: + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: applicability_reason, type: string, required: true} + - {name: causal_evidence_ref, type: string, required: conditional, condition: "impact_scope == local"} + - {name: expanded_artifact_carried, type: boolean, required: true} + on_missing: fail + + - name: change_impact_context + type: object + required: false + description: "Current common change-impact artifact references for a shared, unresolved, or explicitly carried expanded repair." + validation: "Preserves identity/refs; valid carries validator result and valid completion resolves the review projection to the actual review document. Discovery may report gaps; expanded repair requires the common checker." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: status, type: enum, required: true, enum_values: [valid, gaps_reported, blocked, invalid]} + - {name: phase, type: enum, required: conditional, condition: "status == valid", enum_values: [discovery, pre_build, completion]} + - {name: capture_ref, type: string, required: conditional, condition: "status == valid"} + - {name: expected_context_ref, type: string, required: conditional, condition: "status == valid"} + - {name: assessment_ref, type: string, required: conditional, condition: "status == valid and phase in [pre_build, completion]"} + - {name: review_projection_ref, type: string, required: conditional, condition: "status == valid and phase == completion"} + - {name: validator_result_ref, type: string, required: conditional, condition: "status == valid"} + - {name: gap_or_blocker_ref, type: string, required: conditional, condition: "status in [gaps_reported, blocked, invalid]"} + default: null + on_missing: infer diff --git a/skills/assistant-debugging/contracts/output.yaml b/skills/assistant-debugging/contracts/output.yaml index 52e15af4..f03fb6a4 100644 --- a/skills/assistant-debugging/contracts/output.yaml +++ b/skills/assistant-debugging/contracts/output.yaml @@ -1,8 +1,38 @@ -schema_version: "1.0" +schema_version: "1.1" contract: output skill: assistant-debugging artifacts: + - name: change_impact_applicability + type: object + required: conditional + condition: "a diagnosis or proposed repair has a behavior-bearing, cosmetic, or locality/shared/unresolved impact decision" + validation: "Records impact_scope and applicability_reason. not_applicable is cosmetic; local requires causal_evidence_ref; shared/unresolved or explicit expansion requires expanded_artifact_carried=true. Compact local output retains causal evidence without an expanded artifact or checker refs." + on_fail: "Record the applicable impact scope and causal evidence before claiming a local diagnosis or repair result." + object_fields: + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: applicability_reason, type: string, required: true} + - {name: causal_evidence_ref, type: string, required: conditional, condition: "impact_scope == local"} + - {name: expanded_artifact_carried, type: boolean, required: true} + + - name: change_impact_evidence + type: object + required: conditional + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + validation: "Records unchanged artifact_identity, phase, impact_scope, capture/expected/assessment refs, and checker result for expanded impact. Local diagnosis records causal evidence with ordinary regression verification and self-review without checker refs. Before a source fix, shared or unresolved diagnosis has valid pre-build evidence; after a fix, completion requires actual verification and assistant-review's canonical review projection. Audit-only diagnosis may report gaps without claiming a future repair or clean review." + on_fail: "Preserve the incomplete diagnosis and report the blocked/unverified gate; do not narrow consumers to the reported symptom or substitute manual validation." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: phase, type: enum, required: true, enum_values: [discovery, pre_build, completion]} + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: validator_result_ref, type: string, required: conditional, condition: "status == valid"} + - {name: status, type: enum, required: true, enum_values: [valid, gaps_reported, blocked, invalid]} + - {name: capture_ref, type: string, required: conditional, condition: "status == valid"} + - {name: expected_context_ref, type: string, required: conditional, condition: "status == valid"} + - {name: assessment_ref, type: string, required: conditional, condition: "status == valid and phase in [pre_build, completion]"} + - {name: review_projection_ref, type: string, required: conditional, condition: "status == valid and phase == completion"} + - {name: gap_or_blocker_ref, type: string, required: conditional, condition: "status in [gaps_reported, blocked, invalid]"} + - name: status type: enum required: true diff --git a/skills/assistant-debugging/contracts/phase-gates.yaml b/skills/assistant-debugging/contracts/phase-gates.yaml index 4374ee0e..bf1e244d 100644 --- a/skills/assistant-debugging/contracts/phase-gates.yaml +++ b/skills/assistant-debugging/contracts/phase-gates.yaml @@ -1,4 +1,4 @@ -schema_version: "1.0" +schema_version: "1.1" contract: phase-gates skill: assistant-debugging @@ -63,6 +63,14 @@ gates: checkpoint_start: "--- PHASE: FIX ---" checkpoint_end: "--- PHASE: FIX COMPLETE ---" condition: "when can_modify_code is true and a fix or mitigation is attempted" + entry_assertions: + - id: FX_CHANGE_IMPACT_APPLICABILITY + check: "Before any source/test mutation or mutation dispatch, record change_impact_applicability with impact_scope and applicability_reason; local behavior requires causal_evidence_ref. Shared/unresolved or explicitly carried expanded work sets expanded_artifact_carried=true. Cosmetic work records not_applicable." + on_fail: "Stop before edits or mutation dispatch; load references/change-impact.md and record valid applicability before selecting the local or expanded path." + - id: FX_CHANGE_IMPACT_PREBUILD + check: "Before any source/test mutation or mutation dispatch, shared/unresolved/expanded work runs or revalidates the common checker against referenced capture, independent expected context, and assessment for a current valid pre_build result resolving validator refs. Evidenced local repairs keep ordinary regression verification and self-review." + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + on_fail: "Do not patch, write a regression test, or dispatch Fixer; load references/change-impact.md and repair the assessment or report its explicit blocker." exit_assertions: - id: FX1 check: "Each changed file maps to the confirmed cause, mitigation, or regression test" @@ -78,6 +86,10 @@ gates: checkpoint_start: "--- PHASE: VERIFY ---" checkpoint_end: "--- PHASE: VERIFY COMPLETE ---" exit_assertions: + - id: VF_CHANGE_IMPACT_COMPLETION + check: "A triggered completed repair retains the same artifact identity, actual current verification, and assistant-review projection of the canonical review scope/ledger/snapshot bindings; its impact result does not replace assistant-review terminal coverage or result." + condition: "a source fix occurred and (impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried)" + on_fail: "Report verification incomplete or blocked; refresh and rerun the common completion gate and canonical review as applicable." - id: VF1 check: "Original reproduction path passes or status is not_reproduced/blocked/inconclusive with explanation" on_fail: "Run original reproduction path again or downgrade the status" @@ -89,6 +101,11 @@ gates: on_fail: "Move skipped/blocked checks into residual_risks and adjust confidence" invariants: + - id: INV_CHANGE_IMPACT + check: "Every behavior diagnosis retains compact applicability evidence. An expanded diagnosis carries the same common artifact identity across investigation, repair, and verification; absent or stale expanded evidence remains a gap, not a local or clean claim." + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + scope: all_phases + on_fail: "Refresh the bounded evidence or report the residual blocker." - id: INV1 check: "No random patching: every edit traces to a confirmed or strongly supported hypothesis" scope: all_phases diff --git a/skills/assistant-debugging/evals/cases.json b/skills/assistant-debugging/evals/cases.json index b4f6d7cd..5a1f447c 100644 --- a/skills/assistant-debugging/evals/cases.json +++ b/skills/assistant-debugging/evals/cases.json @@ -115,6 +115,60 @@ "destructive diagnostics" ] } + }, + { + "id": "shared-checkout-repair-rejects-symptom-only-locality", + "title": "Debugging carries shared consumer impact before a checkout fix", + "category": "impact_accounting", + "purpose": "Rejects a checkout-only fix claim when invoice, retry, and configuration consumers are supplied.", + "prompt": "Diagnose a checkout discount bug. The helper is also used by invoice export, retry recovery, and configuration-driven validation. A caller proposes calling it local because checkout is the reported symptom. Return the phase-correct debugging result.", + "setup_context": ["The change-impact checker is available but no pre-build assessment has been supplied.", "The reported checkout symptom is only one known consumer."], + "expected_behavior": ["Rejects symptom-only locality.", "Records bounded shared impact or an explicit evidence gap before a repair.", "Does not claim completed verification or clean review from diagnosis."], + "pass_criteria": ["The response names the omitted consumer categories and does not patch first.", "Completion remains unavailable without current verification and review."], + "fail_signals": ["Classifies the helper local solely because checkout reported the bug.", "Claims completion without current verification and review projection."], + "machine_expectations": { + "required_substrings": ["shared", "invoice", "retry", "configuration", "pre-build", "residual"], + "forbidden_substrings": ["local because checkout", "clean review is complete"] + } + }, + { + "id": "local-debugging-retains-compact-causal-impact", + "title": "Local debugging retains compact causal impact", + "category": "impact_accounting", + "purpose": "Requires a locally bounded debugging result to retain causal evidence without carrying an expanded change-impact checker envelope.", + "prompt": "Diagnose a local draft-navigation bug. The trace proves that only this route reaches the guard. Return one valid JSON debugging result.", + "setup_context": ["The causal trace confines the defect to one route.", "No expanded change-impact artifact is carried into debugging."], + "expected_behavior": ["Records local applicability with a causal evidence reference.", "Keeps expanded_artifact_carried false.", "Does not fabricate expanded change-impact checker evidence for the local diagnosis."], + "pass_criteria": ["The compact applicability object has local scope, a reason, and a causal evidence reference.", "No expanded artifact or checker-envelope fields are present."], + "fail_signals": ["Omits local causal evidence.", "Carries an expanded change-impact artifact for the local diagnosis.", "Claims validator, capture, or expected-context checker references."], + "machine_expectations": { + "required_substrings": ["local", "change_impact_applicability", "causal_evidence_ref"], + "forbidden_substrings": ["change_impact_evidence", "validator_result_ref"], + "structured_json_assertions": [ + {"operator":"equals","path":["status"],"expected":"root_cause_found"}, + {"operator":"nonempty_string","path":["symptom_summary"]}, + {"operator":"nonempty_string","path":["reproduction","target"]}, + {"operator":"nonempty_string","path":["reproduction","command_or_steps"]}, + {"operator":"nonempty_string","path":["reproduction","observed_result"]}, + {"operator":"nonempty_string","path":["reproduction","reproduced"]}, + {"operator":"array_items_nonempty_fields","path":["hypotheses"],"fields":["cause","supporting_evidence","disconfirming_evidence","diagnostic_check"]}, + {"operator":"equals","path":["hypotheses",0,"rank"],"expected":1}, + {"operator":"equals","path":["hypotheses",1,"rank"],"expected":2}, + {"operator":"equals","path":["hypotheses",2,"rank"],"expected":3}, + {"operator":"equals","path":["hypotheses",0,"result"],"expected":"confirmed"}, + {"operator":"equals","path":["hypotheses",1,"result"],"expected":"refuted"}, + {"operator":"equals","path":["hypotheses",2,"result"],"expected":"refuted"}, + {"operator":"nonempty_string","path":["root_cause"]}, + {"operator":"equals","path":["confidence"],"expected":"medium"}, + {"operator":"array_items_nonempty_fields","path":["verification"],"fields":["command_or_check","result","evidence"]}, + {"operator":"array_nonblank_strings","path":["residual_risks"],"allow_empty":false}, + {"operator":"equals","path":["change_impact_applicability","impact_scope"],"expected":"local"}, + {"operator":"nonempty_string","path":["change_impact_applicability","applicability_reason"]}, + {"operator":"nonempty_string","path":["change_impact_applicability","causal_evidence_ref"]}, + {"operator":"equals","path":["change_impact_applicability","expanded_artifact_carried"],"expected":false}, + {"operator":"path_absent","path":["change_impact_evidence"]} + ] + } } ] } diff --git a/skills/assistant-debugging/references/change-impact.md b/skills/assistant-debugging/references/change-impact.md new file mode 100644 index 00000000..f7d94942 --- /dev/null +++ b/skills/assistant-debugging/references/change-impact.md @@ -0,0 +1,26 @@ +# Change-impact during debugging + +Load when the reported symptom, diagnosis, proposed fix, or cosmetic decision can affect another +consumer. Keep an audit-only diagnosis honest: discovery can identify missing +or material unknown boundaries, but it cannot claim future execution, review, +or a clean result. + +From the directory containing the actively loaded `SKILL.md`, resolve the common +checker as `../../tools/change-impact/validate-change-impact.cjs` and invoke it +with quoted explicit paths under Node 22+ only for shared, materially +unresolved, or explicitly carried expanded impact. Do not use the process cwd +or assume assistant-workflow is installed. A missing checker/runtime blocks that +expanded deterministic gate; an evidenced local repair remains on its ordinary +regression-verification and self-review path. + +Every proposed behavior repair records impact scope, an applicability reason, +and causal evidence for a local decision. Before any source/test mutation, +including a regression-test write, or Fixer dispatch, shared, unresolved, or +explicitly expanded impact must run or revalidate the common checker against the referenced capture, +independent expected context, and assessment. The current valid `pre_build` +result must resolve against the fix packet; a status or result ref alone is +insufficient. Fresh runs omit `--receipt`; reuse reruns the checker with +`--receipt` pointing to `validator_result_ref`, which must match the current parsed inputs. Preserve the same artifact identity through expanded verification. An expanded fix completion +needs actual current verification and assistant-review's projection of its +existing canonical manifest, ledger, snapshot, and concern bindings; that +projection never replaces assistant-review's own terminal coverage/result. diff --git a/skills/assistant-review/SKILL.md b/skills/assistant-review/SKILL.md index acc64f43..08af2d88 100644 --- a/skills/assistant-review/SKILL.md +++ b/skills/assistant-review/SKILL.md @@ -7,75 +7,103 @@ description: "Review code, fix actionable findings, and run one fresh re-review. ## Contracts -Canonical input, output, phase-gate, and handoff schemas remain authoritative at their enforcement points. Read `contracts/index.yaml` first; do not load every contract at entry. - +Canonical schemas are authoritative at enforcement points. Read +`contracts/index.yaml` first; do not load every contract at entry. - `entry`: load `contracts/input.yaml` review-entry fields selected by `review-entry-fields` in `contracts/index.yaml`. -- `architecture_pack_input`: when Pack review triggers, load `architecture_decision_pack` through `review-architecture-pack-input` before planning or dispatch. -- `current_round`: load the active round step from `contracts/phase-gates.yaml` at each transition. -- `selected_handoff`: select the compact dispatch pointer from `contracts/handoffs.yaml` before Reviewer or QAEvaluator dispatch. -- `reviewer_context`: resolve the fresh bounded Reviewer bundle only when a review pass begins. -- `return_validation`: select the canonical return pointer only after a worker/direct-fallback result exists. -- `completion`: load the applicable `contracts/output.yaml` artifacts at completion, before the final review exit. - -Migration note: assistant-review contracts are v7. Persisted v6 batch packets are invalidated and rebuilt; they never reach CLEAN. Triggered Pack review loads `architecture_pack_input` and validates recoverable selected design, rationale, and viable alternatives/dispositions plus `architecture_decision_pack_checks`. Applicable instructions trigger Reviewer/QA roles with `subagent_trigger_scope`; opt-out, unavailability, or policy blocks use direct fallback. Reviewer returns and final summaries use non-empty `reviewed_scope` so workflow consumers can use the producer packet. - -Selectors use unique id, canonical path, exact section/key, and explicit or allowed runtime names. Entry declares no immediate principles, checklist, or rubric references. +- Triggered `change_impact` and `architecture_pack_input`: resolve before planning or dispatch; keep change-impact guidance out of Reviewer bundles. +- `current_round`: load the active `contracts/phase-gates.yaml` round at each transition; `selected_handoff`: resolve the compact `contracts/handoffs.yaml` dispatch pointer before Reviewer or QAEvaluator dispatch. +- `reviewer_context`: resolve at pass start; `return_validation`: resolve after its result; `completion`: load `contracts/output.yaml` before final exit. -If a selector is missing or invalid, apply `load_full_authoritative_file`: load the full named canonical file, validate the applicable rules, and record any recovery before proceeding. +Migration note: assistant-review contracts are v7.2. Persisted 6.0/7.0/7.1 batch packets are invalidated and rebuilt; they never reach CLEAN. A triggered Pack validates recoverable selected design, rationale, and viable alternatives/dispositions plus `architecture_decision_pack_checks`. +Applicable instructions set `subagent_trigger_scope`; opt-out, unavailability, +or policy blocks use direct fallback. Reviewer returns and final summaries have +non-empty `reviewed_scope` so workflow consumers can use the producer packet. -Run the bounded review policy, keep intermediate results inside the loop, and present one final evidence-calibrated result. Required QA follows build/test and code-review evidence. +Selectors resolve canonical fields. Entry loads no review guidance. A missing or +invalid selector uses `load_full_authoritative_file`: validate that full +canonical file and record recovery. ## Goal -Find evidence-backed defects, regressions, and test gaps; fix them in review-fix mode; and return one policy-safe result without implying proof of correctness. Required QA independently evaluates acceptance, evidence, scoped quality, progression, and readiness. +Find evidence-backed defects, regressions, and test gaps; fix authorized +findings; and return one policy-safe result without claiming proof. Required QA +independently evaluates acceptance, evidence, quality, progression, and readiness. ## Success Criteria -- Scope, mode, and review material are resolved before the loop. -- Findings are severity-ranked with evidence and confidence. -- Every frozen-snapshot batch completes at least two independent passes before aggregation, fix, or exit; audit exits after one started batch reaches terminal accounting, complete or incomplete (`HAS_REMAINING_ITEMS` when incomplete), and returns name non-empty `reviewed_scope` and coverage. -- Every review applies the SOLID, KISS, DRY, YAGNI, and readability lens from `references/review-principles.md`. -- When a carried Architecture Decision Pack applies, the review checks its freshness, ownership/dependency boundary, semantic type ledger, falsifiable quality scenarios, compatibility/extension seam, and verification handoff. -- In review-fix mode, must-fix and should-fix findings are addressed or explicitly deferred. -- Validation and a fresh review follow fixes. +- Resolve scope, mode, and material before the loop; rank findings by severity, + evidence, and confidence. +- Each frozen-snapshot batch plans and attempts at least two independent passes. + All expected passes reach terminal accounting before aggregation, fix, or exit. + `CLEAN`/`ISSUES_FIXED` require complete coverage. Audit exits after one started + batch with non-empty `reviewed_scope`; incomplete coverage, including failed, + timed-out, or blocked passes, returns `HAS_REMAINING_ITEMS`. +- Every review applies SOLID, KISS, DRY, YAGNI, and readability from + `references/review-principles.md`. An applicable Pack checks freshness, + ownership/dependency, semantic types, falsifiable quality, compatibility or + extension, and verification handoff. +- Review-fix addresses or explicitly defers must-fix and should-fix findings, + then validates and performs a fresh review. - QA evaluation runs after code-review/build evidence when `qa_evaluation_mode=required`, returns score progression and a final acceptance verdict, and does not replace code-reviewer. - QA required positive triggers: explicit QA/acceptance evaluation request, accepted Done Contract, harness-capable acceptance scope, domain-scored scope, or scoped UI/visual/product/UX/docs/DX acceptance. - QA non-triggers: template labels/placeholders, generic acceptance criteria labels, optional/not_required reasons, delegation/source-changing work alone, and ordinary medium+ code-review-only/source-changing work. -- QA evaluation loads `references/domain-rubrics.md` only when `domain_context`, explicit `rubric_refs`, or subjective/UI/visual/product/UX/docs/DX/domain acceptance criteria require scoped domain-quality scoring. +- QA evaluation loads `references/domain-rubrics.md` only when acceptance criteria, Done Contract, `domain_context`, or explicit `rubric_refs` scope domain quality; selected_domain_rubrics/domain_quality_scores when scoped. ## Constraints -- Infer review mode from semantic modification authority, not fixed wording, authorship, platform, or connector metadata. Clear report-only intent selects audit; clear source-modification authorization selects review-fix. A carried active approved implementation workflow authorizes only bounded, in-scope review fixes. Only when audit versus editing remains materially unresolved, ask: "Should I only report findings, or also implement and verify fixes?" Do not review or mutate until answered. -- Do not emit intermediate review summaries; present one final summary after loop exit. -- Use concrete risk categories for refactor-related findings. -- Treat clean-code principles as evidence lenses, not acronym-driven style rules. -- Keep QA evaluation separate from code review: QA focuses on acceptance criteria, Done Contract, verification evidence, UI/visual/product/UX/docs/DX/domain quality, score progression, and final result. Code Reviewer continues to own code defects, security, architecture, and test-coverage review. +- Infer authority from semantic modification authority: report-only is audit; + clear source authorization is review-fix; a carried approved workflow allows + only bounded in-scope fixes. If editing versus audit is materially unresolved, + ask: "Should I only report findings, or also implement and verify fixes?" Do + not review or mutate until answered. +- Present no intermediate review summaries. Frame refactor findings with a + concrete risk, and use clean-code principles as evidence lenses. +- Keep QA evaluation separate from code review: QA owns acceptance, Done + Contract, verification, scoped quality, progression, and final result; Code Reviewer continues to own code defects, security, architecture, and test-coverage review. ## Entry -Prefer explicit files/content/diff, then uncommitted changes, then the active task journal or packet, then requested current-file audit. Ask only when no review material can be determined. +Prefer explicit material, then uncommitted changes, task journal/packet, then +current-file audit. Ask only when no review material is available. -A standalone `review this` without carried workflow evidence runs Spec Review against user scope. In review-fix mode, repair an authorized mismatch and repeat to a PASS pointer before Reviewer dispatch. In audit mode, audit retains the Spec Review mismatch as an aggregate finding and continues a frozen read-only multi-pass batch without source mutation; the final report includes the mismatch and every later quality finding. Standalone does not require a task journal; `task_journal_path` is optional. - -A workflow-composed review consumes the carried Spec Review PASS pointer and carried current build/test evidence. After any source fix, every subsequent Reviewer dispatch requires real current passed build/test evidence; a not-applicable marker is invalid. +A standalone `review this` without carried workflow evidence runs Spec Review against user scope. Review-fix +repairs an authorized mismatch and repeats to a PASS pointer before Reviewer +dispatch; audit retains the Spec Review mismatch as an aggregate finding and continues a frozen read-only multi-pass batch. Standalone does not require a task journal; +`task_journal_path` is optional. A workflow-composed review consumes carried +Spec Review PASS and current build/test evidence. After any source fix, every +subsequent Reviewer dispatch requires real current passed build/test evidence; +not-applicable is invalid. ## Review Modes -Use the smallest applicable combination: spec, regression, test, maintainability, bugfix evidence, semantic contract, behavioral contract, agentic loop safety, and security. Contract and loop modes are enabled by the three entry flags; security-sensitive surfaces route to `assistant-security`. A `risk_selected_specialist` security pass uses Code Reviewer with the assistant-security checklist/perspective while retaining the canonical Reviewer schema. - -Authorship, branch/PR ownership, platform, and connector availability may identify material but never authorize source changes. Carried workflow authorization permits only evidence-backed in-scope fixes. Findings expanding scope, requirements, or architecture return to the composing workflow for planning or approval. +Select the applicable spec, regression, test, maintainability, bugfix evidence, +semantic contract, behavioral contract, agentic-loop safety, and security modes. +The three entry flags enable contract/loop modes; security-sensitive surfaces +route to `assistant-security`. `risk_selected_specialist` uses Code Reviewer +with the assistant-security checklist/perspective while keeping the canonical Reviewer +schema. -Findings include severity (`must-fix`, `should-fix`, or `nit`), file/line evidence, concrete impact, smallest useful fix, and evidence-calibrated confidence. Speculative concerns remain non-blocking Observations. +Authorship, branch/PR ownership, platform, and connector availability identify +material but never authorize edits. Out-of-scope findings return to the +composing workflow. Findings state severity (`must-fix`, `should-fix`, or +`nit`), file/line evidence, impact, smallest useful fix, and calibrated +confidence; speculative concerns are non-blocking Observations. -QA evaluation runs after code-review/build evidence and only when `qa_evaluation_mode=required`. Load `references/qa-evaluation-loop.md` at that later boundary. Load `references/domain-rubrics.md` only when acceptance criteria, Done Contract, `domain_context`, or explicit `rubric_refs` scope domain quality; selected_domain_rubrics/domain_quality_scores when scoped. Code Reviewer still owns code defects, security, architecture, and test coverage. +Load `references/qa-evaluation-loop.md` after build/test and code-review +evidence only when QA is required. Code Reviewer still owns code defects, +security, architecture, and test coverage. ## Company-Safe Review Rules -Prefer local diffs and repo-native checks. Do not require external scanners, remote review, or unapproved installs; redact secrets and proprietary data. Offer local/manual equivalents when policy blocks an external scan. +Prefer local diffs and repo-native checks; require no external scanner, remote +review, or unapproved install. Redact secrets and proprietary data, and offer +local/manual alternatives when policy blocks a scan. ## Mandatory Review Checklists -The fresh Reviewer context bundle points to `references/review-checklists.md` and supplies only the applicable checklist sections. Checklist headings alone are not evidence; each selected area produces findings or an explicit "no concrete risk found" check. +The fresh Reviewer context bundle points to `references/review-checklists.md` +and supplies only applicable sections; each yields findings or an explicit "no +concrete risk found" check. - Agentic loop flag -> Agentic Loop Safety Checklist -> `agentic_loop_safety_checks`. - Behavioral flag -> Behavioral Contract Review Checklist -> `behavioral_contract_checks`. @@ -84,47 +112,54 @@ The fresh Reviewer context bundle points to `references/review-checklists.md` an ## Refactor-Related Findings -Use refactor-related findings only for concrete actionable risk. Allowed risk categories: -- correctness -- security -- unsafe change surface -- branching/responsibility growth -- hidden dependency/ownership -- brittle testing -- poor extension seam -- readability/maintainability drag - -Every refactor-related finding MUST state the risk category, affected surface, evidence from the review material, and the smallest durable fix that addresses the risk within the normal finding text. - -Use concrete risk framing instead of generic convention, style, cleanliness, or improvement language. Request broad cleanup only when a smaller durable fix cannot remove the risk. +Allowed risk categories: correctness, security, unsafe change surface, +branching/responsibility growth, hidden dependency/ownership, brittle testing, +poor extension seam, and readability/maintainability drag. Every refactor-related finding MUST state its risk category, affected surface, review evidence, and the smallest durable fix. Use concrete risk framing instead of generic convention, style, cleanliness, or improvement language. Request broad cleanup only when a smaller durable fix cannot remove the risk. ## Architecture Decision Pack Review -Review a Pack only when workflow metadata or review material says one applies. Its nested mode must equal canonical `architecture_design_mode`; review-intensive review requires independent challenge evidence. It is a compact, durable decision record, not a permanent architect role or global memory. Require source/revision freshness and every material question that could invalidate the decision; group questions with why, risk if guessed, and a safe default where available. Check control/early-exit behavior, ownership/disposal, bounded resource envelope, extension registration, and the representative producer-consumer path before accepting an interface or engine. Treat generic `string`, numeric, collection, or callback interfaces as a finding when they erase domain/public/lifecycle semantics without an explicit local-boundary primitive exception and conversion/validation. Do not approve memory, performance, or extensibility claims without workload, budget, measurement method, and failure condition; mark absent evidence as unknown or a verification gap. +Review a Pack only when metadata or material says one applies; its mode equals +canonical `architecture_design_mode`, and `review_intensive` includes +independent challenge evidence. Verify source/revision freshness, invalidating +questions, boundaries, control/early exit, ownership/disposal, resource +envelope, extension registration, representative path, semantic type and +primitive-exception boundaries, falsifiable quality scenarios, compatibility, +extension seam, verification, and rollback. Generic `string`, numeric, +collection, or callback interfaces that erase domain/public/lifecycle semantics +without a local primitive exception and conversion/validation are findings. +Treat unsupported memory, performance, or extensibility claims as unknown or a +verification gap. ## Principle and Readability Lens -The fresh Reviewer bundle includes `references/review-principles.md` for clean-code checks. For medium+ scope, run its Design Coherence Pass and return `principle_checks.design_coherence`: evidence-backed risk or `no concrete risk found`, never a structural heuristic. Report findings only with concrete risk. - -For each principle/readability finding, include the violated lens, affected surface, concrete evidence, risk category, and smallest durable fix. Do not report acronym-only findings such as "violates SOLID" without naming the observed behavior and the user-facing or maintainer risk. +For medium+ reviews, run the **Design Coherence Pass** and return +evidence-bound `principle_checks.design_coherence`; findings name surface, risk, +and smallest fix. ## Review Loop Routing -After entry, load `references/review-loop.md` before the first REVIEW step. It owns the batch protocol, loop, barriers, pivots, and max 10. Principles, triggered checklists, and rubric guidance belong to fresh pass bundles. Before dispatch, include `worker_return_schema_selector`: recursively required/triggered shapes, types, enums, and cardinality only; never sibling/batch state. - -Run QA only when `qa_evaluation_mode=required`. The loop routes to `references/qa-evaluation-loop.md` after build/test and code-review evidence exist; QA evaluates acceptance and scoped quality, and does not replace code review. +After entry, load `references/review-loop.md` before the first REVIEW step. It +owns batch protocol, loop, barriers, pivots, and the max 10 rounds; principles, +checklists, and rubric guidance belong to fresh pass bundles. Before dispatch, +include `worker_return_schema_selector`: recursive required/triggered shapes, +types, enums, and cardinality only; never sibling/batch state. Impact projection +preserves canonical closure; it cannot make audit or review-fix clean. -When an `approved_feature_preparation_qa_acceptance_obligation` is carried, QAEvaluator returns `approved_feature_preparation_qa_acceptance_obligation_result`: the exact requested scope, execution prerequisite, feature-preparation scope, and one carried source binding, together with `requested_scope_status`, `execution_prerequisite_status`, and evidence. `accepted` or `accepted_with_concerns` paired with `CLEAN` or `ISSUES_FIXED` requires `fulfilled` and `met` statuses with the exact binding and evidence; otherwise return only `rejected`/`HAS_REMAINING_ITEMS` or `blocked`/`BLOCKED`. +Required QA follows build/test and code-review evidence. A carried +`approved_feature_preparation_qa_acceptance_obligation` uses the exact result +and successful-pair conditions in `contracts/handoffs.yaml` and +`contracts/phase-gates.yaml`: echo its scope/prerequisite/source binding with +evidence; otherwise return rejected/`HAS_REMAINING_ITEMS` or blocked/`BLOCKED`. ## Exit: Present Final Result -Present one summary using `contracts/output.yaml`. For no findings, say: "No material findings within the reviewed scope and available evidence". `CLEAN` remains a machine enum, not proof of correctness. +Use `contracts/output.yaml`. For no findings, say: "No material findings within the reviewed scope and available evidence"; `CLEAN` is an enum, not proof. ## Rules -- Keep round results internal and present one final summary. -- Use fresh sibling-blind bundles; a single broad pass is never sufficient coverage. -- Wait for every response before aggregation. Incomplete coverage returns `HAS_REMAINING_ITEMS`; prior fixes need closure verification. +- Keep round results internal; use fresh sibling-blind bundles, wait for every + response, and return `HAS_REMAINING_ITEMS` for incomplete coverage. +- Prior fixes require closure verification. ## Output @@ -132,18 +167,22 @@ Return reviewed scope, rounds/result, evidence-backed findings and fixes, verifi ## Stop Rules -- Audit mode stops after one review batch. Report findings without edits after all planned passes are terminal. +- Audit mode stops after one review batch. Report terminal findings without edits. - The normal review-fix path is an initial review batch, fixes and validation, then one fresh re-review batch; mutation requires a new snapshot and complete coverage. -- Before round 3+, require `additional_round_reason` backed by changed files, an unresolved finding, validation failure, regression/drift, or a changed hypothesis. Score below threshold alone is insufficient. -- The hard max 10 rounds remains: round 10 is terminal and round 11 never starts. +- Before round 3+, require `additional_round_reason` backed by changed files, + unresolved finding, validation failure, regression/drift, or changed + hypothesis; score alone is insufficient. round 10 is terminal; never start 11. - Stop and report a blocker if required review material is unavailable or empty. ### Drift detection (medium+ scope) -Compare medium+ rounds using `references/score-tracking.md`. Drift, regression, -stagnation, or pivot evidence returns `pivot_restart_signal`; the orchestrator records `pivot_restart_decision` and applies the round 3+ evidence gate before another pass. +Compare rounds with `references/score-tracking.md`. Drift, regression, +stagnation, or pivot evidence returns `pivot_restart_signal`; the orchestrator records `pivot_restart_decision` before another pass. ## Review Finding Rule Distillation -At the end of review, load `references/review-finding-permanent-rule.md` for every blocker or must-fix finding. Classify each as `one_off_fix`, `permanent_rule_candidate`, or `no_action`. Promote only recurring process gaps, fake-pass eval gaps, missing contracts, missing checklists, or high-impact repeatable failure modes; do not promote style nits or one-off file-specific issues into broad rules. +For each blocker or must-fix finding, load +`references/review-finding-permanent-rule.md` and classify `one_off_fix`, +`permanent_rule_candidate`, or `no_action`. Promote only recurring process/eval +gaps, missing contracts/checklists, or high-impact repeatable failures. diff --git a/skills/assistant-review/contracts/handoffs.yaml b/skills/assistant-review/contracts/handoffs.yaml index 83513dfe..b53cd19c 100644 --- a/skills/assistant-review/contracts/handoffs.yaml +++ b/skills/assistant-review/contracts/handoffs.yaml @@ -4,13 +4,16 @@ # including triggered trivial/small scope. Direct fallback uses the same review # fields as local evidence without spawning a subagent. -schema_version: "7.1" +schema_version: "7.2" contract: handoffs skill: assistant-review -# v7.1 migration: persisted 6.0 and incompatible 7.0 packets are invalidated and -# rebuilt from a freshly rehashed 7.1 snapshot before CLEAN or ISSUES_FIXED. Preserve a -# legacy closure ledger by normalizing UTF-8 NFC(description) as UTF-8 bytes, -# SHA-256 hashing it, and using legacy-v6::; +# v7.2 migration: persisted 6.0 and incompatible 7.0/7.1 packets are invalidated and +# rebuilt from a freshly rehashed 7.2 snapshot before CLEAN or ISSUES_FIXED. Preserve a +# legacy closure ledger by preserving complete identity tuples. For a partial +# v6/v7.0 tuple, preserve a supplied aggregate_finding_id and fill only missing +# source IDs/provenance. When that ID is absent, normalize UTF-8 +# NFC(description) as UTF-8 bytes, SHA-256 hash it, and use +# legacy-v6:: or legacy-v7-0::; # non-colliding records use that base ID; every colliding record appends # :, starting at 1 in original packet position within its group. @@ -32,7 +35,7 @@ worker_status_protocol: - "Every expected review pass reaches a terminal state before aggregation; the first response never establishes batch coverage." - "evidence is required to support the verdict and any findings." - "open_questions is required for NEEDS_CONTEXT and BLOCKED." - - "Persisted 6.0 and incompatible 7.0 packets are invalidated and rebuilt under 7.1; 6.0 UTF-8 NFC legacy IDs use SHA-256 base IDs, un-suffixed when non-colliding and :-suffixed for every colliding record in original packet order." + - "Persisted 6.0 and incompatible 7.0/7.1 packets are invalidated and rebuilt under 7.2. Preserve complete closure identity tuples and raw history; for partial v6/v7.0 tuples preserve a supplied aggregate_finding_id, fill only missing source IDs/provenance, and use the versioned UTF-8 NFC/SHA-256 legacy ID only when that ID is absent. Non-colliding IDs use the base; every colliding record appends : in original packet order." - "status is required on every QAEvaluator return." - "QAEvaluator returns acceptance_findings, qa_scorecard, final_verdict, result, evidence, and every-round score_entry; score_progression is optional history. Selected_domain_rubrics and domain_quality_scores are required only when scoped. QA does not replace Reviewer/code-reviewer findings." - "Reviewer and QAEvaluator may return pivot_restart_signal for PIVOT, STAGNATION, repeated DRIFT, or repeated REGRESSION; the orchestrator owns the final pivot_restart_decision." @@ -44,7 +47,7 @@ dispatch_context_bundles: selected_dispatch_context_ref: "handoffs.orchestrator_to_reviewer.reviewer_pass_context_fields" review_evidence_pointer: required_refs: [pre_batch_criteria_ref, review_material_snapshot, build_test_verification_ref] - context_fields_from_dispatch: [review_preparation_mode, pre_batch_criteria_ref, build_test_verification_ref, review_material_snapshot, batch_id, review_pass_id, review_attempt_id, review_perspective, assigned_scope, coverage_obligations, review_focus, scope_size, round, additional_round_reason, previously_fixed, finding_filter_policy, project_type, quality_principles_required, reuse_search_instruction, semantic_contract_review_required, behavioral_contract_review_required, agentic_loop_safety_review_required, architecture_decision_pack_review_required, architecture_design_mode, architecture_decision_pack, rubric_required] + context_fields_from_dispatch: [review_preparation_mode, pre_batch_criteria_ref, build_test_verification_ref, review_material_snapshot, change_impact_applicability, change_impact_evidence, batch_id, review_pass_id, review_attempt_id, review_perspective, assigned_scope, coverage_obligations, review_focus, scope_size, round, additional_round_reason, previously_fixed, finding_filter_policy, project_type, quality_principles_required, reuse_search_instruction, semantic_contract_review_required, behavioral_contract_review_required, agentic_loop_safety_review_required, architecture_decision_pack_review_required, architecture_design_mode, architecture_decision_pack, rubric_required] worker_references: - path: references/review-principles.md sections: [all] @@ -509,6 +512,34 @@ handoffs: description: "Only this pass's scope_manifest ids; never sibling assignments or outputs." validation: "Exactly matches this review_pass_id assignment in review_batch.expected_passes and every id belongs to review_material_snapshot.scope_manifest." + - name: change_impact_applicability + type: object + required: conditional + condition: "this pass covers a behavior-bearing change or locality/shared/unresolved impact decision" + validation: "impact_scope and applicability_reason are preserved. local carries causal evidence and ordinary review without mandatory expanded refs; shared/unresolved or explicit expansion carries the matching artifact." + + - name: change_impact_evidence + type: object + required: conditional + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + description: "Pass-local pointer for independently challenging locality, captured consumers, transitions, and mapping gaps." + validation: "Preserves artifact_identity, impact_scope, status, and phase. valid carries current capture/expected/checker refs and assessment only for pre-build or completion. Completion carries the actual review document pointer for rerun or receipt reuse; gaps_reported, blocked, or invalid carries a gap_or_blocker_ref and remains independently challengeable. It provides no sibling output or alternate ledger." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: status, type: enum, required: true, enum_values: [valid, gaps_reported, blocked, invalid]} + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: phase, type: enum, required: conditional, condition: "status == valid", enum_values: [discovery, pre_build, completion]} + - {name: capture_ref, type: string, required: conditional, condition: "status == valid"} + - {name: expected_context_ref, type: string, required: conditional, condition: "status == valid"} + - {name: assessment_ref, type: string, required: conditional, condition: "status == valid and phase in [pre_build, completion]"} + - {name: validator_result_ref, type: string, required: conditional, condition: "status == valid"} + - name: review_projection_ref + type: string + required: conditional + condition: "status == valid and phase == completion" + validation: "Completion carries the actual review document pointer for rerun or receipt reuse." + - {name: gap_or_blocker_ref, type: string, required: conditional, condition: "status in [gaps_reported, blocked, invalid]"} + - name: coverage_obligations type: string[] required: true @@ -566,17 +597,17 @@ handoffs: - name: aggregate_finding_id type: string required: true - validation: "New entries require aggregate_finding_id; legacy missing IDs are normalized deterministically before closure dispatch." + validation: "New entries require aggregate_finding_id. Complete historic tuples are unchanged; partial v6/v7.0 tuples preserve a supplied ID, while missing IDs are normalized deterministically before closure dispatch." - name: source_finding_ids type: string[] required: true min_items: 1 - validation: "Exact pre-fix source IDs; v6 uses its migrated ID." + validation: "Exact pre-fix source IDs. Partial v6/v7.0 tuples fill only missing source IDs with their preserved or migrated aggregate ID; an id-less v7.0 record uses legacy-v7-0 with persisted-v7-0-migration provenance." - name: source_provenance type: object[] required: true min_items: 1 - validation: "Exact pre-fix provenance; v6 records persisted-v6-migration." + validation: "Exact pre-fix provenance. Partial v6/v7.0 tuples fill only missing provenance with review_pass:persisted-v6-migration or review_pass:persisted-v7-0-migration." object_fields: - name: source_kind type: enum diff --git a/skills/assistant-review/contracts/index.yaml b/skills/assistant-review/contracts/index.yaml index 78d0bcda..5fd4c281 100644 --- a/skills/assistant-review/contracts/index.yaml +++ b/skills/assistant-review/contracts/index.yaml @@ -1,4 +1,4 @@ -schema_version: "7.1" +schema_version: "7.2" contract: index skill: assistant-review @@ -19,7 +19,7 @@ load_sets: path: contracts/input.yaml section: fields key: name - names: [review_scope, review_material_snapshot, scope_size, mode, change_kind, semantic_contract_review_required, behavioral_contract_review_required, agentic_loop_safety_review_required, architecture_decision_pack_review_required, architecture_design_mode, subagent_policy_state, subagent_execution_mode, subagent_trigger_scope, policy_blocking_source, harness_capable, qa_evaluation_mode, feature_preparation_scope, approved_feature_preparation_qa_acceptance_obligation, previously_fixed, persisted_v6_packet_migration, persisted_v7_0_packet_invalidation] + names: [review_scope, review_material_snapshot, scope_size, mode, change_kind, semantic_contract_review_required, behavioral_contract_review_required, agentic_loop_safety_review_required, architecture_decision_pack_review_required, architecture_design_mode, subagent_policy_state, subagent_execution_mode, subagent_trigger_scope, policy_blocking_source, harness_capable, qa_evaluation_mode, feature_preparation_scope, approved_feature_preparation_qa_acceptance_obligation, previously_fixed, persisted_v6_packet_migration, persisted_v7_0_packet_invalidation, persisted_v7_1_packet_invalidation] budget_words: 5000 architecture_pack_input: selectors: @@ -29,6 +29,20 @@ load_sets: key: name names: [architecture_decision_pack] budget_words: 3000 + change_impact: + references: [references/change-impact.md] + selectors: + - id: review-change-impact-input + path: contracts/input.yaml + section: fields + key: name + names: [change_impact_applicability, change_impact_context] + - id: review-change-impact-output + path: contracts/output.yaml + section: artifacts + key: name + names: [change_impact_applicability, change_impact_review_projection] + budget_words: 2550 current_round: selectors: - id: review-current-round-gate @@ -41,7 +55,7 @@ load_sets: path: contracts/phase-gates.yaml section: invariants key: id - names: [INV0, INV1, INV2, INV3, INV4, INV_BATCH, INV5, INV6, INV7, INV8, INV9, INV10, INV11, INV_QA1, INV_QA2, INV_QA3, INV_QA4] + names: [INV0, INV1, INV2, INV3, INV4, INV_BATCH, INV5, INV6, INV7, INV8, INV9, INV10, INV11, INV_CHANGE_IMPACT, INV_QA1, INV_QA2, INV_QA3, INV_QA4] budget_words: 12000 selected_handoff: selectors: @@ -80,7 +94,7 @@ load_sets: section: artifacts key: name name_from: applicable_completion_artifact - allowed_names: [review_delegation_path, qa_evaluation_delegation_path, pivot_restart_decision, review_finding_rule_distillation, final_summary, qa_evaluation_result, rubric_summary, agentic_loop_safety_review, behavioral_contract_review, semantic_contract_review, architecture_decision_pack_review, bugfix_evidence_review, build_verification, task_journal_entry, audit_report] + allowed_names: [review_delegation_path, qa_evaluation_delegation_path, pivot_restart_decision, review_finding_rule_distillation, final_summary, qa_evaluation_result, rubric_summary, agentic_loop_safety_review, behavioral_contract_review, semantic_contract_review, architecture_decision_pack_review, bugfix_evidence_review, build_verification, task_journal_entry, audit_report, change_impact_applicability, change_impact_review_projection] budget_words: 10000 fallback: diff --git a/skills/assistant-review/contracts/input.yaml b/skills/assistant-review/contracts/input.yaml index e748b70f..5441e39f 100644 --- a/skills/assistant-review/contracts/input.yaml +++ b/skills/assistant-review/contracts/input.yaml @@ -1,4 +1,4 @@ -schema_version: "7.1" +schema_version: "7.2" contract: input skill: assistant-review @@ -17,6 +17,37 @@ fields: User provided PR number/URL → pr. None of the above → ask. + - name: change_impact_applicability + type: object + required: conditional + condition: "the review covers a behavior-bearing, cosmetic, or locality/shared/unresolved impact decision" + description: "Compact impact applicability evidence before review selects ordinary local or expanded impact obligations." + validation: "Records impact_scope and applicability_reason. not_applicable is cosmetic; local requires causal_evidence_ref; shared/unresolved or explicit expansion requires expanded_artifact_carried=true." + object_fields: + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: applicability_reason, type: string, required: true} + - {name: causal_evidence_ref, type: string, required: conditional, condition: "impact_scope == local"} + - {name: expanded_artifact_carried, type: boolean, required: true} + on_missing: fail + + - name: change_impact_context + type: object + required: false + description: "Common change-impact artifact and independent expected authority for a review with shared, unresolved, or explicitly carried expanded impact." + validation: "Uses artifact identity, refs, validator result when valid, and review scope; valid completion resolves the review projection to the actual review document. Read-only audit may report gaps but cannot claim CLEAN from expanded impact evidence." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: status, type: enum, required: true, enum_values: [valid, gaps_reported, blocked, invalid]} + - {name: phase, type: enum, required: conditional, condition: "status == valid", enum_values: [discovery, pre_build, completion]} + - {name: capture_ref, type: string, required: conditional, condition: "status == valid"} + - {name: expected_context_ref, type: string, required: conditional, condition: "status == valid"} + - {name: assessment_ref, type: string, required: conditional, condition: "status == valid and phase in [pre_build, completion]"} + - {name: review_projection_ref, type: string, required: conditional, condition: "status == valid and phase == completion"} + - {name: validator_result_ref, type: string, required: conditional, condition: "status == valid"} + - {name: gap_or_blocker_ref, type: string, required: conditional, condition: "status in [gaps_reported, blocked, invalid]"} + default: null + on_missing: infer + - name: review_material_snapshot type: object required: true @@ -561,8 +592,8 @@ fields: type: object required: conditional condition: "persisted review packet was created under v6" - description: "v6 to producer 7.1 migration." - validation: "Invalidate v6 and rebuild from a freshly rehashed 7.1 snapshot; block CLEAN/ISSUES_FIXED. Missing aggregate_finding_id: SHA-256 UTF-8 NFC(description) into legacy-v6::. Non-colliding records use the base; every colliding record appends : by packet order from 1. Before closure, source_finding_ids uses that ID with review_pass:persisted-v6-migration provenance." + description: "v6 to producer 7.2 migration." + validation: "Invalidate v6 and rebuild from a freshly rehashed 7.2 snapshot; block CLEAN/ISSUES_FIXED. Retain raw history and preserve each complete identity tuple unchanged. For a partial tuple, preserve a supplied aggregate_finding_id and fill only missing source_finding_ids with it and missing source_provenance with review_pass:persisted-v6-migration. Missing aggregate_finding_id: SHA-256 UTF-8 NFC(description) into legacy-v6::. Non-colliding records use the base; every colliding record appends : by packet order from 1. Before closure, source_finding_ids uses that ID with review_pass:persisted-v6-migration provenance only when each field is missing." on_missing: fail object_fields: - name: source_schema_version @@ -579,7 +610,7 @@ fields: - name: rebuilt_snapshot_identity type: object required: true - description: "Fresh 7.1 identity." + description: "Fresh 7.2 identity." object_fields: - name: basis type: enum @@ -600,7 +631,7 @@ fields: type: object required: conditional condition: "persisted review packet schema_version == 7.0" - validation: "Invalidate all 7.0 result/delegation/snapshot/Pack/QA/plan/coverage refs; do not reinterpret as 7.1. Rebuild required review/QA from a fresh 7.1 snapshot before CLEAN/ISSUES_FIXED." + validation: "Invalidate all 7.0 result/delegation/snapshot/Pack/QA/plan/coverage refs; do not reinterpret as 7.2. Retain every raw 7.0 previously_fixed record unchanged as historical evidence. Before closure dispatch, preserve each complete aggregate_finding_id/source_finding_ids/source_provenance tuple unchanged. For a partial tuple, preserve a supplied aggregate_finding_id and fill only missing source_finding_ids with that ID and missing source_provenance with review_pass:persisted-v7-0-migration. When aggregate_finding_id is absent, normalize UTF-8 NFC(description) into UTF-8 bytes, SHA-256 hash it, and use legacy-v7-0::; non-colliding records use the base and every colliding record appends : in original packet order, then fill only missing source_finding_ids with that ID and missing source_provenance with review_pass:persisted-v7-0-migration. Rebuild required review/QA from a fresh 7.2 snapshot before CLEAN/ISSUES_FIXED." on_missing: fail object_fields: - name: source_schema_version @@ -617,7 +648,45 @@ fields: - name: rebuilt_snapshot_identity type: object required: true - description: "Fresh 7.1 identity after rehash." + description: "Fresh 7.2 identity after rehash." + object_fields: + - name: basis + type: enum + required: true + enum_values: [git_revision, diff_digest, content_digest, task_or_pr_revision] + - name: value + type: string + required: true + - name: captured_at + type: string + required: true + - name: scope_manifest_digest + type: string + required: true + description: "Digest of the ordered scope_manifest and its content digests" + + - name: persisted_v7_1_packet_invalidation + type: object + required: conditional + condition: "persisted review packet schema_version == 7.1" + validation: "Invalidate all 7.1 result/delegation/snapshot/Pack/QA/plan/coverage refs; do not reinterpret as 7.2. Preserve existing 7.1-or-later previously_fixed provenance as historical evidence, then rebuild required review/QA from a fresh 7.2 snapshot before CLEAN/ISSUES_FIXED." + on_missing: fail + object_fields: + - name: source_schema_version + type: enum + required: true + enum_values: ["7.1"] + - name: batch_disposition + type: enum + required: true + enum_values: [invalidated_rebuild_required] + - name: rebuilt_review_snapshot_id + type: string + required: true + - name: rebuilt_snapshot_identity + type: object + required: true + description: "Fresh 7.2 identity after rehash." object_fields: - name: basis type: enum diff --git a/skills/assistant-review/contracts/output.yaml b/skills/assistant-review/contracts/output.yaml index 66e66a13..52db422a 100644 --- a/skills/assistant-review/contracts/output.yaml +++ b/skills/assistant-review/contracts/output.yaml @@ -1,11 +1,46 @@ # Review Output Contract # Defines what a completed review MUST produce before presenting results to the user. -schema_version: "7.1" +schema_version: "7.2" contract: output skill: assistant-review artifacts: + - name: change_impact_applicability + type: object + required: conditional + condition: "the review covers a behavior-bearing, cosmetic, or locality/shared/unresolved impact decision" + description: "Applicability." + validation: "not_applicable=cosmetic; compact local needs causal_evidence_ref, no checker; shared/unresolved/expanded needs expanded_artifact_carried=true." + on_fail: "Add scope/reason and local causal evidence; shared/unresolved/expanded need change_impact_review_projection or report blocked." + object_fields: + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: applicability_reason, type: string, required: true} + - {name: causal_evidence_ref, type: string, required: conditional, condition: "impact_scope == local"} + - {name: expanded_artifact_carried, type: boolean, required: true} + + - name: change_impact_review_projection + type: object + required: conditional + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + description: "Exact projection to the common completion document of existing review authority; it is not a second coverage ledger." + validation: "Preserves artifact_identity and maps every captured requirement obligation once to the original current scope_manifest item and coverage_ledger concern. review_source identities equal the canonical final review batch. When status is valid, validator_result_ref resolves to the current valid completion validator result, including its receipt bound to the current parsed inputs. A valid projection does not substitute for final_summary coverage_complete or CLEAN/ISSUES_FIXED; incomplete/blocked/read-only audit results remain explicit gaps." + on_fail: "Do not present a clean completion. Rebuild the current manifest/ledger/snapshot projection and rerun the completion checker for a missing or stale result, or report the missing/blocked impact evidence." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: status, type: enum, required: true, enum_values: [valid, gaps_reported, blocked, invalid]} + - {name: capture_ref, type: string, required: conditional, condition: "status == valid"} + - {name: expected_context_ref, type: string, required: conditional, condition: "status == valid"} + - {name: assessment_ref, type: string, required: conditional, condition: "status == valid"} + - {name: review_projection_ref, type: string, required: conditional, condition: "status == valid"} + - {name: validator_result_ref, type: string, required: conditional, condition: "status == valid"} + - {name: scope_manifest_id, type: string, required: conditional, condition: "status == valid"} + - {name: coverage_ledger_id, type: string, required: conditional, condition: "status == valid"} + - {name: review_snapshot_id, type: string, required: conditional, condition: "status == valid"} + - {name: snapshot_id, type: string, required: conditional, condition: "status == valid"} + - {name: canonical_final_summary_ref, type: string, required: conditional, condition: "status == valid"} + - {name: gap_or_blocker_ref, type: string, required: conditional, condition: "status in [gaps_reported, blocked, invalid]"} + - name: review_delegation_path type: object required: conditional @@ -916,7 +951,7 @@ artifacts: type: object[] required: true description: "Weighted aggregate score and aggregated union finding count per round — used for drift detection and capped at the 10-round review limit" - validation: "v7.1 score tracking uses aggregated_finding_count. Persisted 6.0/7.0 packets are invalidated and rebuilt before score tracking; legacy finding_count is retained only as historical evidence." + validation: "v7.2 score tracking uses aggregated_finding_count. Persisted 6.0/7.0/7.1 packets are invalidated and rebuilt before score tracking; legacy finding_count is retained only as historical evidence." object_fields: - name: round type: int diff --git a/skills/assistant-review/contracts/phase-gates.yaml b/skills/assistant-review/contracts/phase-gates.yaml index 8dc14121..3f9e2d18 100644 --- a/skills/assistant-review/contracts/phase-gates.yaml +++ b/skills/assistant-review/contracts/phase-gates.yaml @@ -2,7 +2,7 @@ # The review loop has 4 steps per round plus entry and exit conditions. # These gates enforce correct sequencing within each round and across the loop. -schema_version: "7.1" +schema_version: "7.2" contract: phase-gates skill: assistant-review @@ -34,6 +34,15 @@ gates: check: "mode is resolved (review-fix or audit)" on_fail: "Ask exactly: 'Should I only report findings, or also implement and verify fixes?' Stop before Reviewer dispatch or source mutation until the user answers. Do not use authorship, branch/PR ownership, platform, or connector as modification authorization." + - id: E_CHANGE_IMPACT_APPLICABILITY + check: "Before dispatch, every behavior, cosmetic, or locality review records change_impact_applicability with impact_scope and applicability_reason; local behavior requires causal_evidence_ref. Shared/unresolved or explicitly carried expanded work sets expanded_artifact_carried=true. Cosmetic work records not_applicable." + on_fail: "Keep the audit/fix result incomplete; load references/change-impact.md and record valid applicability before dispatch or expanded-scope selection." + + - id: E_CHANGE_IMPACT + check: "When impact is shared or materially unresolved, or an expanded artifact is explicitly carried, discovery requires identity/capture/expected refs; assessment is required only for pre-build or completion. The batch scope/coverage obligations include an independent locality, captured-consumer, material-transition, and canonical-mapping challenge. Evidenced local work retains ordinary review obligations without a common-checker gate." + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + on_fail: "Keep the audit/fix result incomplete, load references/change-impact.md, and repair or report the missing evidence before dispatch." + - id: E5 check: "change_kind, semantic_contract_review_required, behavioral_contract_review_required, agentic_loop_safety_review_required, and conditional architecture_decision_pack_review_required with canonical architecture_design_mode are resolved before review starts" on_fail: "Infer change_kind from touched paths/material; set semantic_contract_review_required=true for skill/workflow/framework changes, behavioral_contract_review_required=true for behavior-bearing code changes, agentic_loop_safety_review_required=true for autonomous/repeated loop changes, and architecture_decision_pack_review_required=true only when a carried Pack or relevant architecture decision record applies; require its top-level canonical architecture_design_mode." @@ -53,15 +62,20 @@ gates: on_fail: "Load references/qa-evaluation-loop.md, gather acceptance material and verification evidence, select domain rubric scope only when acceptance criteria, Done Contract, domain_context, or rubric_refs require it, or mark QA blocked with open_questions before dispatching QAEvaluator." - id: E8 - check: "Standalone Spec Review uses user request/scope. On FAIL, mode=review-fix repairs an authorized in-scope mismatch and repeats to spec_review_pass_ref; mode=audit exits repair/Build/source-mutation dispatch, retains the mismatch as a pre-batch aggregate finding, continues the frozen read-only multi-pass batch, and dispatches every planned read-only Reviewer pass through the response barrier." + check: "Standalone Spec Review uses user request/scope. On FAIL, mode=review-fix validates FIX_STEP entry_assertions before repair or mutation dispatch, then repairs an authorized in-scope mismatch and repeats to spec_review_pass_ref; mode=audit exits repair/Build/source-mutation dispatch, retains the mismatch as a pre-batch aggregate finding, continues the frozen read-only multi-pass batch, and dispatches every planned read-only Reviewer pass through the response barrier." condition: "review_preparation_mode == standalone" - on_fail: "In mode=review-fix, repair only an authorized in-scope mismatch and repeat to PASS. In mode=audit, record the mismatch, freeze the read-only batch, dispatch every required pass, wait for the response barrier, and never mutate source." + on_fail: "In mode=review-fix, validate FIX_STEP entry_assertions before repair or mutation dispatch, then repair only an authorized in-scope mismatch and repeat to PASS. In mode=audit, record the mismatch, freeze the read-only batch, dispatch every required pass, wait for the response barrier, and never mutate source." - id: E9 check: "Workflow-composed review consumes carried spec_review_pass_ref and carried current build/test evidence before Reviewer dispatch" condition: "review_preparation_mode == workflow_composed" on_fail: "Block dispatch and request the carried Spec Review PASS and current build/test evidence refs from the composing workflow." + - id: E10 + check: "A persisted 6.0, 7.0, or 7.1 review packet carries its matching invalidation/migration record and cannot contribute CLEAN or ISSUES_FIXED until a fresh 7.2 batch is rebuilt. Complete historic closure tuples, including existing 7.1-or-later provenance, remain unchanged; partial v6/7.0 tuples are normalized before closure dispatch." + condition: "a persisted review packet is supplied" + on_fail: "Invalidate the persisted result/delegation/snapshot/Pack/QA/plan/coverage refs, preserve raw history and complete closure tuples, normalize only missing v6/7.0 closure identity fields before dispatch, and rebuild from a fresh 7.2 snapshot." + # ───────────────────────────────────────────── # REVIEW step — per round # ───────────────────────────────────────────── @@ -221,6 +235,16 @@ gates: condition: "mode == review-fix" checkpoint_start: null checkpoint_end: null + entry_assertions: + + - id: F_CHANGE_IMPACT_APPLICABILITY + check: "Before any source/test mutation or mutation dispatch, record change_impact_applicability with impact_scope and applicability_reason; local behavior requires causal_evidence_ref. Shared/unresolved or explicitly carried expanded work sets expanded_artifact_carried=true. Cosmetic work records not_applicable." + on_fail: "Stop before edits or mutation dispatch; load references/change-impact.md and record valid applicability before selecting the local or expanded path." + + - id: F_CHANGE_IMPACT_PREBUILD + check: "Before any source/test mutation or mutation dispatch, a review-fix with triggered shared, unresolved, or expanded impact runs or revalidates the common checker against referenced capture, independent expected context, and assessment for a current valid pre_build result resolving validator refs for bounded planned obligations, consumer union, and material transitions." + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + on_fail: "Do not edit source, write a regression test, or dispatch a fixer; repair the pre-build assessment or return the review-fix as blocked/remaining." exit_assertions: - id: F1 @@ -235,6 +259,11 @@ gates: check: "Each newly fixed item was added to previously_fixed with its original aggregate_finding_id, source_finding_ids, source_provenance, description, round number, and pending_verification closure status; only legacy pre-ID records may receive deterministic normalized IDs." on_fail: "Update previously_fixed closure ledger before proceeding to VALIDATE" + - id: F_CHANGE_IMPACT + check: "When a review-fix mutates a triggered impact scope, refresh the bounded capture/expected/assessment before the next batch; prior locality or snapshot evidence is stale." + condition: "(impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried) and source changed" + on_fail: "Do not reuse the prior impact projection; recapture and revalidate before re-review." + # ───────────────────────────────────────────── # VALIDATE step — per round (review-fix mode only) # ───────────────────────────────────────────── @@ -331,10 +360,20 @@ gates: check: "A no-material-finding final result has complete coverage, uses the evidence-bounded claim 'No material findings within the reviewed scope and available evidence', and does not present CLEAN as proof of correctness" on_fail: "Keep the compatible machine enum, replace the user-facing certainty claim with the required evidence-bounded wording, and state residual risk." + - id: X_CHANGE_IMPACT + check: "When change-impact accounting is triggered, a current review projection maps the same artifact identity to the original canonical scope_manifest, coverage_ledger, current snapshot, and concern bindings. Common completion validation, final_summary coverage_complete, and CLEAN/ISSUES_FIXED remain independently required; a read-only audit reports gaps rather than manufacturing a clean claim." + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + on_fail: "Retain HAS_REMAINING_ITEMS/BLOCKED or audit gaps, refresh the canonical review batch, and rebuild the projection." + # ───────────────────────────────────────────── # Loop invariants # ───────────────────────────────────────────── invariants: + - id: INV_CHANGE_IMPACT + check: "Impact projection preserves the supplied artifact identity and uses canonical review authority only; it never creates a parallel ledger or permits a clean result without current terminal coverage." + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + scope: all_rounds + on_fail: "Use the existing manifest/ledger/final summary and report the unresolved gap." - id: INV0 check: "Only ONE summary is presented to the user — no intermediate results are shown during the loop" scope: all_rounds diff --git a/skills/assistant-review/evals/cases.json b/skills/assistant-review/evals/cases.json index 50da68b2..961e5501 100644 --- a/skills/assistant-review/evals/cases.json +++ b/skills/assistant-review/evals/cases.json @@ -154,7 +154,9 @@ "post-fix-review-regression-remains-open": "small_post_fix_closure", "audit-spec-review-fail-continues-complete-batch": "small_base", "in-flight-mutation-invalidates-review-batch": "medium_inflight_invalidation", - "trivial-audit-uses-two-isolated-passes": "trivial_base" + "trivial-audit-uses-two-isolated-passes": "trivial_base", + "compact-local-review-retains-causal-applicability": "small_base", + "compact-cosmetic-review-retains-applicability": "small_base" }, "case_requirements": { "audit-batch-waits-for-all-pass-results": {"mode":"audit","required_artifacts":["final_summary","audit_report","review_delegation_path"],"required_envelope_alias":"final_summary"}, @@ -164,7 +166,9 @@ "post-fix-review-regression-remains-open": {"mode":"review","required_artifacts":["final_summary","review_delegation_path"],"required_envelope_alias":"final_summary"}, "audit-spec-review-fail-continues-complete-batch": {"mode":"audit","required_artifacts":["final_summary","audit_report","review_delegation_path"],"required_envelope_alias":"final_summary"}, "in-flight-mutation-invalidates-review-batch": {"mode":"review","required_artifacts":["final_summary","review_delegation_path"],"required_envelope_alias":"final_summary"}, - "trivial-audit-uses-two-isolated-passes": {"mode":"audit","required_artifacts":["final_summary","audit_report","review_delegation_path"],"required_envelope_alias":"final_summary"} + "trivial-audit-uses-two-isolated-passes": {"mode":"audit","required_artifacts":["final_summary","audit_report","review_delegation_path"],"required_envelope_alias":"final_summary"}, + "compact-local-review-retains-causal-applicability": {"mode":"review","required_artifacts":["final_summary","review_delegation_path"],"required_envelope_alias":"final_summary"}, + "compact-cosmetic-review-retains-applicability": {"mode":"review","required_artifacts":["final_summary","review_delegation_path"],"required_envelope_alias":"final_summary"} } }, "canonical_qa_case_requirements": { @@ -191,7 +195,9 @@ "post-fix-review-regression-remains-open": {"active_must_fix_aggregate_finding_ids":["aggregate-fixed-1"],"fixed_must_fix_aggregate_finding_ids":["aggregate-fixed-1"]}, "audit-spec-review-fail-continues-complete-batch": {"active_must_fix_aggregate_finding_ids":["aggregate-1","aggregate-spec-1"],"fixed_must_fix_aggregate_finding_ids":[]}, "in-flight-mutation-invalidates-review-batch": {"active_must_fix_aggregate_finding_ids":[],"fixed_must_fix_aggregate_finding_ids":[]}, - "trivial-audit-uses-two-isolated-passes": {"active_must_fix_aggregate_finding_ids":[],"fixed_must_fix_aggregate_finding_ids":[]} + "trivial-audit-uses-two-isolated-passes": {"active_must_fix_aggregate_finding_ids":[],"fixed_must_fix_aggregate_finding_ids":[]}, + "compact-local-review-retains-causal-applicability": {"active_must_fix_aggregate_finding_ids":[],"fixed_must_fix_aggregate_finding_ids":[]}, + "compact-cosmetic-review-retains-applicability": {"active_must_fix_aggregate_finding_ids":[],"fixed_must_fix_aggregate_finding_ids":[]} }, "canonical_review_snapshot_expectations": { "audit-batch-waits-for-all-pass-results": {"final_review_snapshot_id":"snapshot-1","final_snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"},"batch_projections":[{"batch_id":"batch-1","review_snapshot_id":"snapshot-1","snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"}}]}, @@ -201,7 +207,9 @@ "post-fix-review-regression-remains-open": {"final_review_snapshot_id":"snapshot-1","final_snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"},"batch_projections":[{"batch_id":"batch-0","review_snapshot_id":"snapshot-0","snapshot_identity":{"basis":"diff_digest","value":"digest-0","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-0"}},{"batch_id":"batch-1","review_snapshot_id":"snapshot-1","snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"}}]}, "audit-spec-review-fail-continues-complete-batch": {"final_review_snapshot_id":"snapshot-1","final_snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"},"batch_projections":[{"batch_id":"batch-1","review_snapshot_id":"snapshot-1","snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"}}]}, "in-flight-mutation-invalidates-review-batch": {"final_review_snapshot_id":"snapshot-1","final_snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"},"batch_projections":[{"batch_id":"batch-1","review_snapshot_id":"snapshot-1","snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"}}]}, - "trivial-audit-uses-two-isolated-passes": {"final_review_snapshot_id":"snapshot-1","final_snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"},"batch_projections":[{"batch_id":"batch-1","review_snapshot_id":"snapshot-1","snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"}}]} + "trivial-audit-uses-two-isolated-passes": {"final_review_snapshot_id":"snapshot-1","final_snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"},"batch_projections":[{"batch_id":"batch-1","review_snapshot_id":"snapshot-1","snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"}}]}, + "compact-local-review-retains-causal-applicability": {"final_review_snapshot_id":"snapshot-1","final_snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"},"batch_projections":[{"batch_id":"batch-1","review_snapshot_id":"snapshot-1","snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"}}]}, + "compact-cosmetic-review-retains-applicability": {"final_review_snapshot_id":"snapshot-1","final_snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"},"batch_projections":[{"batch_id":"batch-1","review_snapshot_id":"snapshot-1","snapshot_identity":{"basis":"diff_digest","value":"digest-1","captured_at":"2026-08-28T00:00:00Z","scope_manifest_digest":"manifest-1"}}]} }, "cases": [ { @@ -2027,6 +2035,67 @@ {"operator":"equals","path":["qa_evaluation_result","score_progression",0,"weighted_score"],"expected":1.8} ] } + }, + { + "id": "compact-local-review-retains-causal-applicability", + "title": "Review retains compact local causal applicability", + "category": "multi_pass_review_batch", + "purpose": "Requires a complete canonical small review result to retain local causal applicability without an expanded projection.", + "prompt": "Review a local navigation guard fix with supplied causal evidence. Return the review result.", + "setup_context": ["Only the traced route reaches the guard.", "No shared consumer or expanded impact artifact is carried."], + "expected_behavior": ["Returns the complete canonical small review envelope.", "Carries compact local applicability with its causal evidence.", "Does not invent an expanded impact projection."], + "pass_criteria": ["The local scope, reason, and causal evidence are present.", "The canonical final summary and delegation path remain present."], + "fail_signals": ["Drops causal evidence.", "Replaces the canonical review envelope with applicability alone.", "Adds a shared projection for the local route."], + "machine_expectations": { + "required_substrings": ["change_impact_applicability", "local", "causal evidence"], + "forbidden_substrings": ["shared projection"], + "structured_json_assertions": [ + {"operator":"equals","path":["final_summary","result"],"expected":"CLEAN"}, + {"operator":"equals","path":["change_impact_applicability","impact_scope"],"expected":"local"}, + {"operator":"nonempty_string","path":["change_impact_applicability","applicability_reason"]}, + {"operator":"nonempty_string","path":["change_impact_applicability","causal_evidence_ref"]}, + {"operator":"equals","path":["change_impact_applicability","expanded_artifact_carried"],"expected":false}, + {"operator":"path_absent","path":["change_impact_review_projection"]} + ] + } + }, + { + "id": "compact-cosmetic-review-retains-applicability", + "title": "Review retains compact cosmetic applicability", + "category": "multi_pass_review_batch", + "purpose": "Requires a complete canonical small review result to retain a cosmetic applicability decision without causal or expanded evidence.", + "prompt": "Review a copy-only navigation label correction. Return the review result.", + "setup_context": ["The change is cosmetic and does not alter navigation behavior.", "No local causal trace or expanded impact artifact is carried."], + "expected_behavior": ["Returns the complete canonical small review envelope.", "Carries not_applicable applicability with a reason.", "Does not invent causal or expanded impact evidence."], + "pass_criteria": ["The cosmetic scope and reason are present.", "The canonical final summary and delegation path remain present."], + "fail_signals": ["Drops the applicability decision.", "Adds causal evidence for a cosmetic correction.", "Replaces the canonical review envelope with applicability alone."], + "machine_expectations": { + "required_substrings": ["change_impact_applicability", "not_applicable", "cosmetic"], + "forbidden_substrings": ["causal evidence required"], + "structured_json_assertions": [ + {"operator":"equals","path":["final_summary","result"],"expected":"CLEAN"}, + {"operator":"equals","path":["change_impact_applicability","impact_scope"],"expected":"not_applicable"}, + {"operator":"nonempty_string","path":["change_impact_applicability","applicability_reason"]}, + {"operator":"equals","path":["change_impact_applicability","expanded_artifact_carried"],"expected":false}, + {"operator":"path_absent","path":["change_impact_applicability","causal_evidence_ref"]}, + {"operator":"path_absent","path":["change_impact_review_projection"]} + ] + } + }, + { + "id": "shared-impact-audit-rejects-checkout-only-projection", + "title": "Review keeps canonical coverage authoritative for shared impact", + "category": "impact_accounting", + "purpose": "Rejects a review that covers checkout alone while carried impact evidence names invoice, retry, and configuration consumers.", + "prompt": "Run a read-only audit of a checkout helper change. The carried change-impact capture includes checkout, invoice export, retry recovery, and configuration validation, but the proposed review manifest covers checkout only. Return the audit result.", + "setup_context": ["The supplied impact identity is current.", "The canonical review manifest omits three captured consumers."], + "expected_behavior": ["Reports the missing consumer mappings as review/impact gaps.", "Does not create a replacement coverage ledger.", "Does not claim CLEAN from an impact projection."], + "pass_criteria": ["The audit returns remaining coverage work.", "The canonical ledger remains the terminal review authority."], + "fail_signals": ["Accepts checkout-only coverage.", "Reports CLEAN while canonical coverage is incomplete."], + "machine_expectations": { + "required_substrings": ["invoice", "retry", "configuration", "coverage", "gap", "HAS_REMAINING_ITEMS"], + "forbidden_substrings": ["checkout-only coverage is sufficient", "impact projection makes this CLEAN"] + } } ] } diff --git a/skills/assistant-review/references/change-impact.md b/skills/assistant-review/references/change-impact.md new file mode 100644 index 00000000..90eae6d8 --- /dev/null +++ b/skills/assistant-review/references/change-impact.md @@ -0,0 +1,24 @@ +# Change-impact review projection + +Load for a review that covers a behavior or cosmetic change, carries a change-impact +artifact, or has a locality/shared/unresolved decision. Record compact +applicability evidence for every behavior change. Keep this outside the fresh +reviewer context bundle: the existing 5653-word reviewer-context budget remains +unchanged. + +Resolve the common checker from the loaded skill directory as +`../../tools/change-impact/validate-change-impact.cjs`, quoted and invoked with +explicit files under Node 22+ only for shared, materially unresolved, or +explicitly carried expanded work. This works for selective installs and custom +homes; do not use the process cwd or require assistant-workflow. Evidenced local +work keeps ordinary review and verification without a Node gate. + +Use the existing canonical `scope_manifest`, `coverage_ledger`, current review +snapshot, and concern IDs to create the v1 completion projection. Map each +captured requirement once; do not create an impact-specific coverage ledger. +Read-only audit can identify absent/stale mappings and report the gap. Review +fix must refresh its frozen snapshot after mutation. Fresh runs omit +`--receipt`; reuse reruns the checker with `--receipt` pointing to +`validator_result_ref`, which must match the current parsed inputs. A valid impact projection +never replaces `final_summary.coverage_complete`, required terminal passes, or +the CLEAN/ISSUES_FIXED decision. diff --git a/skills/assistant-review/references/review-batch.md b/skills/assistant-review/references/review-batch.md index e0848a1c..741f4ec5 100644 --- a/skills/assistant-review/references/review-batch.md +++ b/skills/assistant-review/references/review-batch.md @@ -2,7 +2,7 @@ Audit/read-only: frozen batches. -1. Capture identity; mutation invalidates. Rebuild persisted 6.0 or incompatible 7.0 under 7.1 from a fresh snapshot before `CLEAN`/`ISSUES_FIXED`. +1. Capture identity; mutation invalidates. Rebuild persisted 6.0 or incompatible 7.0/7.1 under 7.2 from a fresh snapshot before `CLEAN`/`ISSUES_FIXED`; retain raw history and complete closure tuples as historical evidence, and normalize only missing v6/7.0 closure identity fields before closure dispatch. 2. Plan a minimum of two independent narrow passes; medium adds integration, large architecture; max six. Security uses Code Reviewer `assistant-security`; Reviewer schema authoritative. 3. Unique IDs/scopes/obligations/concerns; exact tuples; sibling-blind; aggregate ledger. 4. Returns carry tuple/status/evidence; only `inspected_no_risk`/`finding` completes. Source IDs: `review_pass_id`; closure: `aggregate_finding_id`. diff --git a/skills/assistant-review/references/review-loop.md b/skills/assistant-review/references/review-loop.md index 8901ddde..0620cc1d 100644 --- a/skills/assistant-review/references/review-loop.md +++ b/skills/assistant-review/references/review-loop.md @@ -4,14 +4,15 @@ Load this reference before the first REVIEW step. ``` round = 1 -previously_fixed = [] +previously_fixed = validated_entry.previously_fixed || [] score_history = [] PREPARE + - Validate entry fields, then seed `previously_fixed` from the validated entry value; use `[]` only when it is absent. Retain raw persisted history. For an invalidated persisted 7.1 packet, retain every aggregate_finding_id, source_finding_ids, and source_provenance entry while invalidating current result/delegation/snapshot/Pack/QA/plan/coverage refs. Before closure dispatch, normalize only records missing any canonical identity tuple field: preserve complete v6, 7.0, 7.1, and current tuples unchanged; for a partial v6/7.0 tuple, preserve a supplied aggregate_finding_id and fill only missing source_finding_ids with it and missing source_provenance with review_pass:persisted-v6-migration or review_pass:persisted-v7-0-migration. When the aggregate ID is absent, use the matching UTF-8 NFC(description)/SHA-256/collision rule and legacy-v6:: or legacy-v7-0::, then fill only missing source_finding_ids and source_provenance. Invalidate persisted 6.0/7.0/7.1 score-tracking refs before rebuilding the fresh 7.2 batch. - Infer review mode before review or mutation. Report-only selects audit; in-scope authority selects review-fix. If unresolved, ask exactly: "Should I only report findings, or also implement and verify fixes?" - - Standalone Spec Review on scope. On mismatch, run only in review-fix mode to repair evidence-backed must-fix and should-fix items. In audit, retain and stay read-only. Dispatch every planned read-only Reviewer pass with sanitized criteria; never expose the mismatch details to discovery siblings. Return it to the composing workflow for planning or approval when it changes scope, risk, or acceptance. + - Standalone Spec Review on scope. Validate FIX_STEP entry_assertions before repair or mutation dispatch. On mismatch, run only in review-fix mode to repair evidence-backed must-fix and should-fix items. In audit, retain and stay read-only. Dispatch every planned read-only Reviewer pass with sanitized criteria; never expose the mismatch details to discovery siblings. Return it to the composing workflow for planning or approval when it changes scope, risk, or acceptance. - Workflow consumes Spec PASS/current build evidence; after a fix, `not_applicable` is invalid. while round <= 10: @@ -19,7 +20,7 @@ while round <= 10: A score below the rubric threshold alone is insufficient to start round 3 or later. 1. REVIEW - - Freeze snapshot/manifest/identity; mutation invalidates. Rebuild persisted 6.0/7.0 under 7.1 before clean. Load `references/review-batch.md` before batch planning. + - Freeze snapshot/manifest/identity; mutation invalidates. Rebuild persisted 6.0/7.0/7.1 under 7.2 before clean while retaining raw history and complete closure tuples. Load `references/review-batch.md` before batch planning. - Derive canonical discovery passes. Without trigger, trivial/small uses two purpose-specific isolated direct-fallback passes; trigger delegates scope. Security/post-fix closure are additive, max six. - First resolve `reviewer_context` from `contracts/index.yaml` strictly below 5653 words; carry bounded selector before response; siblings are blind; only closure gets ledger. - Load principles/rubric/triggered checklist sections. Medium+ returns Design Coherence evidence or `no concrete risk found`. Run a bounded independent capability search; carried Mapper evidence cannot satisfy review. @@ -31,7 +32,7 @@ while round <= 10: - PIVOT/STAGNATION/DRIFT/REGRESSION: orchestrator records pivot_restart_decision. 3. FIX / VALIDATE - - Only review-fix repairs authorized evidence; record closure. + - Validate FIX_STEP entry_assertions before source/test mutation or dispatch. Only review-fix repairs authorized evidence; record closure. - Run build/tests; mutation invalidates the batch and requires new evidence. 4. NEXT ROUND diff --git a/skills/assistant-skill-creator/references/skill-contract-design-guide.md b/skills/assistant-skill-creator/references/skill-contract-design-guide.md index 08977c58..7456267a 100644 --- a/skills/assistant-skill-creator/references/skill-contract-design-guide.md +++ b/skills/assistant-skill-creator/references/skill-contract-design-guide.md @@ -322,11 +322,12 @@ gates: checkpoint_start: "--- PHASE: NAME ---" checkpoint_end: "--- PHASE: NAME COMPLETE ---" condition: "when this phase runs" # optional + entry_assertions: [] # Optional; same schema, checked before any mutation or mutation dispatch exit_assertions: - - id: XX1 # unique ID for referencing - check: "what must be true" # plain-English assertion - condition: "when applicable" # optional — scopes the assertion - on_fail: "corrective action" # what to do if assertion fails + - id: XX1 # unique + check: "what must be true" + condition: "when applicable" # optional + on_fail: "corrective action" invariants: - id: INV1 @@ -512,3 +513,7 @@ encode arbitrary structures. Recipe, typed refs, separate code-review/QA handoffs, pivot/restart decisions, and the max 10 terminal cap when the Process skill has long-running review, QA, or fix-verify loops +15. **Shared-impact evidence** — preserve identity. Discovery reports gaps; + mutation requires current valid pre-build entry evidence; completion needs + verification/review. Missing loaded-path runtimes block. Fanout alone + does not escalate architecture, QA, or lane. diff --git a/skills/assistant-workflow/SKILL.md b/skills/assistant-workflow/SKILL.md index 773e1751..8d727e56 100644 --- a/skills/assistant-workflow/SKILL.md +++ b/skills/assistant-workflow/SKILL.md @@ -1,6 +1,8 @@ --- name: assistant-workflow description: "Prepare, plan, build, or resume persisted task state. Use for repository-grounded feature/epic/story technical preparation, implementation, fixes, migrations, refactors, and project artifacts." +requires: + - assistant-review --- # Development Workflow @@ -24,6 +26,7 @@ Move work to verified outcome through right-sized phases, gates, tests, review, - Triggered Architecture Decision Packs are source-backed and fresh. `prepare_only` retains Discover-only Pack context through Preparation Completion; `execution_intent != prepare_only` continues through Plan binding, task packets, Build, handoff, and Review. - Behavior changes default tests-first or carry explicit validation in the same Build step. - Existing-system prep inspects sources, code, and tests. `prepare_only` ends without implementation claims; optional readiness planning is inline. Carry the exact approved typed preparation result into `implement_only`; existing-system evidence or a `not_applicable` basis and inactive future harness/QA obligations remain part of that result before their gates activate. Product questions require evidence. +- A behavior-bearing change records compact change-impact applicability evidence. Only shared, materially unresolved, or explicitly carried expanded impact assessments use the common checker and carry an artifact identity through planning, Build, and assistant-review; the checker is resolved from the active installation, never the repository cwd. - Review, QA, and security routing apply when triggered. - Medium+ final handoff binds canonical review/QA state and exact snapshot; remaining items, incomplete coverage, or rejected/blocked QA forbid completion. @@ -50,6 +53,7 @@ Canonical contracts are authoritative. Read `contracts/index.yaml` first, valida - `entry`: load entry fields declared by `contracts/index.yaml` from `contracts/input.yaml`; `references/triage-rubric.md` is the only declared entry reference. - `architecture_design`: load `references/architecture-decision-pack.md` when `architecture_design_mode != not_applicable`; use its typed artifact before Decompose or Plan and retain its reference through Review. - `feature_preparation`: for repository-grounded preparation, existing behavior, any `implement_only` workflow, or any carried `approved_feature_preparation_harness_obligation` or `approved_feature_preparation_qa_acceptance_obligation` (including `feature_preparation_scope=not_applicable`), load `references/feature-preparation-evidence.md`; resolve and retain `approved_feature_preparation_result` unchanged plus the applicable evidence ref or typed `not_applicable` source binding before Decompose, Plan, or Build. A carried QA obligation uses only the existing post-Build, post-Code-Reviewer QA Evaluator lane. +- `change_impact`: load `references/change-impact.md` when behavior, cosmetic, or local/shared/unresolved impact affects verification. Record compact applicability; only shared, unresolved, or explicitly carried expanded work requires common pre-Build/completion. - `progressive_discovery`: load `references/progressive-discovery.md` when `uncertainty_shape=progressive`, either durable marker is pending/consumed or active/closed, or `progressive_artifact_retention_state=terminally_archived`; durable markers route even when the retention state is missing or invalid. `terminally_archived` releases the durable artifacts only with the typed `progressive_terminal_archival` tombstone and explicit final archival/termination evidence, never merely `Task state: completed`, and cannot revert. - `delegation`: load role and trigger fields when roles may be required and before any subagent dispatch. - `current_phase`: active `contracts/phase-gates.yaml` at transition. @@ -60,7 +64,7 @@ Selectors resolve by unique id plus canonical path, section, key, and explicit n Missing or invalid selector: `load_full_authoritative_file`; validate the full named canonical file and record recovery. -Migration note: assistant-workflow contracts are v11. v10: `prepare_only` uses `feature_preparation_result`, records execution not started, omits Build/test/review/final-handoff claims, and retains `handoff_binding_state=discover_only` through Preparation Completion even for optional readiness planning. v9: `execution_intent`; existing-system preparation produces or carries `feature_preparation_evidence`. Bash/provider review runners are removed; native task packets retain scope and verification. v8: consumed all-excluded route-clear maps may keep `entries=[]` only with complete exclusion lineage; Pack alternatives use stable `selected_alternative_id` bindings and verified `quality_scenario_id` scenarios use resolvable verification identity. Pack `review_result` retains canonical refs. v6: `semantic_type_inspection`, `contributor_evidence`. v4 consumers use `handoff_binding_state=discover_only` +Migration note: assistant-workflow contracts are v11.1. v11.0 packets lacking a triggered `change_impact_evidence` envelope are not completion-compatible when a behavior/locality gate applies; recapture and rebuild current evidence rather than inventing historical execution or review. v10: `prepare_only` uses `feature_preparation_result`, records execution not started, omits Build/test/review/final-handoff claims, and retains `handoff_binding_state=discover_only` through Preparation Completion even for optional readiness planning. v9: `execution_intent`; existing-system preparation produces or carries `feature_preparation_evidence`. Bash/provider review runners are removed; native task packets retain scope and verification. v8: consumed all-excluded route-clear maps may keep `entries=[]` only with complete exclusion lineage; Pack alternatives use stable `selected_alternative_id` bindings and verified `quality_scenario_id` scenarios use resolvable verification identity. Pack `review_result` retains canonical refs. v6: `semantic_type_inspection`, `contributor_evidence`. v4 consumers use `handoff_binding_state=discover_only` with Discover context/journal. For `execution_intent != prepare_only`, Plan atomically binds task/review refs as `downstream_bound` before Build when `plan_mode!=none`; plan_mode=none binds @@ -143,7 +147,7 @@ Load the generated `references/phases/.md` view for the active ph | Plan | `plan_mode != none`; optional prepare_only readiness | Inline/approval routing. | | Design | UI only; `execution_intent != prepare_only` | Direction, checklist, approval. | | Build | `execution_intent != prepare_only` | Light direct; ordinary medium one edit/test executor; separation only for high-risk or broad/noisy/environment-heavy verification. Tests/validation travel with code. | -| Review | `execution_intent != prepare_only` | Light fresh self-review; standard/strict Spec Review then independent `assistant-review`; QA only when required. | +| Review | `execution_intent != prepare_only` | Light self-review; expanded impact uses canonical `assistant-review`; standard/strict use independent `assistant-review`; QA when required. | | Document | `execution_intent != prepare_only` | Apply state/manual-verification modes; metrics optional and non-blocking. | | Preparation Completion | `prepare_only` | Return readiness; approval is next. | diff --git a/skills/assistant-workflow/contracts/handoffs.yaml b/skills/assistant-workflow/contracts/handoffs.yaml index b4c6c2b6..9e2874dd 100644 --- a/skills/assistant-workflow/contracts/handoffs.yaml +++ b/skills/assistant-workflow/contracts/handoffs.yaml @@ -7,7 +7,7 @@ # Any subagent returning results MUST include the required output fields. # If a required field is missing from the return, follow the on_missing action. -schema_version: "11.0" +schema_version: "11.1" contract: handoffs skill: assistant-workflow @@ -48,7 +48,7 @@ worker_status_protocol: artifact_reference_protocol: description: "Typed Artifact Reference entries are the compact producer/consumer ledger for harness task packets and worker returns; use them instead of ad hoc string refs when passing artifacts between agents." required_fields: [artifact_id, artifact_type, producer, consumer, location_ref, schema_or_contract, validation_status, summary] - artifact_types: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, test_result, review_result, qa_evaluation_result] + artifact_types: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, change_impact_evidence, test_result, review_result, qa_evaluation_result] validation_statuses: [pending, valid, invalid, stale, not_applicable] packet_rules: - "Artifact Reference entries must include artifact_id, artifact_type, producer, consumer, location_ref, schema_or_contract, validation_status, and summary." @@ -925,6 +925,27 @@ handoffs: type: string required: true description: "Restated requirements from Discover phase" + - name: change_impact_evidence + type: object + required: conditional + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + description: "Current expanded common impact artifact for Architect planning." + validation: "Preserves artifact_identity, phase, capture_ref, expected_context_ref, validator_result_ref, impact_scope, and status unchanged through task-packet planning; assessment_ref is carried only for pre-build or completion. Completion carries the actual review document pointer for rerun or receipt reuse." + on_missing: re-dispatch + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: phase, type: enum, required: true, enum_values: [discovery, pre_build, completion]} + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: status, type: enum, required: true, enum_values: [valid]} + - {name: capture_ref, type: string, required: true} + - {name: expected_context_ref, type: string, required: true} + - {name: assessment_ref, type: string, required: conditional, condition: "phase in [pre_build, completion]"} + - {name: validator_result_ref, type: string, required: true} + - name: review_projection_ref + type: string + required: conditional + condition: "phase == completion" + validation: "Completion carries the actual review document pointer for rerun or receipt reuse." - name: requirement_acceptance_map type: string required: true @@ -1478,7 +1499,7 @@ handoffs: - name: artifact_type type: enum required: true - enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, test_result, review_result, qa_evaluation_result] + enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, change_impact_evidence, test_result, review_result, qa_evaluation_result] - name: producer type: string required: true @@ -1602,6 +1623,26 @@ handoffs: to: CodeWriter phase: BUILD context_fields: + - name: change_impact_applicability + type: object + required: conditional + condition: "the current slice has a behavior-bearing change or locality/shared/unresolved impact decision" + validation: "impact_scope and applicability_reason are carried unchanged. local carries causal evidence and does not require expanded checker refs; shared/unresolved or explicit expansion carries the matching expanded artifact envelope." + - name: change_impact_evidence + type: object + required: conditional + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + description: "Current resolved pre-build artifact envelope that authorizes this CodeWriter mutation dispatch." + validation: "The common checker was freshly run, or was rerun with --receipt validator_result_ref against the referenced capture_ref, independent expected_context_ref, and assessment_ref for a current valid pre_build result that resolves artifact_identity against the current task packet; impact_scope and status remain bound unchanged. A status or result ref alone is insufficient. The CodeWriter may not use discovery or completion evidence as mutation authorization." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: phase, type: enum, required: true, enum_values: [pre_build]} + - {name: capture_ref, type: string, required: true} + - {name: expected_context_ref, type: string, required: true} + - {name: assessment_ref, type: string, required: true} + - {name: validator_result_ref, type: string, required: true} + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: status, type: enum, required: true, enum_values: [valid]} - name: build_execution_lane type: enum required: true @@ -1908,7 +1949,7 @@ handoffs: - name: artifact_type type: enum required: true - enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, test_result, review_result, qa_evaluation_result] + enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, change_impact_evidence, test_result, review_result, qa_evaluation_result] - name: producer type: string required: true @@ -2174,7 +2215,7 @@ handoffs: - name: artifact_type type: enum required: true - enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, test_result, review_result, qa_evaluation_result] + enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, change_impact_evidence, test_result, review_result, qa_evaluation_result] - name: producer type: string required: true @@ -2226,6 +2267,11 @@ handoffs: to: BuilderTester phase: BUILD context_fields: + - name: change_impact_applicability + type: object + required: conditional + condition: "the current slice has a behavior-bearing change or locality/shared/unresolved impact decision" + validation: "impact_scope and applicability_reason are carried unchanged. local carries causal evidence and does not require expanded checker refs; shared/unresolved or explicit expansion carries the matching expanded artifact envelope." - name: build_execution_lane type: enum required: true @@ -2239,6 +2285,21 @@ handoffs: type: string[] required: false description: "Legacy alias for changed_files" + - name: change_impact_evidence + type: object + required: conditional + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + description: "Current resolved pre-build artifact envelope that authorizes this BuilderTester mutation dispatch." + validation: "The common checker was freshly run, or was rerun with --receipt validator_result_ref against the referenced capture_ref, independent expected_context_ref, and assessment_ref for a current valid pre_build result that resolves artifact_identity against the current task packet; impact_scope and status remain bound unchanged. A status or result ref alone is insufficient, and BuilderTester must not reuse CodeWriter's pre-build result after earlier mutations." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: phase, type: enum, required: true, enum_values: [pre_build]} + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: status, type: enum, required: true, enum_values: [valid]} + - {name: capture_ref, type: string, required: true} + - {name: expected_context_ref, type: string, required: true} + - {name: assessment_ref, type: string, required: true} + - {name: validator_result_ref, type: string, required: true} - name: build_command type: string required: false @@ -2536,7 +2597,7 @@ handoffs: - name: artifact_type type: enum required: true - enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, test_result, review_result, qa_evaluation_result] + enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, change_impact_evidence, test_result, review_result, qa_evaluation_result] - name: producer type: string required: true @@ -2675,7 +2736,7 @@ handoffs: - name: artifact_type type: enum required: true - enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, test_result, review_result, qa_evaluation_result] + enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, change_impact_evidence, test_result, review_result, qa_evaluation_result] - name: producer type: string required: true diff --git a/skills/assistant-workflow/contracts/index.yaml b/skills/assistant-workflow/contracts/index.yaml index 656d263e..b1a8e014 100644 --- a/skills/assistant-workflow/contracts/index.yaml +++ b/skills/assistant-workflow/contracts/index.yaml @@ -1,4 +1,4 @@ -schema_version: "11.0" +schema_version: "11.1" contract: index skill: assistant-workflow @@ -52,7 +52,22 @@ load_sets: section: artifacts key: name names: [feature_preparation_evidence, feature_preparation_result] - budget_words: 5900 + budget_words: 6000 + change_impact: + references: + - references/change-impact.md + selectors: + - id: workflow-change-impact-input + path: contracts/input.yaml + section: fields + key: name + names: [change_impact_applicability, change_impact_context] + - id: workflow-change-impact-output + path: contracts/output.yaml + section: artifacts + key: name + names: [change_impact_applicability, change_impact_evidence] + budget_words: 3000 progressive_discovery: references: - references/progressive-discovery.md @@ -88,8 +103,8 @@ load_sets: path: contracts/phase-gates.yaml section: invariants key: id - names: [INV_TASK_STATE_FRESHNESS,INV1,INV2,INV3,INV4,INV_SUBAGENT_TRIGGER_STATE,INV_PIVOT_RESTART_REAPPROVAL,INV_CONTROLLER_INTENSITY_STANDARD,INV_BUILD_REPAIR_BOUNDED,INV_FEATURE_PREPARATION_QUESTION_ADMISSIBILITY,INV_ARCHITECTURE_PACK_FRESHNESS,INV_ARCHITECTURE_PACK_HANDOFF_BINDING,INV_SEMANTIC_TYPE_BOUNDARIES,INV_PROGRESSIVE_DISCOVERY_BOUNDARY,INV_PROGRESSIVE_SINGLE_ACTIVE,INV_PROGRESSIVE_DEPENDENCY_ORDER,INV_PROGRESSIVE_ACTIVATION_PROVENANCE,INV_PROGRESSIVE_HUMAN_EVIDENCE,INV_PROGRESSIVE_COLLABORATIVE_EVIDENCE,INV_PROGRESSIVE_BLOCKED_RECOVERY,INV_PROGRESSIVE_RECOMPUTATION,INV_PROGRESSIVE_ROUTE_CLEAR,INV_PROGRESSIVE_RETAINED_REFERENCE_CHAIN,INV_PROGRESSIVE_ARTIFACT_RETENTION_STATE,INV_PROGRESSIVE_TERMINAL_ARCHIVAL,INV_PROGRESSIVE_REPEAT_READINESS] - budget_words: 7200 + names: [INV_TASK_STATE_FRESHNESS,INV1,INV2,INV3,INV4,INV_SUBAGENT_TRIGGER_STATE,INV_PIVOT_RESTART_REAPPROVAL,INV_CONTROLLER_INTENSITY_STANDARD,INV_BUILD_REPAIR_BOUNDED,INV_FEATURE_PREPARATION_QUESTION_ADMISSIBILITY,INV_ARCHITECTURE_PACK_FRESHNESS,INV_ARCHITECTURE_PACK_HANDOFF_BINDING,INV_SEMANTIC_TYPE_BOUNDARIES,INV_CHANGE_IMPACT,INV_PROGRESSIVE_DISCOVERY_BOUNDARY,INV_PROGRESSIVE_SINGLE_ACTIVE,INV_PROGRESSIVE_DEPENDENCY_ORDER,INV_PROGRESSIVE_ACTIVATION_PROVENANCE,INV_PROGRESSIVE_HUMAN_EVIDENCE,INV_PROGRESSIVE_COLLABORATIVE_EVIDENCE,INV_PROGRESSIVE_BLOCKED_RECOVERY,INV_PROGRESSIVE_RECOMPUTATION,INV_PROGRESSIVE_ROUTE_CLEAR,INV_PROGRESSIVE_RETAINED_REFERENCE_CHAIN,INV_PROGRESSIVE_ARTIFACT_RETENTION_STATE,INV_PROGRESSIVE_TERMINAL_ARCHIVAL,INV_PROGRESSIVE_REPEAT_READINESS] + budget_words: 7400 selected_handoff: selectors: - id: workflow-selected-handoff @@ -106,7 +121,7 @@ load_sets: section: artifacts key: name name_from: applicable_completion_artifact - allowed_names: [triage_result,completion_policy,phase_checkpoints,changed_files,debugging_result,test_results, validation_results, fresh_review_result, review_result, qa_evaluation_result, spec_review_result, manual_test_steps, manual_verification_result, subagent_evidence, build_repair_state, task_state_reconciliation, task_journal, context_map, context_budget_note, pattern_retrieval_note, context_handoff_packet, requirement_acceptance_map, architecture_decision_pack, feature_preparation_evidence, feature_preparation_result, final_handoff, artifact_contract, artifact_reference_ledger, done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, decomposition_plan_review, slice_manifest, single_slice_rationale, slice_verification_summary, metrics_entry, candidate_search_result, workflow_experiment_ledger, loop_readiness_assessment, plan_document, user_approval, verified_skill_distillation] + allowed_names: [triage_result,completion_policy,phase_checkpoints,changed_files,debugging_result,test_results, validation_results, fresh_review_result, review_result, qa_evaluation_result, spec_review_result, manual_test_steps, manual_verification_result, subagent_evidence, build_repair_state, task_state_reconciliation, task_journal, context_map, context_budget_note, pattern_retrieval_note, context_handoff_packet, requirement_acceptance_map, architecture_decision_pack, feature_preparation_evidence, feature_preparation_result, change_impact_applicability, change_impact_evidence, final_handoff, artifact_contract, artifact_reference_ledger, done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, decomposition_plan_review, slice_manifest, single_slice_rationale, slice_verification_summary, metrics_entry, candidate_search_result, workflow_experiment_ledger, loop_readiness_assessment, plan_document, user_approval, verified_skill_distillation] budget_words: 12000 fallback: diff --git a/skills/assistant-workflow/contracts/input.yaml b/skills/assistant-workflow/contracts/input.yaml index 8566c733..c0c1899c 100644 --- a/skills/assistant-workflow/contracts/input.yaml +++ b/skills/assistant-workflow/contracts/input.yaml @@ -3,7 +3,7 @@ # Fields are extracted from the user's request. If a required field cannot be inferred, # follow the on_missing action. -schema_version: "11.0" +schema_version: "11.1" contract: input skill: assistant-workflow @@ -121,6 +121,37 @@ fields: default: [] on_missing: skip + - name: change_impact_applicability + type: object + required: conditional + condition: "a behavior, cosmetic, or locality/shared/unresolved impact decision affects verification" + description: "Applicability." + validation: "not_applicable is cosmetic; local needs causal evidence; shared/unresolved/expanded_artifact_carried need artifact." + object_fields: + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: applicability_reason, type: string, required: true} + - {name: causal_evidence_ref, type: string, required: conditional, condition: "impact_scope == local"} + - {name: expanded_artifact_carried, type: boolean, required: true} + on_missing: fail + + - name: change_impact_context + type: object + required: false + description: "Refs." + validation: "Valid carries identity, refs, validator result; pre-build/completion assessment; completion resolves review projection." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: status, type: enum, required: true, enum_values: [valid, gaps_reported, blocked, invalid]} + - {name: phase, type: enum, required: conditional, condition: "status == valid", enum_values: [discovery, pre_build, completion]} + - {name: capture_ref, type: string, required: conditional, condition: "status == valid"} + - {name: expected_context_ref, type: string, required: conditional, condition: "status == valid"} + - {name: assessment_ref, type: string, required: conditional, condition: "status == valid and phase in [pre_build, completion]"} + - {name: review_projection_ref, type: string, required: conditional, condition: "status == valid and phase == completion"} + - {name: validator_result_ref, type: string, required: conditional, condition: "status == valid"} + - {name: gap_or_blocker_ref, type: string, required: conditional, condition: "status in [gaps_reported, blocked, invalid]"} + default: null + on_missing: infer + - name: tdd_mode type: boolean required: false diff --git a/skills/assistant-workflow/contracts/output.yaml b/skills/assistant-workflow/contracts/output.yaml index fb5a6141..6741f0e0 100644 --- a/skills/assistant-workflow/contracts/output.yaml +++ b/skills/assistant-workflow/contracts/output.yaml @@ -3,13 +3,13 @@ # Any agent executing this skill MUST verify all required artifacts exist # before printing --- WORKFLOW COMPLETE ---. -schema_version: "11.0" +schema_version: "11.1" contract: output skill: assistant-workflow # Completion evidence is tiered so small/low-risk work does not inherit the -# full medium+ harness. Artifact definitions below remain the source of truth; -# these tiers define which artifacts are required by default for each class. +# full medium+ harness. Artifact definitions remain source of truth; tiers list +# each class's default artifacts. # controller_intensity maps onto these tiers and gate_tiers: light -> small, # standard -> ordinary medium+, strict -> small_elevated for small work and # large_critical otherwise. @@ -17,20 +17,20 @@ completion_tiers: preparation_only: execution_intent: prepare_only required_artifacts: [completion_policy, validation_results, feature_preparation_result] - conditional_artifacts: [triage_result, phase_checkpoints, task_journal, feature_preparation_evidence, plan_document, requirement_acceptance_map, architecture_decision_pack, context_map, context_budget_note, context_handoff_packet] + conditional_artifacts: [triage_result, phase_checkpoints, task_journal, feature_preparation_evidence, plan_document, requirement_acceptance_map, architecture_decision_pack, context_map, context_budget_note, context_handoff_packet, change_impact_applicability, change_impact_evidence] review_gate: "Preparation-evidence admissibility audit only; no Build, changed-files, implementation test, code-review, or final-handoff claim is permitted." checkpoint_policy: "Returns validation_results plus the typed readiness artifact with Execution not started, exact scope, applicable evidence ref, gaps, open decisions, implications, and next step." small: controller_intensity: light required_artifacts: [completion_policy, changed_files, validation_results, fresh_review_result] - conditional_artifacts: [triage_result, subagent_evidence, build_repair_state, test_results, debugging_result, artifact_contract, task_state_reconciliation, requirement_acceptance_map, feature_preparation_evidence, architecture_decision_pack, review_result, qa_evaluation_result, final_handoff, plan_document, manual_test_steps, manual_verification_result, workflow_experiment_ledger, loop_readiness_assessment] - review_gate: "fresh review after relevant validation; full independent review only when risk or another trigger promotes the work" + conditional_artifacts: [triage_result, subagent_evidence, build_repair_state, test_results, debugging_result, artifact_contract, task_state_reconciliation, requirement_acceptance_map, feature_preparation_evidence, architecture_decision_pack, review_result, qa_evaluation_result, final_handoff, plan_document, manual_test_steps, manual_verification_result, workflow_experiment_ledger, loop_readiness_assessment, change_impact_applicability, change_impact_evidence] + review_gate: "fresh review after validation; Pack/expanded impact uses canonical assistant-review; full independent review only when risk promotes work" checkpoint_policy: "an inline scope/check summary is enough; include an inline plan only when plan_mode=inline. Exact phase markers are required only by strict intensity, explicit project policy, or user request" small_elevated: condition: "execution_intent != prepare_only and size == small and (controller_intensity == strict or qa_evaluation_mode == required)" controller_intensity: strict required_artifacts: [completion_policy, triage_result, phase_checkpoints, changed_files, validation_results, spec_review_result, review_result, final_handoff] - conditional_artifacts: [test_results, subagent_evidence, build_repair_state, debugging_result, requirement_acceptance_map, feature_preparation_evidence, architecture_decision_pack, qa_evaluation_result, plan_document, manual_test_steps, manual_verification_result] + conditional_artifacts: [test_results, subagent_evidence, build_repair_state, debugging_result, requirement_acceptance_map, feature_preparation_evidence, architecture_decision_pack, qa_evaluation_result, plan_document, manual_test_steps, manual_verification_result, change_impact_applicability, change_impact_evidence] prohibited_artifacts: [fresh_review_result, decomposition_plan_review, slice_manifest, single_slice_rationale, slice_verification_summary] review_gate: "typed canonical review and required QA terminal state; no medium+ slice evidence" checkpoint_policy: "strict phase checkpoints without decomposition artifacts" @@ -38,17 +38,49 @@ completion_tiers: controller_intensity: standard intensity_policy: "ordinary medium+ non-harness work keeps harness_capable=false and qa_evaluation_mode=not_required by default; standard does not require Done Contract, Harness Recipe, Trace Ledger, Replay Packet, Artifact Reference Ledger, or QA evaluation." required_artifacts: [completion_policy, triage_result, changed_files, validation_results, subagent_evidence, spec_review_result, review_result, requirement_acceptance_map, final_handoff] - conditional_artifacts: [test_results, build_repair_state, plan_document, task_state_reconciliation, task_journal, context_map, feature_preparation_evidence, architecture_decision_pack, slice_manifest, single_slice_rationale, slice_verification_summary, metrics_entry, manual_test_steps, manual_verification_result, candidate_search_result, workflow_experiment_ledger, loop_readiness_assessment, done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, artifact_reference_ledger, qa_evaluation_result] + conditional_artifacts: [test_results, build_repair_state, plan_document, task_state_reconciliation, task_journal, context_map, feature_preparation_evidence, architecture_decision_pack, slice_manifest, single_slice_rationale, slice_verification_summary, metrics_entry, manual_test_steps, manual_verification_result, candidate_search_result, workflow_experiment_ledger, loop_readiness_assessment, done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, artifact_reference_ledger, qa_evaluation_result, change_impact_applicability, change_impact_evidence] review_gate: "spec review plus code quality review, with independent QA evaluation when required and evidence matched to the approved plan" checkpoint_policy: "task journal or carried-forward state records phase transitions; visible updates use concise natural language unless exact markers were explicitly triggered" large_critical: controller_intensity: strict required_artifacts: [completion_policy, triage_result, phase_checkpoints, changed_files, test_results, validation_results, subagent_evidence, spec_review_result, review_result, requirement_acceptance_map, final_handoff, task_journal, context_map, context_budget_note, slice_manifest, slice_verification_summary] - conditional_artifacts: [debugging_result, build_repair_state, plan_document, artifact_contract, task_state_reconciliation, pattern_retrieval_note, decomposition_plan_review, feature_preparation_evidence, architecture_decision_pack, single_slice_rationale, metrics_entry, manual_test_steps, manual_verification_result, candidate_search_result, workflow_experiment_ledger, loop_readiness_assessment, done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, artifact_reference_ledger, qa_evaluation_result, verified_skill_distillation] + conditional_artifacts: [debugging_result, build_repair_state, plan_document, artifact_contract, task_state_reconciliation, pattern_retrieval_note, decomposition_plan_review, feature_preparation_evidence, architecture_decision_pack, single_slice_rationale, metrics_entry, manual_test_steps, manual_verification_result, candidate_search_result, workflow_experiment_ledger, loop_readiness_assessment, done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, artifact_reference_ledger, qa_evaluation_result, verified_skill_distillation, change_impact_applicability, change_impact_evidence] review_gate: "full strict evidence, security/operability review when applicable, QA evaluation when required, and all slice criteria verified" checkpoint_policy: "explicit phase checkpoints are required" artifacts: + - name: change_impact_applicability + type: object + required: conditional + condition: "a behavior, cosmetic, or locality/shared/unresolved impact decision affects verification" + description: "Applicability." + validation: "not_applicable=cosmetic; local causal_evidence_ref/no checker; otherwise expanded_artifact_carried=true." + on_fail: "scope/reason; local causal; shared/unresolved/expanded change_impact_evidence or blocked." + object_fields: + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: applicability_reason, type: string, required: true} + - {name: causal_evidence_ref, type: string, required: conditional, condition: "impact_scope == local"} + - {name: expanded_artifact_carried, type: boolean, required: true} + + - name: change_impact_evidence + type: object + required: conditional + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + description: "Expanded evidence." + validation: "Keep artifact_identity across phases. Gaps/blocks cannot claim verification/review. pre_build needs valid assessment, no material unknown. completion needs verification and assistant-review projection binding original scope_manifest, coverage_ledger, current snapshot; terminal checks remain." + on_fail: "Run checker or report blocked; no manual pass." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: phase, type: enum, required: true, enum_values: [discovery, pre_build, completion]} + - {name: impact_scope, type: enum, required: true, enum_values: [not_applicable, local, shared, unresolved]} + - {name: validator_result_ref, type: string, required: conditional, condition: "status == valid"} + - {name: status, type: enum, required: true, enum_values: [valid, gaps_reported, blocked, invalid]} + - {name: capture_ref, type: string, required: conditional, condition: "status == valid"} + - {name: expected_context_ref, type: string, required: conditional, condition: "status == valid"} + - {name: assessment_ref, type: string, required: conditional, condition: "status == valid and phase in [pre_build, completion]"} + - {name: review_projection_ref, type: string, required: conditional, condition: "status == valid and phase == completion"} + - {name: gap_or_blocker_ref, type: string, required: conditional, condition: "status in [gaps_reported, blocked, invalid]"} + - name: triage_result type: object required: conditional @@ -392,8 +424,8 @@ artifacts: required: conditional condition: "execution_intent != prepare_only and controller_intensity == light" description: "Compact review performed after relevant validation with fresh attention to carried Discover scope/criteria, any applicable inline plan, changed files, and acceptance criteria." - validation: "Result is PASS, reviewed_scope names the changed surface, findings are empty or resolved, and evidence identifies the diff/validation inspected. When an Architecture Decision Pack applies, canonical_result_ref, producer_schema_version, final_review_snapshot_id, final_snapshot_identity_ref, final_snapshot_identity, delegation_path_ref, and architecture_decision_pack_review_ref resolve to validated canonical assistant-review outputs; the alias and identity bind the same final_summary and exact current final batch." - on_fail: "Run a fresh post-validation review, fix findings, rerun relevant validation, and record PASS evidence before completing light work. For an applicable Pack, bind final_snapshot_identity_ref to the validated final_summary's exact current final-batch identity and delegation_path_ref to its validated review_delegation_path before marking validation_status=validated." + validation: "Result is PASS, reviewed_scope names the changed surface, findings are empty or resolved, and evidence identifies inspected validation. When a Pack or expanded impact applies, canonical refs resolve to one validated assistant-review final_summary and exact current final batch; Pack refs remain Pack-only." + on_fail: "Run fresh post-validation review, fix findings, rerun validation, and record PASS evidence. For a Pack or expanded impact, bind final_snapshot_identity_ref and delegation_path_ref to the canonical final_summary before validation_status=validated." object_fields: - name: result type: enum @@ -412,34 +444,34 @@ artifacts: - name: canonical_result_ref type: string required: conditional - condition: "architecture_design_mode in [lightweight, required, review_intensive]" - description: "Stable ref to the validated assistant-review final_summary for the light Pack review" + condition: "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + description: "Stable ref to the validated assistant-review final_summary" validation: "Resolves to a validated canonical assistant-review final_summary" - name: canonical_contract type: string required: conditional - condition: "architecture_design_mode in [lightweight, required, review_intensive]" + condition: "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" validation: "Must equal assistant-review/contracts/output.yaml#final_summary" - name: producer_schema_version type: string required: conditional - condition: "architecture_design_mode in [lightweight, required, review_intensive]" + condition: "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" validation: "Equals skills/assistant-review/contracts/index.yaml#schema_version at validation time." - name: final_review_snapshot_id type: string required: conditional - condition: "architecture_design_mode in [lightweight, required, review_intensive]" + condition: "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" validation: "Equals canonical_result_ref.final_review_snapshot_id and the current final batch review_material_snapshot.review_snapshot_id, and identifies the batch bound by final_snapshot_identity." - name: final_snapshot_identity_ref type: string required: conditional - condition: "architecture_design_mode in [lightweight, required, review_intensive]" + condition: "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" description: "Stable ref to canonical_result_ref.final_snapshot_identity" validation: "Resolves within the same validated assistant-review final_summary to final_snapshot_identity, which equals the current final batch review_material_snapshot.snapshot_identity; final_review_snapshot_id exactly equals the current final batch review_material_snapshot.review_snapshot_id." - name: final_snapshot_identity type: object required: conditional - condition: "architecture_design_mode in [lightweight, required, review_intensive]" + condition: "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" description: "Exact identity projection used to reject stale or foreign light-review bindings" validation: "Equals canonical_result_ref.final_snapshot_identity and the current final batch review_material_snapshot.snapshot_identity; final_review_snapshot_id exactly identifies this identity and equals the current final batch review_material_snapshot.review_snapshot_id." object_fields: @@ -459,13 +491,13 @@ artifacts: - name: delegation_path_ref type: string required: conditional - condition: "architecture_design_mode in [lightweight, required, review_intensive]" - description: "Stable ref to the validated assistant-review review_delegation_path for the light Pack review" + condition: "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + description: "Stable ref to the validated assistant-review review_delegation_path" validation: "Resolves to the review_delegation_path produced with the canonical final_summary" - name: delegation_contract type: string required: conditional - condition: "architecture_design_mode in [lightweight, required, review_intensive]" + condition: "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" validation: "Must equal assistant-review/contracts/output.yaml#review_delegation_path" - name: architecture_decision_pack_review_ref type: string @@ -481,7 +513,7 @@ artifacts: - name: validation_status type: enum required: conditional - condition: "architecture_design_mode in [lightweight, required, review_intensive]" + condition: "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" enum_values: [validated] - name: review_result @@ -1862,7 +1894,7 @@ artifacts: - name: artifact_type type: enum required: true - enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, test_result, review_result, qa_evaluation_result] + enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, change_impact_evidence, test_result, review_result, qa_evaluation_result] validation: "Classifies the artifact without requiring prose parsing" on_missing: fail - name: producer diff --git a/skills/assistant-workflow/contracts/phase-gates.yaml b/skills/assistant-workflow/contracts/phase-gates.yaml index edf88e8d..c2db74b1 100644 --- a/skills/assistant-workflow/contracts/phase-gates.yaml +++ b/skills/assistant-workflow/contracts/phase-gates.yaml @@ -16,7 +16,7 @@ # work, but they do not by themselves block progress unless project policy opts # into stricter enforcement. -schema_version: "11.0" +schema_version: "11.1" contract: phase-gates skill: assistant-workflow @@ -455,6 +455,16 @@ gates: checkpoint_start: "--- PHASE: BUILD ---" checkpoint_end: "--- PHASE: BUILD COMPLETE ---" condition: "execution_intent != prepare_only" + entry_assertions: + + - id: B_CHANGE_IMPACT_APPLICABILITY + check: "Before any source/test mutation or mutation dispatch, record change_impact_applicability with impact_scope and applicability_reason; local behavior requires causal_evidence_ref. Shared/unresolved or explicitly carried expanded work sets expanded_artifact_carried=true. Cosmetic work records not_applicable." + on_fail: "Stop before edits or mutation dispatch; load references/change-impact.md and record valid applicability before selecting the local or expanded path." + + - id: B_CHANGE_IMPACT_PREBUILD + check: "Before any source/test mutation or mutation dispatch, shared/unresolved/expanded work runs or revalidates the common checker against referenced capture, independent expected context, and assessment for a current valid pre_build result resolving validator refs. Local uses ordinary verification/self-review; no Node gate." + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + on_fail: "Stop before behavior edits, regression-test writes, or mutation dispatch; load references/change-impact.md, resolve the active installed checker, and repair the assessment or report the explicit blocker." exit_assertions: - id: B1 @@ -594,9 +604,9 @@ gates: on_fail: "Clear mismatched refs, record task_state_reconciliation.assistant_review_packet_compatibility=invalidated, and rerun review, QA, or both under the current assistant-review contract." - id: R_LIGHT_FRESH_REVIEW - check: "Light work has a fresh post-validation review result with PASS, reviewed scope, resolved findings, and evidence; when an Architecture Decision Pack applies, fresh_review_result records validated canonical assistant-review final_summary, exact current final-batch final_snapshot_identity ref and object, review_delegation_path, and architecture_decision_pack_review refs" + check: "Light work has a fresh post-validation review result with PASS, reviewed scope, resolved findings, and evidence; when a Pack or expanded impact applies, fresh_review_result records validated canonical assistant-review final_summary, exact current final-batch final_snapshot_identity ref/object, and review_delegation_path; Pack refs remain Pack-only" condition: "controller_intensity == light" - on_fail: "Run the compact fresh-review lane, fix findings, rerun relevant validation, and record PASS evidence. For an applicable Pack, validate and record its canonical assistant-review final_summary, exact current final-batch final_snapshot_identity, review_delegation_path, and architecture_decision_pack_review refs." + on_fail: "Run the compact fresh-review lane, fix findings, rerun validation, and record PASS evidence. For a Pack or expanded impact, validate and record canonical final_summary, exact current final-batch final_snapshot_identity, and review_delegation_path; retain Pack refs when applicable." - id: R2A_CODE_REVIEWER_DISTINCT check: "After Build completes, Review owns and records independent Code Reviewer dispatch/result evidence, or fresh direct-fallback review evidence, in subagent_evidence plus validated canonical assistant-review result and final_snapshot_identity refs in review_result. Code Reviewer or Reviewer compatibility evidence is recorded separately from QA Evaluator evidence." @@ -812,6 +822,12 @@ invariants: scope: all_phases on_fail: "Repair the Type Ledger and affected interface/task packet before implementation; do not hide domain meaning in unexplained generic primitives or introduce wrapper-only types without an owned semantic boundary." + - id: INV_CHANGE_IMPACT + check: "Behavior changes retain applicability. Expanded artifacts preserve identity through every phase. execution_intent == prepare_only retains discovery applicability and artifact identity without completion/review claims. execution_intent != prepare_only requires common completion validation and assistant-review terminal coverage." + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + scope: all_phases + on_fail: "Refresh the bounded capture and expected authority, rerun affected verification/review, or retain the result as blocked/remaining rather than complete." + - id: INV_PROGRESSIVE_DISCOVERY_BOUNDARY check: "Progressive mapping, resolving, route_clear, or blocked state remains in Discover and is a no-execution boundary: no Decompose, Plan, Build, project/source mutation, external writes, branch creation, or credential-location recording occurs. The only permitted local state update is the framework-owned journal/equivalent carried-state update required to record progressive state, including a route-clear handoff or its consumption; it never authorizes project/source mutation or an external write." condition: "uncertainty_shape == progressive and progressive_discovery_state in [mapping, resolving, route_clear, blocked]" diff --git a/skills/assistant-workflow/evals/cases.json b/skills/assistant-workflow/evals/cases.json index 38b961ac..d8410bcc 100644 --- a/skills/assistant-workflow/evals/cases.json +++ b/skills/assistant-workflow/evals/cases.json @@ -2576,8 +2576,8 @@ {"operator":"equals","path":["canonical_final_summary","ref"],"expected":"journal#final-summary"}, {"operator":"equals","path":["review_result","canonical_contract"],"expected":"assistant-review/contracts/output.yaml#final_summary"}, {"operator":"equals_path","path":["review_result","canonical_contract"],"other_path":["canonical_final_summary","contract"]}, - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, - {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, + {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals_path","path":["review_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, {"operator":"equals_path","path":["review_result","final_review_snapshot_id"],"other_path":["canonical_final_summary","artifact","final_review_snapshot_id"]}, {"operator":"equals_path","path":["canonical_final_summary","artifact","final_review_snapshot_id"],"other_path":["current_final_batch","review_snapshot_id"]}, @@ -2667,8 +2667,8 @@ {"operator":"equals","path":["canonical_final_summary","ref"],"expected":"journal#final-summary"}, {"operator":"equals","path":["fresh_review_result","canonical_contract"],"expected":"assistant-review/contracts/output.yaml#final_summary"}, {"operator":"equals_path","path":["fresh_review_result","canonical_contract"],"other_path":["canonical_final_summary","contract"]}, - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, - {"operator":"equals","path":["fresh_review_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, + {"operator":"equals","path":["fresh_review_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals_path","path":["fresh_review_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, {"operator":"equals_path","path":["fresh_review_result","final_review_snapshot_id"],"other_path":["canonical_final_summary","artifact","final_review_snapshot_id"]}, {"operator":"equals_path","path":["canonical_final_summary","artifact","final_review_snapshot_id"],"other_path":["current_final_batch","review_snapshot_id"]}, @@ -2751,8 +2751,8 @@ {"operator":"equals","path":["canonical_final_summary","ref"],"expected":"journal#final-summary"}, {"operator":"equals","path":["review_result","canonical_contract"],"expected":"assistant-review/contracts/output.yaml#final_summary"}, {"operator":"equals_path","path":["review_result","canonical_contract"],"other_path":["canonical_final_summary","contract"]}, - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, - {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, + {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals_path","path":["review_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, {"operator":"equals_path","path":["review_result","final_review_snapshot_id"],"other_path":["canonical_final_summary","artifact","final_review_snapshot_id"]}, {"operator":"equals_path","path":["canonical_final_summary","artifact","final_review_snapshot_id"],"other_path":["current_final_batch","review_snapshot_id"]}, @@ -2849,8 +2849,8 @@ {"operator":"equals","path":["canonical_final_summary","ref"],"expected":"journal#final-summary"}, {"operator":"equals","path":["review_result","canonical_contract"],"expected":"assistant-review/contracts/output.yaml#final_summary"}, {"operator":"equals_path","path":["review_result","canonical_contract"],"other_path":["canonical_final_summary","contract"]}, - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, - {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, + {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals_path","path":["review_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, {"operator":"equals_path","path":["review_result","final_review_snapshot_id"],"other_path":["canonical_final_summary","artifact","final_review_snapshot_id"]}, {"operator":"equals_path","path":["canonical_final_summary","artifact","final_review_snapshot_id"],"other_path":["current_final_batch","review_snapshot_id"]}, @@ -2913,16 +2913,16 @@ {"operator":"path_absent","path":["canonical_final_summary","artifact","final_snapshot_identity_ref"]}, {"operator":"path_absent","path":["canonical_final_summary","artifact","approved_feature_preparation_qa_acceptance_obligation_result_ref"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","rounds"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, {"operator":"nonempty_array","path":["canonical_qa_result","artifact","score_progression"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","score_progression",0,"delta"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"drift_status"],"expected_values":["GENUINE","SUSPICIOUS","DRIFT","REGRESSION","STAGNATION","NEUTRAL","NOT_APPLICABLE"]}, {"operator":"array_items_nonempty_fields","path":["canonical_qa_result","artifact","evidence"],"fields":["source","detail"]}, @@ -2943,7 +2943,7 @@ {"operator":"equals_path","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"other_path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"]}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected":1}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected":0}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","acceptance_coverage"]}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","evidence_strength"]}, {"operator":"equals","path":["canonical_qa_result","artifact","qa_scorecard","rationale","domain_quality"],"expected":"not_applicable: no scoped domain rubric applies."}, @@ -2999,8 +2999,8 @@ {"operator":"equals","path":["canonical_final_summary","ref"],"expected":"journal#final-summary"}, {"operator":"equals","path":["review_result","canonical_contract"],"expected":"assistant-review/contracts/output.yaml#final_summary"}, {"operator":"equals_path","path":["review_result","canonical_contract"],"other_path":["canonical_final_summary","contract"]}, - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, - {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, + {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals_path","path":["review_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, {"operator":"equals_path","path":["review_result","final_review_snapshot_id"],"other_path":["canonical_final_summary","artifact","final_review_snapshot_id"]}, {"operator":"equals_path","path":["canonical_final_summary","artifact","final_review_snapshot_id"],"other_path":["current_final_batch","review_snapshot_id"]}, @@ -3061,16 +3061,16 @@ {"operator":"path_absent","path":["canonical_final_summary","artifact","final_snapshot_identity_ref"]}, {"operator":"path_absent","path":["canonical_final_summary","artifact","approved_feature_preparation_qa_acceptance_obligation_result_ref"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","rounds"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, {"operator":"nonempty_array","path":["canonical_qa_result","artifact","score_progression"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","score_progression",0,"delta"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"drift_status"],"expected_values":["GENUINE","SUSPICIOUS","DRIFT","REGRESSION","STAGNATION","NEUTRAL","NOT_APPLICABLE"]}, {"operator":"array_items_nonempty_fields","path":["canonical_qa_result","artifact","evidence"],"fields":["source","detail"]}, @@ -3090,7 +3090,7 @@ {"operator":"equals_path","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"other_path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"]}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected":1}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected":1}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","acceptance_coverage"]}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","evidence_strength"]}, {"operator":"equals","path":["canonical_qa_result","artifact","qa_scorecard","rationale","domain_quality"],"expected":"not_applicable: no scoped domain rubric applies."}, @@ -4906,8 +4906,8 @@ {"operator":"equals_path","path":["review_result","canonical_result_ref"],"other_path":["canonical_final_summary","ref"]}, {"operator":"nonempty_string","path":["review_result","canonical_result_ref"]}, {"operator":"equals","path":["review_result","canonical_contract"],"expected":"assistant-review/contracts/output.yaml#final_summary"}, - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, - {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, + {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals_path","path":["review_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, {"operator":"equals_path","path":["review_result","final_review_snapshot_id"],"other_path":["canonical_final_summary","artifact","final_review_snapshot_id"]}, {"operator":"equals_path","path":["canonical_final_summary","artifact","final_review_snapshot_id"],"other_path":["current_final_batch","review_snapshot_id"]}, @@ -4929,7 +4929,7 @@ {"operator":"equals","path":["review_result","delegation_contract"],"expected":"assistant-review/contracts/output.yaml#review_delegation_path"}, {"operator":"equals","path":["review_result","validation_status"],"expected":"validated"}, {"operator":"nonempty_string","path":["qa_evaluation_result","delegation_path_ref"]}, - {"operator":"equals","path":["qa_evaluation_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["qa_evaluation_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals","path":["qa_evaluation_result","delegation_contract"],"expected":"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path"}, {"operator":"equals","path":["qa_evaluation_result","validation_status"],"expected":"validated"}, {"operator":"equals_path","path":["qa_evaluation_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, @@ -4986,16 +4986,16 @@ {"operator":"path_absent","path":["canonical_final_summary","artifact","final_snapshot_identity_ref"]}, {"operator":"path_absent","path":["canonical_final_summary","artifact","approved_feature_preparation_qa_acceptance_obligation_result_ref"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","rounds"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, {"operator":"nonempty_array","path":["canonical_qa_result","artifact","score_progression"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","score_progression",0,"delta"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"drift_status"],"expected_values":["GENUINE","SUSPICIOUS","DRIFT","REGRESSION","STAGNATION","NEUTRAL","NOT_APPLICABLE"]}, {"operator":"array_items_nonempty_fields","path":["canonical_qa_result","artifact","evidence"],"fields":["source","detail"]}, @@ -5018,7 +5018,7 @@ {"operator":"equals_path","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"other_path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"]}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected":1}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected":0}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","acceptance_coverage"]}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","evidence_strength"]}, {"operator":"equals","path":["canonical_qa_result","artifact","qa_scorecard","rationale","domain_quality"],"expected":"not_applicable: no scoped domain rubric applies."}, @@ -5131,8 +5131,8 @@ {"operator":"equals_path","path":["review_result","canonical_result_ref"],"other_path":["canonical_final_summary","ref"]}, {"operator":"nonempty_string","path":["review_result","canonical_result_ref"]}, {"operator":"equals","path":["review_result","canonical_contract"],"expected":"assistant-review/contracts/output.yaml#final_summary"}, - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, - {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, + {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals_path","path":["review_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, {"operator":"equals_path","path":["review_result","final_review_snapshot_id"],"other_path":["canonical_final_summary","artifact","final_review_snapshot_id"]}, {"operator":"equals_path","path":["canonical_final_summary","artifact","final_review_snapshot_id"],"other_path":["current_final_batch","review_snapshot_id"]}, @@ -5154,7 +5154,7 @@ {"operator":"equals","path":["review_result","delegation_contract"],"expected":"assistant-review/contracts/output.yaml#review_delegation_path"}, {"operator":"equals","path":["review_result","validation_status"],"expected":"validated"}, {"operator":"nonempty_string","path":["qa_evaluation_result","delegation_path_ref"]}, - {"operator":"equals","path":["qa_evaluation_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["qa_evaluation_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals","path":["qa_evaluation_result","delegation_contract"],"expected":"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path"}, {"operator":"equals","path":["qa_evaluation_result","validation_status"],"expected":"validated"}, {"operator":"equals_path","path":["qa_evaluation_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, @@ -5209,16 +5209,16 @@ {"operator":"path_absent","path":["canonical_final_summary","artifact","final_snapshot_identity_ref"]}, {"operator":"path_absent","path":["canonical_final_summary","artifact","approved_feature_preparation_qa_acceptance_obligation_result_ref"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","rounds"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, {"operator":"nonempty_array","path":["canonical_qa_result","artifact","score_progression"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","score_progression",0,"delta"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"drift_status"],"expected_values":["GENUINE","SUSPICIOUS","DRIFT","REGRESSION","STAGNATION","NEUTRAL","NOT_APPLICABLE"]}, {"operator":"array_items_nonempty_fields","path":["canonical_qa_result","artifact","evidence"],"fields":["source","detail"]}, @@ -5241,7 +5241,7 @@ {"operator":"equals_path","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"other_path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"]}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected":1}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected":0}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","acceptance_coverage"]}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","evidence_strength"]}, {"operator":"equals","path":["canonical_qa_result","artifact","qa_scorecard","rationale","domain_quality"],"expected":"not_applicable: no scoped domain rubric applies."}, @@ -5337,7 +5337,7 @@ "required_substrings": ["rejection_recovery", "changed", "build_validation_ref", "rehash_evidence_ref", "fresh_review_result_ref"], "forbidden_substrings": ["reuse stale review"], "structured_json_assertions": [ - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","final_verdict"],"expected":"rejected"}, {"operator":"nonempty_string","path":["prior_canonical_qa_result","ref"]}, {"operator":"equals","path":["prior_canonical_qa_result","contract"],"expected":"assistant-review/contracts/output.yaml#qa_evaluation_result"}, @@ -5365,7 +5365,7 @@ {"operator":"equals","path":["fresh_canonical_final_summary","artifact","coverage_complete"],"expected":true}, {"operator":"nonempty_string","path":["qa_evaluation_result","canonical_result_ref"]}, {"operator":"equals","path":["qa_evaluation_result","canonical_contract"],"expected":"assistant-review/contracts/output.yaml#qa_evaluation_result"}, - {"operator":"equals","path":["qa_evaluation_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["qa_evaluation_result","producer_schema_version"],"expected":"7.2"}, {"operator":"nonempty_string","path":["qa_evaluation_result","delegation_path_ref"]}, {"operator":"equals","path":["qa_evaluation_result","delegation_contract"],"expected":"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path"}, {"operator":"equals","path":["qa_evaluation_result","validation_status"],"expected":"validated"}, @@ -5384,16 +5384,16 @@ {"operator":"path_absent","path":["fresh_canonical_final_summary","artifact","final_snapshot_identity_ref"]}, {"operator":"path_absent","path":["fresh_canonical_final_summary","artifact","approved_feature_preparation_qa_acceptance_obligation_result_ref"]}, {"operator":"one_of","path":["prior_canonical_qa_result","artifact","rounds"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, {"operator":"nonempty_array","path":["prior_canonical_qa_result","artifact","score_progression"]}, {"operator":"one_of","path":["prior_canonical_qa_result","artifact","score_progression",0,"round"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, {"operator":"nonempty_string","path":["prior_canonical_qa_result","artifact","score_progression",0,"delta"]}, {"operator":"one_of","path":["prior_canonical_qa_result","artifact","score_progression",0,"drift_status"],"expected_values":["GENUINE","SUSPICIOUS","DRIFT","REGRESSION","STAGNATION","NEUTRAL","NOT_APPLICABLE"]}, {"operator":"array_items_nonempty_fields","path":["prior_canonical_qa_result","artifact","evidence"],"fields":["source","detail"]}, @@ -5415,7 +5415,7 @@ {"operator":"equals_path","path":["prior_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"other_path":["prior_canonical_qa_result","artifact","qa_scorecard","weighted_score"]}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","score_progression",0,"round"],"expected":1}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected":1}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, {"operator":"nonempty_string","path":["prior_canonical_qa_result","artifact","qa_scorecard","rationale","acceptance_coverage"]}, {"operator":"nonempty_string","path":["prior_canonical_qa_result","artifact","qa_scorecard","rationale","evidence_strength"]}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","qa_scorecard","rationale","domain_quality"],"expected":"not_applicable: no scoped domain rubric applies."}, @@ -5427,13 +5427,13 @@ {"operator":"equals","path":["prior_canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected":3.1}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected":3.1}, {"operator":"equals","path":["current_canonical_qa_result","artifact","rounds"],"expected":1}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, {"operator":"nonempty_string","path":["current_canonical_qa_result","artifact","qa_scorecard","rationale","acceptance_coverage"]}, {"operator":"nonempty_string","path":["current_canonical_qa_result","artifact","qa_scorecard","rationale","evidence_strength"]}, {"operator":"equals","path":["current_canonical_qa_result","artifact","qa_scorecard","rationale","domain_quality"],"expected":"not_applicable: no scoped domain rubric applies."}, @@ -5449,8 +5449,8 @@ {"operator":"equals","path":["current_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected":5}, {"operator":"equals","path":["current_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected":0}, {"operator":"one_of","path":["current_canonical_qa_result","artifact","score_progression",0,"round"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, {"operator":"nonempty_string","path":["current_canonical_qa_result","artifact","score_progression",0,"delta"]}, {"operator":"one_of","path":["current_canonical_qa_result","artifact","score_progression",0,"drift_status"],"expected_values":["GENUINE","SUSPICIOUS","DRIFT","REGRESSION","STAGNATION","NEUTRAL","NOT_APPLICABLE"]}, {"operator":"array_field_values_exact","path":["current_canonical_qa_result","artifact","evidence"],"field":"source","expected_values":["focused lifecycle validation"]}, @@ -5506,7 +5506,7 @@ "required_substrings": ["rejection_recovery", "equal", "digest_equality_evidence_ref", "rehash_evidence_ref", "qa_resume_authorized"], "forbidden_substrings": ["build_validation_ref", "fresh_review_result_ref", "fresh_review_snapshot_identity", "fresh_review_coverage_complete"], "structured_json_assertions": [ - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","final_verdict"],"expected":"rejected"}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","result"],"expected":"HAS_REMAINING_ITEMS"}, {"operator":"nonempty_string","path":["prior_canonical_qa_result","ref"]}, @@ -5531,19 +5531,19 @@ {"operator":"path_absent","path":["fresh_canonical_final_summary"]}, {"operator":"path_absent","path":["current_final_batch"]}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","rounds"],"expected":1}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, {"operator":"array_field_values_exact","path":["prior_canonical_qa_result","artifact","score_progression"],"field":"drift_status","expected_values":["NEUTRAL"]}, {"operator":"equals_path","path":["prior_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"other_path":["prior_canonical_qa_result","artifact","qa_scorecard","weighted_score"]}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","score_progression",0,"round"],"expected":1}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected":1}, {"operator":"one_of","path":["prior_canonical_qa_result","artifact","score_progression",0,"round"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, {"operator":"nonempty_string","path":["prior_canonical_qa_result","artifact","score_progression",0,"delta"]}, {"operator":"one_of","path":["prior_canonical_qa_result","artifact","score_progression",0,"drift_status"],"expected_values":["GENUINE","SUSPICIOUS","DRIFT","REGRESSION","STAGNATION","NEUTRAL","NOT_APPLICABLE"]}, {"operator":"array_field_values_exact","path":["prior_canonical_qa_result","artifact","evidence"],"field":"source","expected_values":["focused lifecycle validation"]}, @@ -5552,7 +5552,7 @@ {"operator":"path_absent","path":["prior_canonical_qa_result","artifact","canonical_contract"]}, {"operator":"path_absent","path":["prior_canonical_qa_result","artifact","final_snapshot_identity_ref"]}, {"operator":"path_absent","path":["prior_canonical_qa_result","artifact","approved_feature_preparation_qa_acceptance_obligation_result_ref"]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["prior_canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["prior_canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, {"operator":"nonempty_string","path":["prior_canonical_qa_result","artifact","qa_scorecard","rationale","acceptance_coverage"]}, {"operator":"nonempty_string","path":["prior_canonical_qa_result","artifact","qa_scorecard","rationale","evidence_strength"]}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","qa_scorecard","rationale","domain_quality"],"expected":"not_applicable: no scoped domain rubric applies."}, @@ -5564,13 +5564,13 @@ {"operator":"equals","path":["prior_canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected":3.1}, {"operator":"equals","path":["prior_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected":3.1}, {"operator":"equals","path":["current_canonical_qa_result","artifact","rounds"],"expected":1}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, {"operator":"nonempty_string","path":["current_canonical_qa_result","artifact","qa_scorecard","rationale","acceptance_coverage"]}, {"operator":"nonempty_string","path":["current_canonical_qa_result","artifact","qa_scorecard","rationale","evidence_strength"]}, {"operator":"equals","path":["current_canonical_qa_result","artifact","qa_scorecard","rationale","domain_quality"],"expected":"not_applicable: no scoped domain rubric applies."}, @@ -5586,8 +5586,8 @@ {"operator":"equals","path":["current_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected":5}, {"operator":"equals","path":["current_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected":0}, {"operator":"one_of","path":["current_canonical_qa_result","artifact","score_progression",0,"round"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["current_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["current_canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, {"operator":"nonempty_string","path":["current_canonical_qa_result","artifact","score_progression",0,"delta"]}, {"operator":"one_of","path":["current_canonical_qa_result","artifact","score_progression",0,"drift_status"],"expected_values":["GENUINE","SUSPICIOUS","DRIFT","REGRESSION","STAGNATION","NEUTRAL","NOT_APPLICABLE"]}, {"operator":"array_field_values_exact","path":["current_canonical_qa_result","artifact","evidence"],"field":"source","expected_values":["focused lifecycle validation"]}, @@ -5648,8 +5648,8 @@ {"operator":"equals","path":["canonical_final_summary","artifact","evidence_bounded_claim"],"expected":"No material findings within the reviewed scope and available evidence"}, {"operator":"equals_path","path":["review_result","canonical_result_ref"],"other_path":["canonical_final_summary","ref"]}, {"operator":"nonempty_string","path":["review_result","canonical_result_ref"]}, - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, - {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, + {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals_path","path":["review_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, {"operator":"equals_path","path":["review_result","final_review_snapshot_id"],"other_path":["canonical_final_summary","artifact","final_review_snapshot_id"]}, {"operator":"equals_path","path":["canonical_final_summary","artifact","final_review_snapshot_id"],"other_path":["current_final_batch","review_snapshot_id"]}, @@ -5671,7 +5671,7 @@ {"operator":"equals","path":["review_result","validation_status"],"expected":"validated"}, {"operator":"nonempty_string","path":["qa_evaluation_result","delegation_path_ref"]}, {"operator":"equals","path":["qa_evaluation_result","canonical_contract"],"expected":"assistant-review/contracts/output.yaml#qa_evaluation_result"}, - {"operator":"equals","path":["qa_evaluation_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["qa_evaluation_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals","path":["qa_evaluation_result","delegation_contract"],"expected":"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path"}, {"operator":"equals","path":["qa_evaluation_result","validation_status"],"expected":"validated"}, {"operator":"equals_path","path":["qa_evaluation_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, @@ -5710,16 +5710,16 @@ {"operator":"path_absent","path":["canonical_final_summary","artifact","final_snapshot_identity_ref"]}, {"operator":"path_absent","path":["canonical_final_summary","artifact","approved_feature_preparation_qa_acceptance_obligation_result_ref"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","rounds"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, {"operator":"nonempty_array","path":["canonical_qa_result","artifact","score_progression"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","score_progression",0,"delta"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"drift_status"],"expected_values":["GENUINE","SUSPICIOUS","DRIFT","REGRESSION","STAGNATION","NEUTRAL","NOT_APPLICABLE"]}, {"operator":"array_items_nonempty_fields","path":["canonical_qa_result","artifact","evidence"],"fields":["source","detail"]}, @@ -5742,7 +5742,7 @@ {"operator":"equals_path","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"other_path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"]}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected":1}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected":0}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","acceptance_coverage"]}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","evidence_strength"]}, {"operator":"equals","path":["canonical_qa_result","artifact","qa_scorecard","rationale","domain_quality"],"expected":"not_applicable: no scoped domain rubric applies."}, @@ -5856,8 +5856,8 @@ {"operator":"equals","path":["canonical_final_summary","artifact","evidence_bounded_claim"],"expected":"No material findings within the reviewed scope and available evidence"}, {"operator":"equals_path","path":["review_result","canonical_result_ref"],"other_path":["canonical_final_summary","ref"]}, {"operator":"nonempty_string","path":["review_result","canonical_result_ref"]}, - {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.1"}, - {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["current_assistant_review_contract","schema_version"],"expected":"7.2"}, + {"operator":"equals","path":["review_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals_path","path":["review_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, {"operator":"equals_path","path":["review_result","final_review_snapshot_id"],"other_path":["canonical_final_summary","artifact","final_review_snapshot_id"]}, {"operator":"equals_path","path":["canonical_final_summary","artifact","final_review_snapshot_id"],"other_path":["current_final_batch","review_snapshot_id"]}, @@ -5879,7 +5879,7 @@ {"operator":"equals","path":["review_result","validation_status"],"expected":"validated"}, {"operator":"nonempty_string","path":["qa_evaluation_result","delegation_path_ref"]}, {"operator":"equals","path":["qa_evaluation_result","canonical_contract"],"expected":"assistant-review/contracts/output.yaml#qa_evaluation_result"}, - {"operator":"equals","path":["qa_evaluation_result","producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["qa_evaluation_result","producer_schema_version"],"expected":"7.2"}, {"operator":"equals","path":["qa_evaluation_result","delegation_contract"],"expected":"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path"}, {"operator":"equals","path":["qa_evaluation_result","validation_status"],"expected":"validated"}, {"operator":"equals_path","path":["qa_evaluation_result","producer_schema_version"],"other_path":["current_assistant_review_contract","schema_version"]}, @@ -5918,16 +5918,16 @@ {"operator":"path_absent","path":["canonical_final_summary","artifact","final_snapshot_identity_ref"]}, {"operator":"path_absent","path":["canonical_final_summary","artifact","approved_feature_preparation_qa_acceptance_obligation_result_ref"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","rounds"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","acceptance_coverage"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","evidence_strength"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","domain_quality"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","final_readiness"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"],"expected_type":"number"}, {"operator":"nonempty_array","path":["canonical_qa_result","artifact","score_progression"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected_values":[1,2,3,4,5,6,7,8,9,10]}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"expected_type":"number"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected_type":"number"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","score_progression",0,"delta"]}, {"operator":"one_of","path":["canonical_qa_result","artifact","score_progression",0,"drift_status"],"expected_values":["GENUINE","SUSPICIOUS","DRIFT","REGRESSION","STAGNATION","NEUTRAL","NOT_APPLICABLE"]}, {"operator":"array_items_nonempty_fields","path":["canonical_qa_result","artifact","evidence"],"fields":["source","detail"]}, @@ -5949,7 +5949,7 @@ {"operator":"equals_path","path":["canonical_qa_result","artifact","score_progression",0,"weighted_score"],"other_path":["canonical_qa_result","artifact","qa_scorecard","weighted_score"]}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"round"],"expected":1}, {"operator":"equals","path":["canonical_qa_result","artifact","score_progression",0,"failed_acceptance_count"],"expected":1}, - {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.1","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, + {"operator":"required_when_equals","when_path":["current_assistant_review_contract","schema_version"],"value":"7.2","path":["canonical_qa_result","artifact","qa_scorecard","rationale"],"expected_type":"object"}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","acceptance_coverage"]}, {"operator":"nonempty_string","path":["canonical_qa_result","artifact","qa_scorecard","rationale","evidence_strength"]}, {"operator":"equals","path":["canonical_qa_result","artifact","qa_scorecard","rationale","domain_quality"],"expected":"not_applicable: no scoped domain rubric applies."}, @@ -6090,25 +6090,93 @@ "id": "stale-assistant-review-version-invalidates-persisted-results", "title": "Assistant-review producer version mismatch invalidates persisted refs", "category": "resume_integrity", - "purpose": "Prevents incompatible persisted 7.0 review/QA wrappers from resuming after the authoritative producer version moves to 7.1.", + "purpose": "Prevents incompatible persisted 7.1 review/QA wrappers from resuming after the authoritative producer version moves to 7.2.", "prompt": "Reconcile this persisted workflow after the assistant-review producer schema changed. Return one valid JSON object.", - "setup_context": ["Persisted review and QA wrappers record producer_schema_version=7.0.", "skills/assistant-review/contracts/index.yaml#schema_version is 7.1."], + "setup_context": ["Persisted review and QA wrappers record producer_schema_version=7.1.", "skills/assistant-review/contracts/index.yaml#schema_version is 7.2."], "expected_behavior": ["Classifies state stale, invalidates all affected refs, and selects rerun_review_and_qa."], "pass_criteria": ["No stale result, delegation, snapshot, Pack, or QA ref remains resumable."], - "fail_signals": ["Treats the incompatible 7.0 wrapper as current under 7.1."], + "fail_signals": ["Treats the incompatible 7.1 wrapper as current under 7.2."], "machine_expectations": { - "required_substrings": ["producer_schema_version", "7.0", "7.1", "invalidated", "rerun_review_and_qa"], + "required_substrings": ["producer_schema_version", "7.1", "7.2", "invalidated", "rerun_review_and_qa"], "forbidden_substrings": ["packet shapes are compatible by assumption"], "structured_json_assertions": [ {"operator":"equals","path":["task_state_reconciliation","classification"],"expected":"stale"}, - {"operator":"equals","path":["task_state_reconciliation","assistant_review_packet_compatibility","persisted_producer_schema_versions"],"expected":["7.0"]}, - {"operator":"equals","path":["task_state_reconciliation","assistant_review_packet_compatibility","current_producer_schema_version"],"expected":"7.1"}, + {"operator":"equals","path":["task_state_reconciliation","assistant_review_packet_compatibility","persisted_producer_schema_versions"],"expected":["7.1"]}, + {"operator":"equals","path":["task_state_reconciliation","assistant_review_packet_compatibility","current_producer_schema_version"],"expected":"7.2"}, {"operator":"equals","path":["task_state_reconciliation","assistant_review_packet_compatibility","status"],"expected":"invalidated"}, {"operator":"equals","path":["task_state_reconciliation","assistant_review_packet_compatibility","invalidated_refs"],"expected":["review_result.canonical_result_ref","review_result.delegation_path_ref","review_result.final_snapshot_identity_ref","review_result.architecture_decision_pack_review_ref","qa_evaluation_result.canonical_result_ref","qa_evaluation_result.delegation_path_ref","qa_evaluation_result.approved_feature_preparation_qa_acceptance_obligation_result_ref"]}, {"operator":"equals","path":["task_state_reconciliation","assistant_review_packet_compatibility","rebuild_route"],"expected":"rerun_review_and_qa"}, {"operator":"nonempty_string","path":["task_state_reconciliation","exact_next_action"]} ] } + }, + { + "id": "workflow-rejects-checkout-only-shared-impact", + "title": "Workflow blocks symptom-only locality before checkout repair", + "category": "impact_accounting", + "purpose": "Covers the accepted checkout omission response: known invoice, retry, and configuration consumers cannot be omitted by calling the reported checkout path local.", + "prompt": "Prepare a workflow bugfix for a checkout discount helper. Repository evidence names invoice export, retry recovery, and configuration validation as additional consumers. The request proposes a checkout-only test and calls the change local. Return a phase-correct workflow result before Build.", + "setup_context": ["No Architecture Decision Pack, strict lane, or QA trigger is independently evidenced.", "The common change-impact capture/expected authority is available but its proposed assessment omits the non-checkout consumers."], + "expected_behavior": ["Rejects the local assessment and checkout-only verification plan.", "Requires bounded shared accounting and pre-build obligations before behavior edits.", "Keeps architecture/controller/QA/delegation routing conditional rather than fanout-driven."], + "pass_criteria": ["Invoice, retry, and configuration consumers remain in the impact scope.", "The result does not claim Build, executed verification, review completion, or a forced Architecture Pack."], + "fail_signals": ["Treats checkout as the only consumer because it is the reported symptom.", "Claims a shared helper automatically requires strict mode, QA, or an Architecture Pack."], + "machine_expectations": { + "required_substrings": ["shared", "invoice", "retry", "configuration", "pre-build", "not automatically"], + "forbidden_substrings": ["checkout-only verification is enough", "local because checkout", "must create an Architecture Decision Pack"] + } + }, + { + "id": "prepare-only-shared-impact-retains-discovery-evidence", + "title": "Prepare-only shared impact retains discovery evidence", + "category": "change_impact_prepare_only", + "purpose": "Requires a shared prepare-only workflow to retain discovery artifact identity without claiming implementation, verification, or review completion.", + "prompt": "Prepare a medium existing-system navigation change that affects a shared helper. Do not implement it. Return one valid JSON workflow result.", + "setup_context": ["Current discovery change-impact evidence has artifact identity capture-prepare-only-current, validator_result_ref change-impact#discovery, capture_ref change-impact#capture, and expected_context_ref change-impact#expected.", "No implementation approval, pre-build assessment, executed verification, or review projection is available."], + "expected_behavior": ["Carries the current shared discovery evidence identity.", "Keeps execution intent prepare_only and execution status not_started.", "Does not claim a pre-build assessment, implementation verification, or terminal review."], + "pass_criteria": ["The discovery artifact has its canonical identity and evidence bindings.", "Implementation and review artifacts are absent."], + "fail_signals": ["Promotes discovery evidence to pre_build.", "Adds assessment or review projection claims.", "Starts Build or returns a terminal review result."], + "machine_expectations": { + "required_substrings": ["prepare_only", "change_impact_applicability", "change_impact_evidence", "discovery", "capture-prepare-only-current"], + "forbidden_substrings": ["Build started", "review complete"], + "structured_json_assertions": [ + {"operator":"equals","path":["execution_intent"],"expected":"prepare_only"}, + {"operator":"equals","path":["size"],"expected":"medium"}, + {"operator":"nonempty_string","path":["completion_policy","selection_reason"]}, + {"operator":"equals","path":["completion_policy","build_execution_lane"],"expected":"none"}, + {"operator":"equals","path":["completion_policy","plan_mode"],"expected":"none"}, + {"operator":"equals","path":["triage_result","execution_intent"],"expected":"prepare_only"}, + {"operator":"equals","path":["triage_result","build_execution_lane"],"expected":"none"}, + {"operator":"equals","path":["triage_result","plan_mode"],"expected":"none"}, + {"operator":"nonempty_array","path":["validation_results"]}, + {"operator":"nonempty_array","path":["context_budget_note","exact_pinned"]}, + {"operator":"nonempty_array","path":["requirement_acceptance_map","entries"]}, + {"operator":"nonempty_string","path":["feature_preparation_evidence","ref"]}, + {"operator":"equals","path":["feature_preparation_result","execution_status"],"expected":"not_started"}, + {"operator":"nonempty_array","path":["feature_preparation_result","open_decisions"]}, + {"operator":"nonempty_string","path":["feature_preparation_result","recommended_next_step"]}, + {"operator":"equals","path":["change_impact_applicability","impact_scope"],"expected":"shared"}, + {"operator":"nonempty_string","path":["change_impact_applicability","applicability_reason"]}, + {"operator":"equals","path":["change_impact_applicability","expanded_artifact_carried"],"expected":true}, + {"operator":"path_absent","path":["change_impact_applicability","causal_evidence_ref"]}, + {"operator":"equals","path":["change_impact_evidence","artifact_identity"],"expected":"capture-prepare-only-current"}, + {"operator":"equals","path":["change_impact_evidence","phase"],"expected":"discovery"}, + {"operator":"equals","path":["change_impact_evidence","impact_scope"],"expected":"shared"}, + {"operator":"equals","path":["change_impact_evidence","status"],"expected":"valid"}, + {"operator":"equals","path":["change_impact_evidence","validator_result_ref"],"expected":"change-impact#discovery"}, + {"operator":"equals","path":["change_impact_evidence","capture_ref"],"expected":"change-impact#capture"}, + {"operator":"equals","path":["change_impact_evidence","expected_context_ref"],"expected":"change-impact#expected"}, + {"operator":"path_absent","path":["change_impact_evidence","assessment_ref"]}, + {"operator":"path_absent","path":["change_impact_evidence","review_projection_ref"]}, + {"operator":"path_absent","path":["feature_preparation_result","future_qa_acceptance_obligation"]}, + {"operator":"path_absent","path":["feature_preparation_result","future_harness_obligation"]}, + {"operator":"path_absent","path":["changed_files"]}, + {"operator":"path_absent","path":["test_results"]}, + {"operator":"path_absent","path":["fresh_review_result"]}, + {"operator":"path_absent","path":["review_result"]}, + {"operator":"path_absent","path":["final_handoff"]}, + {"operator":"path_absent","path":["artifact_reference_ledger"]} + ] + } } ], "canonical_feature_preparation_result_expectations": { diff --git a/skills/assistant-workflow/references/build-worker-protocol.md b/skills/assistant-workflow/references/build-worker-protocol.md index 93230648..07610836 100644 --- a/skills/assistant-workflow/references/build-worker-protocol.md +++ b/skills/assistant-workflow/references/build-worker-protocol.md @@ -18,11 +18,14 @@ no public behavior, data, security, harness, or QA acceptance risk: - direct implementation with relevant automated validation/tests - a fresh self-review after validation -The light lane does not require Code Writer, Builder/Tester, Code Reviewer, or +Shared fanout alone does not promote this Build lane. The light lane does not require Code Writer, Builder/Tester, Code Reviewer, or Reviewer dispatch/direct-fallback evidence. Any security-sensitive, high-risk, harness-capable, required-QA, non-localized, or otherwise promoted work uses the standard/strict lane and its existing gates. +Expanded impact keeps this Build lane; Review records canonical +`assistant-review` final-summary and delegation-path refs. + Select `build_execution_lane` before dispatch: - `bounded_executor` is the default for ordinary medium standard-risk work. @@ -62,6 +65,20 @@ Do not pretend delegation happened. Record `subagent_policy_state`, reason, matching bounded/separated evidence, independent Code Reviewer evidence during Review, and QA Evaluator evidence when QA is required. +## Expanded-impact mutation authorization + +Before any source/test mutation, including a regression-test write, or any +mutation dispatch, run or revalidate the common checker against the referenced +capture, independent expected context, and assessment for shared, unresolved, +or explicitly expanded impact. The current valid `pre_build` result must resolve +against the task packet; a status or result ref alone is insufficient. Reuse is +allowed only for demonstrably current same-input evidence. + +After an earlier mutation, refresh this evidence before each subsequent +mutation dispatch. In particular, Builder/Tester must not reuse a Code Writer +pre-build result. The same rule applies to light work and direct fallback; +those paths may remain local without the expanded trigger. + ## Standard/Strict Phase Evidence Gates Before standard/strict Build can complete, the task journal Agent Dispatch Log diff --git a/skills/assistant-workflow/references/change-impact.md b/skills/assistant-workflow/references/change-impact.md new file mode 100644 index 00000000..72aeabb4 --- /dev/null +++ b/skills/assistant-workflow/references/change-impact.md @@ -0,0 +1,31 @@ +# Change-impact gates + +Use when behavior, cosmetic, or a locality claim affects verification scope. Cosmetic +work records applicability without an impact ledger. + +Resolve the installed common checker as +`../../tools/change-impact/validate-change-impact.cjs`, never from cwd or a +repository `.codex/tools` copy. Run Node 22+ with explicit file arguments. +Missing Node or checker blocks only the expanded shared/material deterministic +gate; it is not a manual-pass exception. + +Every behavior change records scope, applicability reason, and causal local +evidence. Local work keeps its ordinary +relevant verification and self-review; it does not require Node or a common +artifact. Discovery records the bounded base/candidate universe, unknown +material boundaries, and the same `artifact_identity` only for shared, +materially unresolved, or explicitly carried expanded work. Before any +source/test mutation or dispatch, run/revalidate the checker using capture, +expected context, and assessment. `pre_build` validates task packet. Reuse runs +`--receipt validator_result_ref`; receipt matches parsed inputs. +The expanded path covers relevant configuration, +registration, event, state, public, wrapper, added, and removed consumers and +transitions. + +`prepare_only` keeps applicability and artifact identity without completion/review +claims. Otherwise completion refreshes universe/context, +binds verification identity and assistant-review's original `scope_manifest`, +`coverage_ledger`, review snapshot, and concern bindings, and preserves original +ledgers and terminal coverage/CLEAN/ISSUES_FIXED checks. Shared fanout alone does +not require Pack, strict, QA, or a different Build lane; existing triggers apply. +Light expanded work records assistant-review final-summary and delegation-path refs. diff --git a/skills/assistant-workflow/references/phases.md b/skills/assistant-workflow/references/phases.md index 4bac8465..cf328c9c 100644 --- a/skills/assistant-workflow/references/phases.md +++ b/skills/assistant-workflow/references/phases.md @@ -10,6 +10,16 @@ the task needs shared routing/default, movement, harness, review, QA, or subagent separation decisions. This file owns phase execution mechanics; the controller reference owns decision boundaries and ordinary workflow defaults. +For a behavior-bearing, cosmetic, or locality/shared-impact claim, load +`references/change-impact.md` before deciding the verification surface and record +compact applicability evidence. Only shared, materially unresolved, or +explicitly carried expanded work requires the common pre-build result and its +completion projection when `execution_intent != prepare_only`; prepare-only +work retains discovery applicability and artifact identity without implementation +completion claims. Evidenced local work keeps ordinary relevant verification and +self-review without a Node gate. Shared fanout alone does not activate an +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit @@ -388,7 +398,7 @@ Print: `--- PHASE: REVIEW ---` Load `references/review-qa-router.md`. Reused validation evidence never replaces Spec Review, independent code review, or a current final review snapshot. Light work uses its compact fresh-review lane as a fresh self-review without worker or independent-review dispatch -evidence. Standard/strict work uses Stage 1 Spec Review and Stage 2 independent +evidence. Expanded impact instead records canonical `assistant-review` final-summary and delegation-path refs, without changing light Build. Standard/strict work uses Stage 1 Spec Review and Stage 2 independent Code Quality Review through `assistant-review`; Stage 3 QA Evaluation runs only when `qa_evaluation_mode=required`. diff --git a/skills/assistant-workflow/references/phases/build.md b/skills/assistant-workflow/references/phases/build.md index a9a69ad7..77967fc3 100644 --- a/skills/assistant-workflow/references/phases/build.md +++ b/skills/assistant-workflow/references/phases/build.md @@ -12,6 +12,16 @@ the task needs shared routing/default, movement, harness, review, QA, or subagent separation decisions. This file owns phase execution mechanics; the controller reference owns decision boundaries and ordinary workflow defaults. +For a behavior-bearing, cosmetic, or locality/shared-impact claim, load +`references/change-impact.md` before deciding the verification surface and record +compact applicability evidence. Only shared, materially unresolved, or +explicitly carried expanded work requires the common pre-build result and its +completion projection when `execution_intent != prepare_only`; prepare-only +work retains discovery applicability and artifact identity without implementation +completion claims. Evidenced local work keeps ordinary relevant verification and +self-review without a Node gate. Shared fanout alone does not activate an +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit diff --git a/skills/assistant-workflow/references/phases/decompose.md b/skills/assistant-workflow/references/phases/decompose.md index fd688030..a6c6c355 100644 --- a/skills/assistant-workflow/references/phases/decompose.md +++ b/skills/assistant-workflow/references/phases/decompose.md @@ -12,6 +12,16 @@ the task needs shared routing/default, movement, harness, review, QA, or subagent separation decisions. This file owns phase execution mechanics; the controller reference owns decision boundaries and ordinary workflow defaults. +For a behavior-bearing, cosmetic, or locality/shared-impact claim, load +`references/change-impact.md` before deciding the verification surface and record +compact applicability evidence. Only shared, materially unresolved, or +explicitly carried expanded work requires the common pre-build result and its +completion projection when `execution_intent != prepare_only`; prepare-only +work retains discovery applicability and artifact identity without implementation +completion claims. Evidenced local work keeps ordinary relevant verification and +self-review without a Node gate. Shared fanout alone does not activate an +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit diff --git a/skills/assistant-workflow/references/phases/design.md b/skills/assistant-workflow/references/phases/design.md index 63cf96ee..925e925d 100644 --- a/skills/assistant-workflow/references/phases/design.md +++ b/skills/assistant-workflow/references/phases/design.md @@ -12,6 +12,16 @@ the task needs shared routing/default, movement, harness, review, QA, or subagent separation decisions. This file owns phase execution mechanics; the controller reference owns decision boundaries and ordinary workflow defaults. +For a behavior-bearing, cosmetic, or locality/shared-impact claim, load +`references/change-impact.md` before deciding the verification surface and record +compact applicability evidence. Only shared, materially unresolved, or +explicitly carried expanded work requires the common pre-build result and its +completion projection when `execution_intent != prepare_only`; prepare-only +work retains discovery applicability and artifact identity without implementation +completion claims. Evidenced local work keeps ordinary relevant verification and +self-review without a Node gate. Shared fanout alone does not activate an +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit diff --git a/skills/assistant-workflow/references/phases/discover.md b/skills/assistant-workflow/references/phases/discover.md index a78bb568..850b45b8 100644 --- a/skills/assistant-workflow/references/phases/discover.md +++ b/skills/assistant-workflow/references/phases/discover.md @@ -12,6 +12,16 @@ the task needs shared routing/default, movement, harness, review, QA, or subagent separation decisions. This file owns phase execution mechanics; the controller reference owns decision boundaries and ordinary workflow defaults. +For a behavior-bearing, cosmetic, or locality/shared-impact claim, load +`references/change-impact.md` before deciding the verification surface and record +compact applicability evidence. Only shared, materially unresolved, or +explicitly carried expanded work requires the common pre-build result and its +completion projection when `execution_intent != prepare_only`; prepare-only +work retains discovery applicability and artifact identity without implementation +completion claims. Evidenced local work keeps ordinary relevant verification and +self-review without a Node gate. Shared fanout alone does not activate an +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit diff --git a/skills/assistant-workflow/references/phases/document.md b/skills/assistant-workflow/references/phases/document.md index 2e4f3cff..f06f4fd6 100644 --- a/skills/assistant-workflow/references/phases/document.md +++ b/skills/assistant-workflow/references/phases/document.md @@ -12,6 +12,16 @@ the task needs shared routing/default, movement, harness, review, QA, or subagent separation decisions. This file owns phase execution mechanics; the controller reference owns decision boundaries and ordinary workflow defaults. +For a behavior-bearing, cosmetic, or locality/shared-impact claim, load +`references/change-impact.md` before deciding the verification surface and record +compact applicability evidence. Only shared, materially unresolved, or +explicitly carried expanded work requires the common pre-build result and its +completion projection when `execution_intent != prepare_only`; prepare-only +work retains discovery applicability and artifact identity without implementation +completion claims. Evidenced local work keeps ordinary relevant verification and +self-review without a Node gate. Shared fanout alone does not activate an +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit diff --git a/skills/assistant-workflow/references/phases/plan.md b/skills/assistant-workflow/references/phases/plan.md index 5de9158d..b0f2fd0a 100644 --- a/skills/assistant-workflow/references/phases/plan.md +++ b/skills/assistant-workflow/references/phases/plan.md @@ -12,6 +12,16 @@ the task needs shared routing/default, movement, harness, review, QA, or subagent separation decisions. This file owns phase execution mechanics; the controller reference owns decision boundaries and ordinary workflow defaults. +For a behavior-bearing, cosmetic, or locality/shared-impact claim, load +`references/change-impact.md` before deciding the verification surface and record +compact applicability evidence. Only shared, materially unresolved, or +explicitly carried expanded work requires the common pre-build result and its +completion projection when `execution_intent != prepare_only`; prepare-only +work retains discovery applicability and artifact identity without implementation +completion claims. Evidenced local work keeps ordinary relevant verification and +self-review without a Node gate. Shared fanout alone does not activate an +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit diff --git a/skills/assistant-workflow/references/phases/preparation-completion.md b/skills/assistant-workflow/references/phases/preparation-completion.md index 7e315f53..03a2231a 100644 --- a/skills/assistant-workflow/references/phases/preparation-completion.md +++ b/skills/assistant-workflow/references/phases/preparation-completion.md @@ -12,6 +12,16 @@ the task needs shared routing/default, movement, harness, review, QA, or subagent separation decisions. This file owns phase execution mechanics; the controller reference owns decision boundaries and ordinary workflow defaults. +For a behavior-bearing, cosmetic, or locality/shared-impact claim, load +`references/change-impact.md` before deciding the verification surface and record +compact applicability evidence. Only shared, materially unresolved, or +explicitly carried expanded work requires the common pre-build result and its +completion projection when `execution_intent != prepare_only`; prepare-only +work retains discovery applicability and artifact identity without implementation +completion claims. Evidenced local work keeps ordinary relevant verification and +self-review without a Node gate. Shared fanout alone does not activate an +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit diff --git a/skills/assistant-workflow/references/phases/review.md b/skills/assistant-workflow/references/phases/review.md index aac105ed..21f9a472 100644 --- a/skills/assistant-workflow/references/phases/review.md +++ b/skills/assistant-workflow/references/phases/review.md @@ -12,6 +12,16 @@ the task needs shared routing/default, movement, harness, review, QA, or subagent separation decisions. This file owns phase execution mechanics; the controller reference owns decision boundaries and ordinary workflow defaults. +For a behavior-bearing, cosmetic, or locality/shared-impact claim, load +`references/change-impact.md` before deciding the verification surface and record +compact applicability evidence. Only shared, materially unresolved, or +explicitly carried expanded work requires the common pre-build result and its +completion projection when `execution_intent != prepare_only`; prepare-only +work retains discovery applicability and artifact identity without implementation +completion claims. Evidenced local work keeps ordinary relevant verification and +self-review without a Node gate. Shared fanout alone does not activate an +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit @@ -29,7 +39,7 @@ Print: `--- PHASE: REVIEW ---` Load `references/review-qa-router.md`. Reused validation evidence never replaces Spec Review, independent code review, or a current final review snapshot. Light work uses its compact fresh-review lane as a fresh self-review without worker or independent-review dispatch -evidence. Standard/strict work uses Stage 1 Spec Review and Stage 2 independent +evidence. Expanded impact instead records canonical `assistant-review` final-summary and delegation-path refs, without changing light Build. Standard/strict work uses Stage 1 Spec Review and Stage 2 independent Code Quality Review through `assistant-review`; Stage 3 QA Evaluation runs only when `qa_evaluation_mode=required`. diff --git a/skills/assistant-workflow/references/plan-harness-appendix.md b/skills/assistant-workflow/references/plan-harness-appendix.md index e60848d1..a89b9eaf 100644 --- a/skills/assistant-workflow/references/plan-harness-appendix.md +++ b/skills/assistant-workflow/references/plan-harness-appendix.md @@ -21,7 +21,7 @@ harness-capable. - artifact_reference_ledger_ref: [Artifact Reference Ledger section/ref, or N/A: reason] - Typed artifact refs: - artifact_id: [stable task-local id] - artifact_type: [done_contract | harness_recipe | harness_run_state | trace_ledger | replay_packet | pivot_restart_decision | changed_files | verification_evidence | plan_deviation | task_packet | context_map | test_result | review_result | qa_evaluation_result] + artifact_type: [done_contract | harness_recipe | harness_run_state | trace_ledger | replay_packet | pivot_restart_decision | changed_files | verification_evidence | plan_deviation | task_packet | context_map | architecture_decision_pack | change_impact_evidence | test_result | review_result | qa_evaluation_result] producer: [role/subagent/automation/task packet] consumer: [role/subagent/automation/phase] location_ref: [typed location/ref pointer] @@ -83,7 +83,7 @@ Each row is a typed producer/consumer record, not an ad hoc string reference. | Artifact ID | Artifact Type | Producer | Consumer | Location Ref | Schema or Contract | Validation Status | Summary | |-------------|---------------|----------|----------|--------------|--------------------|-------------------|---------| -| [id] | [done_contract/harness_recipe/harness_run_state/trace_ledger/replay_packet/pivot_restart_decision/changed_files/verification_evidence/plan_deviation/task_packet/context_map/test_result/review_result/qa_evaluation_result] | [role] | [role/phase] | [file/section/dispatch/command ref] | [contract/template/fields] | [pending/valid/invalid/stale/not_applicable] | [concise state] | +| [id] | [done_contract/harness_recipe/harness_run_state/trace_ledger/replay_packet/pivot_restart_decision/changed_files/verification_evidence/plan_deviation/task_packet/context_map/architecture_decision_pack/change_impact_evidence/test_result/review_result/qa_evaluation_result] | [role] | [role/phase] | [file/section/dispatch/command ref] | [contract/template/fields] | [pending/valid/invalid/stale/not_applicable] | [concise state] | ## QA Routing diff --git a/skills/assistant-workflow/references/review-qa-router.md b/skills/assistant-workflow/references/review-qa-router.md index b7361cf2..c10f1632 100644 --- a/skills/assistant-workflow/references/review-qa-router.md +++ b/skills/assistant-workflow/references/review-qa-router.md @@ -43,11 +43,11 @@ only evidence-backed findings within authorized scope before completion. Light work does not load the full rubric/QA loop unless risk or an independent trigger promotes it. This is a fresh self-review and does not require Code Reviewer, Reviewer, Code Writer, or Builder/Tester dispatch/direct -fallback evidence. When an Architecture Decision Pack applies, persist -validated refs to `assistant-review/contracts/output.yaml#final_summary`, its -exact current final-batch `final_snapshot_identity`, and -`assistant-review/contracts/output.yaml#architecture_decision_pack_review` in -`fresh_review_result`, including the canonical `review_delegation_path`. +fallback evidence. For an Architecture Decision Pack or expanded impact, invoke +`assistant-review`, resolve its own policy, and persist validated refs to its +final summary, exact current final-batch `final_snapshot_identity`, and +`review_delegation_path` in `fresh_review_result`; do not copy light Build policy. Persist +`assistant-review/contracts/output.yaml#architecture_decision_pack_review` only for a Pack. ## Stage 1 - Spec Review @@ -175,11 +175,12 @@ Enforce the review cycle before presenting results: Pack-backed `review_result` must also record validated refs to `assistant-review/contracts/output.yaml#architecture_decision_pack_review`; canonical review fields remain owned by assistant-review. -- Light Pack-backed `fresh_review_result` must record validated refs to - `assistant-review/contracts/output.yaml#final_summary`, its exact current - final-batch `final_snapshot_identity`, - `assistant-review/contracts/output.yaml#review_delegation_path`, and - `assistant-review/contracts/output.yaml#architecture_decision_pack_review`. +- Light Pack-backed or expanded-impact `fresh_review_result` must record + validated refs to `assistant-review/contracts/output.yaml#final_summary`, its + exact current final-batch `final_snapshot_identity`, and + `assistant-review/contracts/output.yaml#review_delegation_path`. + `assistant-review/contracts/output.yaml#architecture_decision_pack_review` + remains required only for a Pack. - Independent Code Reviewer dispatch/result evidence, or allowed fresh direct-fallback evidence, must be created in Review after Build completes. - When `qa_evaluation_mode=required`, workflow must record validated refs to diff --git a/skills/assistant-workflow/references/subagent-dispatch.md b/skills/assistant-workflow/references/subagent-dispatch.md index bfbf45c5..6371daf9 100644 --- a/skills/assistant-workflow/references/subagent-dispatch.md +++ b/skills/assistant-workflow/references/subagent-dispatch.md @@ -18,7 +18,7 @@ Before spawning any subagent, resolve: Light small low-risk localized work uses `subagent_policy_state=not_required` and `subagent_execution_mode=not_applicable`; it does not ask for delegation and instead records direct implementation, relevant automated validation/tests, and -a fresh self-review. For standard/strict development/code-work roles, Assistant +a fresh self-review. Expanded impact keeps this Build policy; Review records canonical `assistant-review` final-summary and delegation-path refs. For standard/strict development/code-work roles, Assistant Framework policy treats a direct user request or applicable `AGENTS.md` or active-skill instruction as a delegation trigger. Infer `subagent_trigger_scope` with its provenance and covered roles/phases/actions, set @@ -95,7 +95,7 @@ its compact validation plus fresh-review evidence instead. | Size | Agents used | Flow | |---|---|---| -| **Small light** | None | Direct implementation, relevant automated validation/tests, and fresh self-review; promote out of light when risk/harness/QA criteria apply | +| **Small light** | None | Direct implementation, relevant automated validation/tests, and fresh self-review; expanded impact records canonical review refs; promote for risk/harness/QA | | **Small standard/strict** | Bounded executor → Code Reviewer, or separated workers when triggered | Sequential, minimal (no Decompose); QA only when required | | **Medium** | Bounded executor → Code Reviewer, plus mapping/design roles only when their concrete trigger applies | Ordinary default; add Builder/Tester only when separated_workers triggers | | **Large** | Current-boundary mapping/analysis/design roles as needed → Code Writer/Builder-Tester when selected → Code Reviewer → QA Evaluator when required | Full evidence path is selected by risk and uncertainty, not by role count | @@ -105,7 +105,7 @@ its compact validation plus fresh-review evidence instead. - **Light lane**: small low-risk localized work may keep `required_agents` empty and use direct implementation, relevant automated validation/tests, and - fresh self-review evidence. `subagent_execution_mode=not_applicable` is valid + fresh self-review evidence. Expanded impact keeps Build direct but records canonical review refs. `subagent_execution_mode=not_applicable` is valid for this lane. Security, high-risk, harness-capable, required-QA, or otherwise promoted work cannot use this exception. - **Standard ordinary-medium minimum**: bounded executor → Code Reviewer. diff --git a/skills/assistant-workflow/references/task-journal-harness-appendix.md b/skills/assistant-workflow/references/task-journal-harness-appendix.md index fbab1b67..5e90dc7a 100644 --- a/skills/assistant-workflow/references/task-journal-harness-appendix.md +++ b/skills/assistant-workflow/references/task-journal-harness-appendix.md @@ -116,7 +116,7 @@ typed producer/consumer records, not ad hoc strings] | Artifact ID | Artifact Type | Producer | Consumer | Location Ref | Schema or Contract | Validation Status | Summary | |-------------|---------------|----------|----------|--------------|--------------------|-------------------|---------| -| [id] | [done_contract/harness_recipe/harness_run_state/trace_ledger/replay_packet/pivot_restart_decision/changed_files/verification_evidence/plan_deviation/task_packet/context_map/test_result/review_result/qa_evaluation_result] | [role/subagent/automation] | [role/subagent/phase] | [file/section/dispatch/command ref] | [contract/template/fields] | [pending/valid/invalid/stale/not_applicable] | [concise state] | +| [id] | [done_contract/harness_recipe/harness_run_state/trace_ledger/replay_packet/pivot_restart_decision/changed_files/verification_evidence/plan_deviation/task_packet/context_map/architecture_decision_pack/change_impact_evidence/test_result/review_result/qa_evaluation_result] | [role/subagent/automation] | [role/subagent/phase] | [file/section/dispatch/command ref] | [contract/template/fields] | [pending/valid/invalid/stale/not_applicable] | [concise state] | ## QA Evaluation Log diff --git a/skills/assistant-workflow/references/triage-rubric.md b/skills/assistant-workflow/references/triage-rubric.md index bcc810cb..fa50edab 100644 --- a/skills/assistant-workflow/references/triage-rubric.md +++ b/skills/assistant-workflow/references/triage-rubric.md @@ -53,7 +53,7 @@ through the existing post-Build Code Reviewer then QA Evaluator route. | Intensity | Use when | |---|---| -| `light` | Small low-risk localized work. | +| `light` | Small low-risk localized work; shared fanout alone does not promote Build. | | `standard` | Ordinary medium+ source-changing work when `harness_capable=false` and `qa_evaluation_mode=not_required`. | | `strict` | High/critical risk, or—only when `execution_intent != prepare_only`—`harness_capable == true`, `qa_evaluation_mode == required`, explicit harness/QA acceptance, or trace/replay. | diff --git a/skills/assistant-workflow/references/workflow-controller.md b/skills/assistant-workflow/references/workflow-controller.md index bf5e0540..14b957fa 100644 --- a/skills/assistant-workflow/references/workflow-controller.md +++ b/skills/assistant-workflow/references/workflow-controller.md @@ -68,14 +68,15 @@ file centralizes decision boundaries that cut across phase details while scope choices, repository approval policy, or an explicit user request. Wait for approval before Build. -- `light`: small, low-risk, local work with no public behavior, data, security, +- `light`: small, low-risk, localized work with no public behavior, data, security, harness, or QA acceptance risk. It may run inline/direct with relevant automated validation and a fresh self-review. Use `workflow_state_mode=inline`, `subagent_policy_state=not_required`, and `subagent_execution_mode=not_applicable`; do not require Code Writer, Builder/Tester, or independent Reviewer dispatch evidence. It does not require a journal, metrics, or manual verification unless separately - triggered. + triggered. Shared fanout alone keeps this Build policy; Review records canonical + `assistant-review` final-summary and delegation-path refs. - `standard`: ordinary medium+ source-changing work defaults to `controller_intensity=standard`, `harness_capable=false`, and `qa_evaluation_mode=not_required`. It uses diff --git a/tests/change-impact/change-impact.test.cjs b/tests/change-impact/change-impact.test.cjs new file mode 100644 index 00000000..7a870be9 --- /dev/null +++ b/tests/change-impact/change-impact.test.cjs @@ -0,0 +1,698 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); +const { test } = require("node:test"); +const { validate, readDocument, MAX_BYTES } = require("../../tools/change-impact/validate-change-impact.cjs"); + +const fixture = JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", "multidomain.v1.json"), "utf8")); +const commonExample = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "tools", "change-impact", "example.completion.v1.json"), "utf8")); + +function clone(value) { return JSON.parse(JSON.stringify(value)); } + +function reverseObjectKeys(value) { + if (Array.isArray(value)) return value.map(reverseObjectKeys); + if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).reverse().map(([key, child]) => [key, reverseObjectKeys(child)])); + return value; +} + +function validDocuments() { + const documents = clone(fixture); + documents.expected.required_edges = clone(documents.capture.edges); + documents.expected.required_requirements = documents.capture.requirements.map((requirement) => ({ + ...requirement, + verification_source_id: requirement.id === "req-nav-success" || requirement.id === "req-nav-modal" ? "test-navigation-current" : + requirement.id === "req-nav-dirty" ? "test-dirty-current" : requirement.id === "req-config" ? "test-config-current" : + requirement.id === "req-event" ? "test-event-current" : requirement.id === "req-state" ? "test-state-current" : + requirement.id === "req-cache" ? "test-cache-current" : "public-compatibility-evidence-current", + })); + const sources = new Map(documents.expected.required_requirements.map((requirement) => [requirement.id, requirement.verification_source_id])); + documents.assessment.verification_plans = [ + ["plan-nav", "navigation", "route-success", "test-navigation-current"], ["plan-dirty", "navigation", "dirty-cancel", "test-dirty-current"], + ["plan-config", "configuration", "loads-default", "test-config-current"], ["plan-event", "async-work", "stale-completion", "test-event-current"], + ["plan-state", "state", "restore-draft", "test-state-current"], ["plan-cache", "cache", "fresh-miss", "test-cache-current"], + ].map(([id, contract_id, state_transition, source_identity]) => ({ id, contract_id, state_transition, oracle: `oracle-${id}`, steps_ref: `steps-${id}`, source_identity })); + const planFor = { "req-nav-success": "plan-nav", "req-nav-dirty": "plan-dirty", "req-nav-modal": "plan-nav", "req-config": "plan-config", "req-event": "plan-event", "req-state": "plan-state", "req-cache": "plan-cache" }; + documents.assessment.obligations = documents.capture.requirements.map((requirement, index) => ({ + id: `obligation-${index + 1}`, requirement_id: requirement.id, disposition: requirement.id === "req-public" ? "unaffected" : "preserve", + rationale_ref: `rationale-${requirement.id}`, authorization_ref: null, verification_id: requirement.id === "req-nav-success" || requirement.id === "req-nav-modal" || requirement.id === "req-public" ? null : planFor[requirement.id], + equivalence_group_id: requirement.id === "req-nav-success" || requirement.id === "req-nav-modal" ? "group-navigation-success" : null, + })); + documents.assessment.equivalence_groups = [{ id: "group-navigation-success", member_obligation_ids: ["obligation-1", "obligation-3"], justification_ref: "same-route-contract", verification_id: "plan-nav" }]; + documents.assessment.actual_verifications = documents.assessment.verification_plans.map((plan) => ({ verification_id: plan.id, outcome: "passed", executed_snapshot: clone(documents.assessment.snapshot), executed_source_identity: plan.source_identity, evidence_ref: `result-${plan.id}` })); + documents.expected.review_context.required_bindings = documents.capture.requirements.map((requirement, index) => ({ requirement_id: requirement.id, scope_item_id: `scope-${index + 1}`, coverage_concern_id: `concern-${index + 1}` })); + documents.review.bindings = documents.assessment.obligations.map((obligation, index) => ({ obligation_id: obligation.id, requirement_id: obligation.requirement_id, scope_item_id: `scope-${index + 1}`, coverage_concern_id: `concern-${index + 1}` })); + return documents; +} + +function codes(result) { return result.reasons.map((reason) => reason.code); } + +function runCli(documents, phase, mutate, priorResult, indentation) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "change-impact-")); + if (mutate) mutate(documents, directory); + for (const [name, value] of Object.entries(documents)) fs.writeFileSync(path.join(directory, `${name}.json`), JSON.stringify(value, null, indentation)); + const args = [path.join(process.cwd(), "tools/change-impact/validate-change-impact.cjs"), "--phase", phase, "--capture", path.join(directory, "capture.json"), "--expected", path.join(directory, "expected.json")]; + if (phase !== "discovery") args.push("--assessment", path.join(directory, "assessment.json")); + if (phase === "completion") args.push("--review", path.join(directory, "review.json")); + if (priorResult !== undefined) { + fs.writeFileSync(path.join(directory, "receipt.json"), JSON.stringify(priorResult)); + args.push("--receipt", path.join(directory, "receipt.json")); + } + const child = spawnSync(process.execPath, args, { encoding: "utf8" }); + return { ...child, result: JSON.parse(child.stdout) }; +} + +function runApiAtRuntime(version) { + const script = [ + 'const fs = require("node:fs");', + 'Object.defineProperty(process.versions, "node", { value: process.argv[2], configurable: true });', + 'const { validate } = require(process.argv[1]);', + 'const documents = JSON.parse(fs.readFileSync(process.argv[3], "utf8"));', + 'process.stdout.write(JSON.stringify(validate({ phase: "completion", ...documents })));', + ].join(""); + const child = spawnSync(process.execPath, ["-e", script, path.join(process.cwd(), "tools/change-impact/validate-change-impact.cjs"), version, path.join(process.cwd(), "tools/change-impact/example.completion.v1.json")], { encoding: "utf8" }); + return { ...child, result: JSON.parse(child.stdout) }; +} + +function runCliAtRuntime(version) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "change-impact-runtime-")); + try { + const bootstrap = path.join(directory, "runtime.cjs"); + fs.writeFileSync(bootstrap, `Object.defineProperty(process.versions, "node", { value: ${JSON.stringify(version)}, configurable: true });`); + const child = spawnSync(process.execPath, ["--require", bootstrap, path.join(process.cwd(), "tools/change-impact/validate-change-impact.cjs"), "--phase", "discovery", "--capture", path.join(directory, "missing-capture.json"), "--expected", path.join(directory, "missing-expected.json")], { encoding: "utf8" }); + return { ...child, result: JSON.parse(child.stdout) }; + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +} + +test("multidomain completion closes independent consumer, transition, verification, and review authority", () => { + const result = validate({ phase: "completion", ...validDocuments() }); + assert.equal(result.valid, true); + assert.equal(result.complete, true); +}); + +test("portable completion example is an executable protocol document", () => { + assert.equal(validate({ phase: "completion", ...commonExample }).complete, true); +}); + +test("API and CLI reject unsupported Node runtimes before validation while Node 22 remains supported", () => { + for (const version of ["20.20.0", "21.7.3"]) { + const api = runApiAtRuntime(version); + assert.equal(api.status, 0, api.stderr); + assert.deepEqual(api.result, { schema_version: "change-impact-validation-result/v1", valid: false, complete: false, phase: null, reasons: [{ code: "RUNTIME_NODE22_OR_NEWER_REQUIRED" }], receipt: null }); + + const cli = runCliAtRuntime(version); + assert.equal(cli.status, 2, cli.stderr); + assert.deepEqual(cli.result, api.result); + } + + const supported = runApiAtRuntime("22.0.0"); + assert.equal(supported.status, 0, supported.stderr); + assert.equal(supported.result.complete, true); +}); + +test("behavior pre-build and completion reject an all-unaffected assessment without a bound current verification", () => { + const documents = clone(commonExample); + documents.assessment.obligations = documents.assessment.obligations.map((obligation) => ({ + ...obligation, + disposition: "unaffected", + verification_id: null, + equivalence_group_id: null, + })); + documents.assessment.verification_plans = []; + documents.assessment.equivalence_groups = []; + documents.assessment.actual_verifications = []; + + assert.equal(validate({ phase: "discovery", capture: documents.capture, expected: documents.expected }).valid, true); + const preBuildResult = validate({ phase: "pre_build", ...documents }); + assert.equal(preBuildResult.valid, false); + const preBuildCliResult = runCli(documents, "pre_build"); + assert.equal(preBuildCliResult.status, 1); + assert.deepEqual(preBuildCliResult.result, preBuildResult); + const apiResult = validate({ phase: "completion", ...documents }); + assert.equal(apiResult.complete, false); + assert.ok(codes(apiResult).includes("BEHAVIOR_COMPLETION_VERIFICATION_MISSING")); + + const cliResult = runCli(documents, "completion"); + assert.equal(cliResult.status, 1); + assert.deepEqual(cliResult.result, apiResult); +}); + +test("document admission reads a single descriptor with a byte bound before parsing", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "change-impact-descriptor-")); + const document = path.join(directory, "document.json"); + fs.writeFileSync(document, '{"label":"é"}'); + assert.deepEqual(readDocument(document), { label: "é" }); + + let shortRead = true; + const originalReadSync = fs.readSync; + fs.readSync = function shortReadSync(descriptor, buffer, offset, length, position) { + if (shortRead) { + shortRead = false; + return originalReadSync.call(this, descriptor, buffer, offset, 1, position); + } + return originalReadSync.call(this, descriptor, buffer, offset, length, position); + }; + try { + assert.deepEqual(readDocument(document), { label: "é" }); + } finally { + fs.readSync = originalReadSync; + } + + fs.writeFileSync(document, "{}"); + fs.readSync = function boundedReadSync(...args) { + fs.writeFileSync(document, " ".repeat(MAX_BYTES + 1)); + return originalReadSync.apply(this, args); + }; + try { + assert.throws(() => readDocument(document), /INPUT_TOO_LARGE/); + } finally { + fs.readSync = originalReadSync; + } +}); + +test("document admission rejects a FIFO without blocking", (context) => { + if (process.platform === "win32") return context.skip("FIFO admission is not available on Windows"); + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "change-impact-fifo-")); + const fifo = path.join(directory, "document.fifo"); + const create = spawnSync("mkfifo", [fifo]); + if (create.error || create.status !== 0) return context.skip("mkfifo is unavailable"); + assert.throws(() => readDocument(fifo), /INPUT_TOO_LARGE/); +}); + +test("shared and material captures record a discovery root, while compact cosmetic controls remain optional", () => { + const shared = clone(commonExample); + shared.capture.roots = []; + assert.ok(codes(validate({ phase: "completion", ...shared })).includes("CAPTURE_DISCOVERY_ROOTS_MISSING")); + + const material = validDocuments(); + material.expected.required_impact_scope = "local"; + material.assessment.impact_scope = "local"; + material.capture.roots = []; + material.capture.unknown_boundaries.push({ id: "unknown-material-root", material: true, source_id: "boundary-current" }); + assert.ok(codes(validate({ phase: "discovery", capture: material.capture, expected: material.expected })).includes("CAPTURE_DISCOVERY_ROOTS_MISSING")); + + const cosmetic = validDocuments(); + cosmetic.expected.required_impact_scope = "not_applicable"; + cosmetic.assessment.impact_scope = "not_applicable"; + cosmetic.assessment.assessment_kind = "cosmetic"; + cosmetic.capture.roots = []; + cosmetic.assessment.obligations = cosmetic.assessment.obligations.map((obligation) => ({ + ...obligation, + disposition: "unaffected", + verification_id: null, + equivalence_group_id: null, + })); + cosmetic.assessment.verification_plans = []; + cosmetic.assessment.equivalence_groups = []; + cosmetic.assessment.actual_verifications = []; + assert.equal(validate({ phase: "discovery", capture: cosmetic.capture, expected: cosmetic.expected }).valid, true); + assert.equal(validate({ phase: "pre_build", ...cosmetic }).valid, true); +}); + +test("shared inventory requires captured consumers and contract/state-transition requirements", () => { + const emptyShared = clone(commonExample); + emptyShared.capture.edges = []; emptyShared.capture.requirements = []; + emptyShared.expected.required_edges = []; emptyShared.expected.required_requirements = []; + emptyShared.expected.review_context.required_bindings = []; + emptyShared.assessment.obligations = []; emptyShared.assessment.verification_plans = []; + emptyShared.assessment.equivalence_groups = []; emptyShared.assessment.actual_verifications = []; + emptyShared.review.bindings = []; + const apiResult = validate({ phase: "pre_build", ...emptyShared }); + assert.equal(apiResult.phase, "pre_build"); + assert.deepEqual(codes(apiResult), ["CAPTURE_SHARED_CONSUMERS_EMPTY", "CAPTURE_SHARED_REQUIREMENTS_EMPTY"]); + + const cliResult = runCli(emptyShared, "pre_build"); + assert.equal(cliResult.status, 1); + assert.deepEqual(cliResult.result, apiResult); + + const completionApiResult = validate({ phase: "completion", ...emptyShared }); + const completionCliResult = runCli(emptyShared, "completion"); + assert.equal(completionApiResult.phase, "completion"); + assert.equal(completionCliResult.status, 1); + assert.deepEqual(completionCliResult.result, completionApiResult); +}); + +test("behavior assessments require roots, consumers, and requirements at pre-build and completion, including local", () => { + for (const phase of ["pre_build", "completion"]) { + const missingRoots = validDocuments(); + missingRoots.expected.required_impact_scope = "local"; + missingRoots.assessment.impact_scope = "local"; + missingRoots.capture.roots = []; + if (phase === "pre_build") missingRoots.assessment.actual_verifications = []; + assert.ok(codes(validate({ phase, ...missingRoots })).includes("CAPTURE_BEHAVIOR_ROOTS_EMPTY"), phase); + + const missingConsumers = validDocuments(); + missingConsumers.expected.required_impact_scope = "local"; + missingConsumers.assessment.impact_scope = "local"; + missingConsumers.capture.edges = []; + missingConsumers.capture.requirements = []; + missingConsumers.expected.required_edges = []; + missingConsumers.expected.required_requirements = []; + missingConsumers.expected.review_context.required_bindings = []; + missingConsumers.assessment.obligations = []; + missingConsumers.assessment.verification_plans = []; + missingConsumers.assessment.equivalence_groups = []; + missingConsumers.assessment.actual_verifications = []; + missingConsumers.review.bindings = []; + assert.ok(codes(validate({ phase, ...missingConsumers })).includes("CAPTURE_BEHAVIOR_CONSUMERS_EMPTY"), phase); + + const missingRequirements = validDocuments(); + missingRequirements.expected.required_impact_scope = "local"; + missingRequirements.assessment.impact_scope = "local"; + missingRequirements.capture.requirements = []; + missingRequirements.expected.required_requirements = []; + missingRequirements.expected.review_context.required_bindings = []; + missingRequirements.assessment.obligations = []; + missingRequirements.assessment.verification_plans = []; + missingRequirements.assessment.equivalence_groups = []; + missingRequirements.assessment.actual_verifications = []; + missingRequirements.review.bindings = []; + assert.ok(codes(validate({ phase, ...missingRequirements })).includes("CAPTURE_EDGE_REQUIREMENT_MISSING"), phase); + } + + const emptyLocal = clone(commonExample); + emptyLocal.expected.required_impact_scope = "local"; + emptyLocal.assessment.impact_scope = "local"; + emptyLocal.capture.roots = []; + emptyLocal.capture.edges = []; + emptyLocal.capture.requirements = []; + emptyLocal.expected.required_edges = []; + emptyLocal.expected.required_requirements = []; + emptyLocal.expected.review_context.required_bindings = []; + emptyLocal.assessment.obligations = []; + emptyLocal.assessment.verification_plans = []; + emptyLocal.assessment.equivalence_groups = []; + emptyLocal.assessment.actual_verifications = []; + emptyLocal.review.bindings = []; + const apiResult = validate({ phase: "completion", ...emptyLocal }); + const cliResult = runCli(emptyLocal, "completion"); + assert.equal(apiResult.valid, false); + assert.equal(cliResult.status, 1); + assert.deepEqual(cliResult.result, apiResult); + + const populatedLocal = validDocuments(); + populatedLocal.expected.required_impact_scope = "local"; + populatedLocal.assessment.impact_scope = "local"; + assert.equal(validate({ phase: "completion", ...populatedLocal }).complete, true); +}); + +test("discovery and pre-build permit planned work without fabricated actual execution", () => { + const documents = validDocuments(); + assert.equal(validate({ phase: "discovery", capture: documents.capture, expected: documents.expected }).valid, true); + documents.assessment.actual_verifications = []; + assert.equal(validate({ phase: "pre_build", ...documents }).valid, true); + documents.expected.review_context = null; + assert.equal(validate({ phase: "discovery", capture: documents.capture, expected: documents.expected }).valid, true); + assert.equal(validate({ phase: "pre_build", ...documents }).valid, true); + assert.ok(codes(validate({ phase: "completion", ...documents })).includes("EXPECTED_REVIEW_CONTEXT_INVALID")); +}); + +test("invalid phase diagnostics retain PHASE_INVALID without echoing untrusted phase input", () => { + const sentinel = "PRIVATE_PHASE_SENTINEL"; + const result = validate({ phase: sentinel, ...validDocuments() }); + assert.equal(result.phase, null); + assert.deepEqual(codes(result), ["PHASE_INVALID"]); + + const child = runCli(validDocuments(), sentinel); + assert.equal(child.status, 1); + assert.equal(child.result.phase, null); + assert.deepEqual(codes(child.result), ["PHASE_INVALID"]); + assert.equal(child.stdout.includes(sentinel), false); +}); + +test("canonical planned pre_build retains unaffected plus bound direct and equivalence plans through the API and CLI", () => { + const documents = validDocuments(); + documents.assessment.actual_verifications = []; + const apiResult = validate({ phase: "pre_build", ...documents }); + const cliResult = runCli(documents, "pre_build"); + assert.equal(apiResult.valid, true); + assert.equal(cliResult.status, 0); + assert.deepEqual(cliResult.result, apiResult); +}); + +test("validation receipts bind current relevant parsed inputs for API and CLI reuse", () => { + const documents = validDocuments(); + documents.assessment.actual_verifications = []; + const fresh = validate({ phase: "pre_build", ...documents }); + assert.deepEqual(fresh.receipt, { + schema_version: "change-impact-validation-receipt/v1", + validation_contract: "change-impact-validator/v1", + canonicalization: "change-impact-canonical-json/v1", + digest_algorithm: "sha256", + phase: "pre_build", + input_digests: fresh.receipt.input_digests, + }); + assert.ok(Object.values(fresh.receipt.input_digests).every((value) => value === null || /^sha256:[0-9a-f]{64}$/.test(value))); + assert.equal(fresh.receipt.input_digests.review, null); + + const apiReuse = validate({ phase: "pre_build", ...documents, priorResult: fresh }); + const cliReuse = runCli(documents, "pre_build", null, fresh); + assert.deepEqual(apiReuse, fresh); + assert.equal(cliReuse.status, 0); + assert.deepEqual(cliReuse.result, fresh); + + const reordered = clone(documents); + reordered.capture = Object.fromEntries(Object.entries(reordered.capture).reverse()); + assert.deepEqual(validate({ phase: "pre_build", ...reordered }).receipt, fresh.receipt); + const reorderedArrays = clone(documents); + reorderedArrays.capture.roots.reverse(); + assert.notEqual(validate({ phase: "pre_build", ...reorderedArrays }).receipt.input_digests.capture, fresh.receipt.input_digests.capture); + + const changedAssessment = clone(documents); + changedAssessment.assessment.verification_plans[0].oracle = "changed-current-oracle"; + const mismatch = validate({ phase: "pre_build", ...changedAssessment, priorResult: fresh }); + assert.equal(mismatch.valid, false); + assert.equal(mismatch.receipt, null); + assert.ok(codes(mismatch).includes("RECEIPT_MISMATCH")); + + const malformed = validate({ phase: "pre_build", ...documents, priorResult: { valid: true } }); + assert.equal(malformed.receipt, null); + assert.ok(codes(malformed).includes("RECEIPT_SCHEMA_INVALID")); + const outerPhaseMismatch = clone(fresh); + outerPhaseMismatch.phase = "completion"; + assert.ok(codes(validate({ phase: "pre_build", ...documents, priorResult: outerPhaseMismatch })).includes("RECEIPT_SCHEMA_INVALID")); + const outerCompletionMismatch = clone(fresh); + outerCompletionMismatch.complete = true; + assert.ok(codes(validate({ phase: "pre_build", ...documents, priorResult: outerCompletionMismatch })).includes("RECEIPT_SCHEMA_INVALID")); + const phaseSlotMismatch = clone(fresh); + phaseSlotMismatch.receipt.input_digests.review = phaseSlotMismatch.receipt.input_digests.capture; + assert.ok(codes(validate({ phase: "pre_build", ...documents, priorResult: phaseSlotMismatch })).includes("RECEIPT_SCHEMA_INVALID")); + const invalidCurrent = clone(documents); + invalidCurrent.assessment.obligations.pop(); + const invalidResult = validate({ phase: "pre_build", ...invalidCurrent, priorResult: fresh }); + assert.equal(invalidResult.receipt, null); + assert.ok(codes(invalidResult).includes("CAPTURE_REQUIREMENT_OBLIGATION_MISSING")); + + const completion = validDocuments(); + const phaseMismatch = validate({ phase: "completion", ...completion, priorResult: fresh }); + assert.ok(codes(phaseMismatch).includes("RECEIPT_MISMATCH")); + const discovery = validate({ phase: "discovery", capture: documents.capture, expected: documents.expected, assessment: { ignored: true } }); + assert.equal(discovery.receipt.input_digests.assessment, null); + assert.equal(discovery.receipt.input_digests.review, null); +}); + +test("receipts reuse every phase and reject valid current capture, expected, and review changes", () => { + for (const phase of ["discovery", "pre_build", "completion"]) { + const documents = validDocuments(); + if (phase === "pre_build") documents.assessment.actual_verifications = []; + const fresh = validate({ phase, ...documents }); + assert.equal(fresh.valid, true, phase); + assert.deepEqual(validate({ phase, ...documents, priorResult: fresh }), fresh, `${phase} API`); + const cli = runCli(documents, phase, null, fresh); + assert.equal(cli.status, 0, `${phase} CLI`); + assert.deepEqual(cli.result, fresh, `${phase} CLI result`); + } + + const preBuild = validDocuments(); + preBuild.assessment.actual_verifications = []; + const priorPreBuild = validate({ phase: "pre_build", ...preBuild }); + const changedCapture = clone(preBuild); + changedCapture.capture.roots[0].source_id = "current-capture-changed"; + assert.ok(codes(validate({ phase: "pre_build", ...changedCapture, priorResult: priorPreBuild })).includes("RECEIPT_MISMATCH")); + const changedExpected = clone(preBuild); + changedExpected.expected.authority_id = "current-expected-changed"; + assert.ok(codes(validate({ phase: "pre_build", ...changedExpected, priorResult: priorPreBuild })).includes("RECEIPT_MISMATCH")); + + const completion = validDocuments(); + const priorCompletion = validate({ phase: "completion", ...completion }); + const reorderedReview = clone(completion); + reorderedReview.review.bindings.reverse(); + assert.equal(validate({ phase: "completion", ...reorderedReview }).valid, true); + assert.ok(codes(validate({ phase: "completion", ...reorderedReview, priorResult: priorCompletion })).includes("RECEIPT_MISMATCH")); +}); + +test("receipt canonicalization ignores recursive object order and CLI whitespace, while API admission fails safely", () => { + const documents = validDocuments(); + documents.assessment.actual_verifications = []; + const fresh = validate({ phase: "pre_build", ...documents }); + const reordered = reverseObjectKeys(documents); + assert.deepEqual(validate({ phase: "pre_build", ...reordered }).receipt, fresh.receipt); + const compactCli = runCli(documents, "pre_build"); + const spacedCli = runCli(documents, "pre_build", null, undefined, 2); + assert.deepEqual(spacedCli.result.receipt, compactCli.result.receipt); + + const aliased = validDocuments(); + for (const actual of aliased.assessment.actual_verifications) actual.executed_snapshot = aliased.assessment.snapshot; + const aliasedResult = validate({ phase: "completion", ...aliased }); + const jsonClonedResult = validate({ phase: "completion", ...clone(aliased) }); + assert.equal(aliasedResult.valid, true); + assert.deepEqual(aliasedResult, jsonClonedResult); + + const cyclic = validDocuments(); + cyclic.assessment.actual_verifications = []; + cyclic.capture.snapshot.loop = cyclic.capture.snapshot; + const cyclicResult = validate({ phase: "pre_build", ...cyclic }); + assert.equal(cyclicResult.valid, false); + assert.equal(cyclicResult.receipt, null); + const oversized = validDocuments(); + oversized.assessment.actual_verifications = []; + oversized.capture.roots[0].source_id = "x".repeat(MAX_BYTES + 1); + const oversizedResult = validate({ phase: "pre_build", ...oversized }); + assert.equal(oversizedResult.valid, false); + assert.equal(oversizedResult.receipt, null); + + const dense = validDocuments(); + dense.assessment.actual_verifications = []; + const densePrior = validate({ phase: "pre_build", ...dense }); + const sparse = clone(dense); + sparse.capture.unknown_boundaries = Array(1); + for (const priorResult of [undefined, densePrior]) { + const sparseResult = validate({ phase: "pre_build", ...sparse, priorResult }); + assert.equal(sparseResult.receipt, null); + assert.ok(codes(sparseResult).includes("INPUT_UNSAFE_SHAPE")); + } + + const accessor = clone(dense); + let accessorReads = 0; + Object.defineProperty(accessor.capture, "capture_id", { enumerable: true, get() { accessorReads += 1; return "capture-example-current"; } }); + for (const priorResult of [undefined, densePrior]) { + const accessorResult = validate({ phase: "pre_build", ...accessor, priorResult }); + assert.equal(accessorResult.receipt, null); + assert.ok(codes(accessorResult).includes("INPUT_UNSAFE_SHAPE")); + } + assert.equal(accessorReads, 0); + const proxy = clone(dense); + proxy.capture = new Proxy(proxy.capture, {}); + assert.ok(codes(validate({ phase: "pre_build", ...proxy })).includes("INPUT_UNSAFE_SHAPE")); + const symbol = clone(dense); + symbol.capture[Symbol("unexpected")] = "value"; + assert.ok(codes(validate({ phase: "pre_build", ...symbol })).includes("INPUT_UNSAFE_SHAPE")); + const nonEnumerable = clone(dense); + Object.defineProperty(nonEnumerable.capture, "hidden", { value: "value", enumerable: false }); + assert.ok(codes(validate({ phase: "pre_build", ...nonEnumerable })).includes("INPUT_UNSAFE_SHAPE")); + const extraArrayKey = clone(dense); + extraArrayKey.capture.unknown_boundaries.extra = "value"; + for (const priorResult of [undefined, densePrior]) assert.ok(codes(validate({ phase: "pre_build", ...extraArrayKey, priorResult })).includes("INPUT_UNSAFE_SHAPE")); + const frozen = clone(dense); + Object.freeze(frozen.capture); + const frozenResult = validate({ phase: "pre_build", ...frozen }); + assert.equal(frozenResult.valid, true); + assert.deepEqual(validate({ phase: "pre_build", ...frozen, priorResult: frozenResult }), frozenResult); + + const prior = fresh; + assert.equal(runCli(documents, "pre_build", null, { valid: true }).status, 1); + assert.equal(runCli(documents, "pre_build", null, "x".repeat(MAX_BYTES + 1)).status, 2); + let deep = {}; + for (let index = 0; index <= 17; index += 1) deep = { nested: deep }; + assert.equal(runCli(documents, "pre_build", null, deep).status, 2); + assert.equal(runCli(documents, "pre_build", null, prior).status, 0); +}); + +test("completion binds direct and equivalence verification records to the executed snapshot", () => { + const current = validDocuments(); + assert.equal(validate({ phase: "completion", ...current }).complete, true); + + const stale = validDocuments(); + const refreshedSnapshot = { base_id: "base-refreshed", candidate_id: "candidate-refreshed", universe_id: "universe-refreshed" }; + stale.capture.capture_id = "capture-refreshed"; + stale.expected.capture_id = "capture-refreshed"; + stale.assessment.capture_id = "capture-refreshed"; + stale.review.capture_id = "capture-refreshed"; + stale.capture.snapshot = clone(refreshedSnapshot); + stale.expected.snapshot = clone(refreshedSnapshot); + stale.assessment.snapshot = clone(refreshedSnapshot); + stale.review.snapshot = clone(refreshedSnapshot); + const apiResult = validate({ phase: "completion", ...stale }); + assert.ok(codes(apiResult).includes("EXECUTED_VERIFICATION_MISSING_OR_STALE")); + const cliResult = runCli(stale, "completion"); + assert.equal(cliResult.status, 1); + assert.deepEqual(cliResult.result, apiResult); + + for (const mutate of [ + (documents) => { delete documents.assessment.actual_verifications[0].executed_snapshot; }, + (documents) => { documents.assessment.actual_verifications[0].executed_snapshot = { candidate_id: "candidate-only" }; }, + (documents) => { documents.assessment.actual_verifications[0].executed_snapshot.base_id = "base-stale"; }, + (documents) => { documents.assessment.actual_verifications[0].executed_snapshot.candidate_id = "candidate-stale"; }, + (documents) => { documents.assessment.actual_verifications[0].executed_snapshot.universe_id = "universe-stale"; }, + ]) { + const documents = validDocuments(); + mutate(documents); + const result = validate({ phase: "completion", ...documents }); + assert.equal(result.valid, false); + assert.ok(codes(result).some((code) => ["ACTUAL_VERIFICATIONS_INVALID", "EXECUTED_VERIFICATION_MISSING_OR_STALE"].includes(code))); + } +}); + +test("completion rejects each stale component on the direct and equivalence execution records", () => { + for (const verificationId of ["plan-nav", "plan-dirty"]) { + for (const component of ["base_id", "candidate_id", "universe_id"]) { + const documents = validDocuments(); + const actual = documents.assessment.actual_verifications.find((item) => item.verification_id === verificationId); + actual.executed_snapshot[component] = `${actual.executed_snapshot[component]}-stale`; + assert.ok(codes(validate({ phase: "completion", ...documents })).includes("EXECUTED_VERIFICATION_MISSING_OR_STALE"), `${verificationId} ${component}`); + } + } +}); + +test("known omitted consumer edge and omitted dirty-state transition fail against independent authority", () => { + const documents = validDocuments(); + documents.capture.edges.pop(); + documents.capture.requirements = documents.capture.requirements.filter((requirement) => requirement.edge_id !== "edge-cache"); + assert.ok(codes(validate({ phase: "discovery", ...documents })).includes("CAPTURE_EXPECTED_EDGES_MISMATCH")); + const transition = validDocuments(); + transition.assessment.obligations = transition.assessment.obligations.filter((item) => item.requirement_id !== "req-nav-dirty"); + assert.ok(codes(validate({ phase: "pre_build", ...transition })).includes("CAPTURE_REQUIREMENT_OBLIGATION_MISSING")); +}); + +test("base deletion, candidate addition, all supported dependency kinds, and equivalent navigation verification are covered", () => { + const documents = validDocuments(); + assert.deepEqual(new Set(documents.capture.edges.map((edge) => edge.dependency_kind)), new Set(["call", "wrapper", "config", "registration", "event", "state", "public"])); + assert.ok(documents.capture.edges.some((edge) => edge.presence === "base")); + assert.ok(documents.capture.edges.some((edge) => edge.presence === "candidate")); + assert.equal(validate({ phase: "completion", ...documents }).valid, true); + documents.assessment.equivalence_groups[0].justification_ref = ""; + assert.ok(codes(validate({ phase: "pre_build", ...documents })).includes("EQUIVALENCE_GROUPS_INVALID")); +}); + +test("unsupported local claim, stale universe, stale verification source, and wrong review projection reject", () => { + const local = validDocuments(); local.assessment.impact_scope = "local"; + assert.ok(codes(validate({ phase: "pre_build", ...local })).includes("IMPACT_SCOPE_CONTEXT_MISMATCH")); + const stale = validDocuments(); stale.expected.snapshot.universe_id = "universe-after-new-registration"; + assert.ok(codes(validate({ phase: "completion", ...stale })).includes("CAPTURE_CONTEXT_MISMATCH")); + const source = validDocuments(); source.assessment.verification_plans[1].source_identity = "old-test-source"; + assert.ok(codes(validate({ phase: "completion", ...source })).includes("VERIFICATION_REQUIREMENT_MISMATCH")); + const review = validDocuments(); review.review.bindings[0].coverage_concern_id = "wrong-concern"; + assert.ok(codes(validate({ phase: "completion", ...review })).includes("REVIEW_OBLIGATION_BINDING_INCOMPLETE")); +}); + +test("waived obligations require authorization at pre-build and remain residual risk at completion", () => { + const missingAuthority = validDocuments(); missingAuthority.assessment.obligations[6].disposition = "waived"; missingAuthority.assessment.actual_verifications = []; + assert.deepEqual(codes(validate({ phase: "pre_build", ...missingAuthority })), ["AUTHORIZATION_REFERENCE_MISSING"]); + const waived = validDocuments(); waived.assessment.obligations[6].disposition = "waived"; waived.assessment.obligations[6].authorization_ref = "existing-policy-approval"; waived.assessment.actual_verifications = []; + assert.equal(validate({ phase: "pre_build", ...waived }).valid, true); + assert.ok(codes(validate({ phase: "completion", ...waived })).includes("UNVERIFIED_RESIDUAL_RISK")); +}); + +test("blocked, unresolved, and planned-as-actual completion states remain incomplete", () => { + const blocked = validDocuments(); blocked.assessment.obligations[6].disposition = "blocked"; blocked.assessment.actual_verifications = []; + assert.ok(codes(validate({ phase: "pre_build", ...blocked })).includes("BLOCKED_IMPACT_UNRESOLVED")); + assert.ok(codes(validate({ phase: "completion", ...blocked })).includes("UNVERIFIED_RESIDUAL_RISK")); + const unresolved = validDocuments(); unresolved.capture.unknown_boundaries.push({ id: "unknown-public-client", material: true, source_id: "boundary-current" }); + assert.ok(codes(validate({ phase: "pre_build", ...unresolved })).includes("MATERIAL_IMPACT_UNRESOLVED")); + assert.ok(codes(validate({ phase: "completion", ...unresolved })).includes("MATERIAL_IMPACT_UNRESOLVED")); + const planned = validDocuments(); planned.assessment.actual_verifications = []; + assert.ok(codes(validate({ phase: "completion", ...planned })).includes("EXECUTED_VERIFICATION_MISSING_OR_STALE")); +}); + +test("capture requirements, reciprocal equivalence, and exact plan and actual consumption cannot be bypassed", () => { + const edgeWithoutRequirement = validDocuments(); + edgeWithoutRequirement.capture.requirements = edgeWithoutRequirement.capture.requirements.filter((item) => item.edge_id !== "edge-cache"); + edgeWithoutRequirement.expected.required_requirements = edgeWithoutRequirement.expected.required_requirements.filter((item) => item.edge_id !== "edge-cache"); + edgeWithoutRequirement.expected.review_context.required_bindings = edgeWithoutRequirement.expected.review_context.required_bindings.filter((item) => item.requirement_id !== "req-cache"); + assert.ok(codes(validate({ phase: "discovery", ...edgeWithoutRequirement })).includes("CAPTURE_EDGE_REQUIREMENT_MISSING")); + const directReuse = validDocuments(); directReuse.assessment.equivalence_groups = []; + directReuse.assessment.obligations[0].verification_id = "plan-nav"; directReuse.assessment.obligations[0].equivalence_group_id = null; + directReuse.assessment.obligations[2].verification_id = "plan-nav"; directReuse.assessment.obligations[2].equivalence_group_id = null; + assert.ok(codes(validate({ phase: "pre_build", ...directReuse })).includes("DIRECT_VERIFICATION_REUSE_REQUIRES_EQUIVALENCE")); + const reciprocal = validDocuments(); reciprocal.assessment.obligations[2].equivalence_group_id = null; + assert.ok(codes(validate({ phase: "pre_build", ...reciprocal })).includes("EQUIVALENCE_GROUP_BINDING_INVALID")); + const orphan = validDocuments(); orphan.assessment.verification_plans.push({ id: "unused-plan", contract_id: "cache", state_transition: "fresh-miss", oracle: "unused", steps_ref: "unused", source_identity: "test-cache-current" }); orphan.assessment.actual_verifications = []; + assert.ok(codes(validate({ phase: "pre_build", ...orphan })).includes("VERIFICATION_PLAN_ORPHANED")); + const actual = validDocuments(); actual.assessment.actual_verifications.push({ verification_id: "unconsumed", outcome: "passed", executed_snapshot: clone(actual.assessment.snapshot), executed_source_identity: "anything", evidence_ref: "anything" }); + assert.ok(codes(validate({ phase: "completion", ...actual })).includes("ACTUAL_VERIFICATION_ORPHANED")); +}); + +test("a verification plan cannot be reused by two disjoint equivalence groups", () => { + const documents = validDocuments(); + for (const [suffix, consumer] of [["a", "viewer"], ["b", "dialog"]]) { + const edge = { id: `edge-nav-${suffix}`, consumer_id: consumer, contract_id: "navigation", dependency_kind: "call", presence: "both", source_id: `${consumer}-current` }; + const requirement = { id: `req-nav-${suffix}`, edge_id: edge.id, contract_id: "navigation", state_transition: "route-success", source_id: edge.source_id, verification_source_id: "test-navigation-current" }; + documents.capture.edges.push(edge); + const { verification_source_id, ...capturedRequirement } = requirement; + documents.capture.requirements.push(capturedRequirement); + documents.expected.required_edges.push(clone(edge)); + documents.expected.required_requirements.push(requirement); + } + const next = documents.assessment.obligations.length + 1; + documents.assessment.obligations.push( + { id: `obligation-${next}`, requirement_id: "req-nav-a", disposition: "preserve", rationale_ref: "rationale-req-nav-a", authorization_ref: null, verification_id: null, equivalence_group_id: "group-navigation-second" }, + { id: `obligation-${next + 1}`, requirement_id: "req-nav-b", disposition: "preserve", rationale_ref: "rationale-req-nav-b", authorization_ref: null, verification_id: null, equivalence_group_id: "group-navigation-second" }, + ); + documents.assessment.equivalence_groups.push({ id: "group-navigation-second", member_obligation_ids: [`obligation-${next}`, `obligation-${next + 1}`], justification_ref: "same-route-contract-second", verification_id: "plan-nav" }); + documents.expected.review_context.required_bindings.push( + { requirement_id: "req-nav-a", scope_item_id: `scope-${next}`, coverage_concern_id: `concern-${next}` }, + { requirement_id: "req-nav-b", scope_item_id: `scope-${next + 1}`, coverage_concern_id: `concern-${next + 1}` }, + ); + documents.review.bindings.push( + { obligation_id: `obligation-${next}`, requirement_id: "req-nav-a", scope_item_id: `scope-${next}`, coverage_concern_id: `concern-${next}` }, + { obligation_id: `obligation-${next + 1}`, requirement_id: "req-nav-b", scope_item_id: `scope-${next + 1}`, coverage_concern_id: `concern-${next + 1}` }, + ); + assert.ok(codes(validate({ phase: "pre_build", ...{ ...documents, assessment: { ...documents.assessment, actual_verifications: [] } } })).includes("EQUIVALENCE_VERIFICATION_REUSE_REQUIRES_SEPARATE_PLANS")); + assert.ok(codes(validate({ phase: "completion", ...documents })).includes("EQUIVALENCE_VERIFICATION_REUSE_REQUIRES_SEPARATE_PLANS")); +}); + +test("scalar and null array members never throw and return structural reasons", () => { + for (const mutate of [ + (documents) => { documents.capture.edges = [null]; }, + (documents) => { documents.expected.required_edges = [null]; }, + (documents) => { documents.assessment.equivalence_groups = [null]; }, + (documents) => { documents.assessment.actual_verifications = [null]; }, + ]) { + const documents = validDocuments(); mutate(documents); + assert.doesNotThrow(() => validate({ phase: "completion", ...documents })); + assert.equal(validate({ phase: "completion", ...documents }).valid, false); + } +}); + +test("duplicate and dangling identities fail with closure reasons", () => { + const duplicate = validDocuments(); + duplicate.assessment.obligations.push({ ...duplicate.assessment.obligations[0], id: "obligation-duplicate" }); + assert.ok(codes(validate({ phase: "pre_build", ...duplicate })).includes("OBLIGATION_REQUIREMENT_DUPLICATE")); + const dangling = validDocuments(); dangling.assessment.obligations[3].verification_id = "not-a-plan"; + assert.ok(codes(validate({ phase: "pre_build", ...dangling })).includes("VERIFICATION_PLAN_DANGLING")); +}); + +test("cosmetic local control remains proportionate", () => { + const documents = validDocuments(); + documents.capture.edges = []; documents.capture.requirements = []; documents.expected.required_edges = []; documents.expected.required_requirements = []; + documents.expected.required_impact_scope = "not_applicable"; documents.expected.review_context.required_bindings = []; + documents.assessment.impact_scope = "not_applicable"; documents.assessment.assessment_kind = "cosmetic"; documents.assessment.obligations = []; documents.assessment.verification_plans = []; documents.assessment.equivalence_groups = []; documents.assessment.actual_verifications = []; + documents.review.bindings = []; + assert.equal(validate({ phase: "completion", ...documents }).valid, true); +}); + +test("CLI rejects malformed, oversized, nested, and command-looking input without executing it or echoing it", () => { + const documents = validDocuments(); + const clean = runCli(documents, "completion"); + assert.equal(clean.status, 0, JSON.stringify({ stdout: clean.stdout, stderr: clean.stderr, result: clean.result })); assert.equal(clean.result.complete, true); + const marker = path.join(os.tmpdir(), `change-impact-marker-${process.pid}`); + const malicious = validDocuments(); malicious.assessment.verification_plans[0].steps_ref = `$(touch ${marker})`; malicious.assessment.actual_verifications = []; + const noExecution = runCli(malicious, "pre_build"); + assert.equal(noExecution.status, 0); assert.equal(fs.existsSync(marker), false); assert.equal(noExecution.stdout.includes(marker), false); + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "change-impact-bounds-")); + fs.writeFileSync(path.join(directory, "capture.json"), "{".repeat(MAX_BYTES + 1)); + fs.writeFileSync(path.join(directory, "expected.json"), JSON.stringify(validDocuments().expected)); + const oversized = spawnSync(process.execPath, [path.join(process.cwd(), "tools/change-impact/validate-change-impact.cjs"), "--phase", "discovery", "--capture", path.join(directory, "capture.json"), "--expected", path.join(directory, "expected.json")], { encoding: "utf8" }); + assert.equal(oversized.status, 2); assert.ok(oversized.stdout.includes("INPUT_TOO_LARGE")); + fs.writeFileSync(path.join(directory, "capture.json"), "{broken"); + const malformed = spawnSync(process.execPath, [path.join(process.cwd(), "tools/change-impact/validate-change-impact.cjs"), "--phase", "discovery", "--capture", path.join(directory, "capture.json"), "--expected", path.join(directory, "expected.json")], { encoding: "utf8" }); + assert.equal(malformed.status, 2); assert.ok(malformed.stdout.includes("INPUT_MALFORMED")); + const nested = validDocuments(); let cursor = {}; nested.capture.roots = cursor; for (let index = 0; index < 20; index += 1) cursor.next = cursor = {}; + const nestedResult = runCli(nested, "discovery"); + assert.equal(nestedResult.status, 2); assert.ok(nestedResult.stdout.includes("INPUT_UNSAFE_SHAPE")); +}); diff --git a/tests/change-impact/fixtures/independent-fixture-truth/capture.mjs b/tests/change-impact/fixtures/independent-fixture-truth/capture.mjs new file mode 100644 index 00000000..9b56a706 --- /dev/null +++ b/tests/change-impact/fixtures/independent-fixture-truth/capture.mjs @@ -0,0 +1,26 @@ +import { createHash } from "node:crypto"; +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; + +const relevant = (name) => name.endsWith(".mjs") || name.endsWith("-registry.json"); + +async function walk(root, relative = "") { + const entries = await readdir(path.join(root, relative), { withFileTypes: true }); + const paths = []; + for (const entry of entries) { + const next = path.join(relative, entry.name); + if (entry.isDirectory()) paths.push(...await walk(root, next)); + else if (relevant(entry.name)) paths.push(next); + } + return paths.sort(); +} + +export async function capture(root) { + const files = await walk(root); + const entries = await Promise.all(files.map(async (file) => { + const bytes = await readFile(path.join(root, file)); + return { file, sha256: createHash("sha256").update(bytes).digest("hex") }; + })); + const content = entries.map((entry) => `${entry.file}:${entry.sha256}`).join("\n"); + return { identity_kind: "content-sha256/no-git", files: entries, universe_sha256: createHash("sha256").update(content).digest("hex") }; +} diff --git a/tests/change-impact/fixtures/independent-fixture-truth/fixture-authority.json b/tests/change-impact/fixtures/independent-fixture-truth/fixture-authority.json new file mode 100644 index 00000000..bcba7798 --- /dev/null +++ b/tests/change-impact/fixtures/independent-fixture-truth/fixture-authority.json @@ -0,0 +1,13 @@ +{ + "authority_source": "/private/tmp/change-impact-fixture-truth.zkZv1K", + "source_verification": "root rerun 6/6 before S3 adaptation", + "sha256": { + "truth.expected.json": "f2da653a3a75d4c81c8a24d25ff3dca14c72a0929c0b19f5bcc71beef598512f", + "capture.mjs": "676cbc045758aed75d96c83b645d62fb3f08798cc7773e9513209b29a59dc0b1", + "fixture-src/validation.mjs": "8fedc2e00186bb316571236f870e9a26228ab6d4ec6de1e572ff95a2a3bfa90f", + "fixture-src/cache-retry.mjs": "c54119a18d6c36889c1a6518d5682fa500e2ecbca041363a63c1faa820cdad52", + "fixture-src/base-registry.json": "b2eb5ff31ef0d2e86e82fb4e63d294ac2fbba17aa33af3489dbf05a765e8bc03", + "fixture-src/candidate-registry.json": "79ae63aa3a392dba4895178595601b5d87caee24c9604695c847e7fbd1908b60", + "fixture-src/unrelated-dirty-note.txt": "624283c881130264e94331141947aeca858249c047b778ab2dff6a4db97d461b" + } +} diff --git a/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/base-registry.json b/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/base-registry.json new file mode 100644 index 00000000..206cfdef --- /dev/null +++ b/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/base-registry.json @@ -0,0 +1 @@ +{ "registrations": [{ "id": "legacy-json", "consumer": "public-legacy-json-client", "format": "json", "public": true }] } diff --git a/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/cache-retry.mjs b/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/cache-retry.mjs new file mode 100644 index 00000000..ccd597f6 --- /dev/null +++ b/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/cache-retry.mjs @@ -0,0 +1,11 @@ +export function foregroundRead({ stale, cancelled, retryResult = "fresh" }) { + if (!stale) return ["fresh"]; + if (cancelled) return ["stale", "cancelled"]; + return ["stale", "retrying", retryResult]; +} + +export function backgroundRefresh({ stale, cancelled }) { + if (!stale) return ["fresh"]; + if (cancelled) return ["stale", "cancelled"]; + return ["stale", "queued", "fresh"]; +} diff --git a/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/candidate-registry.json b/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/candidate-registry.json new file mode 100644 index 00000000..94c7441a --- /dev/null +++ b/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/candidate-registry.json @@ -0,0 +1 @@ +{ "registrations": [{ "id": "yaml", "consumer": "candidate-yaml-client", "format": "yaml", "public": true }] } diff --git a/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/unrelated-dirty-note.txt b/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/unrelated-dirty-note.txt new file mode 100644 index 00000000..118b8582 --- /dev/null +++ b/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/unrelated-dirty-note.txt @@ -0,0 +1 @@ +This simulates unrelated dirty work and must not change the relevant source capture. diff --git a/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/validation.mjs b/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/validation.mjs new file mode 100644 index 00000000..3668f4fa --- /dev/null +++ b/tests/change-impact/fixtures/independent-fixture-truth/fixture-src/validation.mjs @@ -0,0 +1,21 @@ +export function normalizeOptional(value) { + if (value === null) return null; + return String(value).trim(); +} + +export function formSubmit(value) { + const normalized = normalizeOptional(value); + if (normalized === null) return { state: "untouched" }; + if (normalized === "") return { state: "required-error" }; + return { state: "submitted", value: normalized }; +} + +export function bulkImport(value) { + const normalized = normalizeOptional(value); + if (normalized === null) return { state: "use-default-mapping" }; + if (normalized === "") return { state: "no-column-error" }; + return { state: "map-column", value: normalized }; +} + +export const equivalentFormConsumers = ["form-submit", "form-draft", "form-accessibility"]; +export function equivalentFormConsumer(value) { return formSubmit(value); } diff --git a/tests/change-impact/fixtures/independent-fixture-truth/truth.expected.json b/tests/change-impact/fixtures/independent-fixture-truth/truth.expected.json new file mode 100644 index 00000000..0c09d944 --- /dev/null +++ b/tests/change-impact/fixtures/independent-fixture-truth/truth.expected.json @@ -0,0 +1,42 @@ +{ + "fixture_version": 1, + "cases": [ + { + "id": "local-cosmetic-control", + "scope": "local", + "assessment_kind": "cosmetic", + "expected_consumers": [], + "expected_transitions": [], + "note": "A label-only edit has no behavior obligation." + }, + { + "id": "validation-null-empty-contracts", + "scope": "shared", + "expected_consumers": ["form-submit", "bulk-import"], + "expected_contracts": ["null means untouched/default", "empty string is explicit input"], + "expected_transitions": ["form:null->untouched", "form:empty->required-error", "import:null->use-default-mapping", "import:empty->no-column-error"] + }, + { + "id": "cache-retry-material-state", + "scope": "shared", + "expected_consumers": ["foreground-screen", "background-refresh"], + "expected_contracts": ["foreground exposes stale then retry result", "background schedules stale refresh", "cancellation suppresses retry/error"], + "expected_transitions": ["foreground:stale->retrying->fresh", "foreground:stale->cancelled", "background:stale->queued->fresh", "background:stale->cancelled"] + }, + { + "id": "serialization-config-public-compatibility", + "scope": "shared", + "expected_consumers": ["public-legacy-json-client", "candidate-yaml-client"], + "expected_contracts": ["base-only legacy registration remains a public compatibility obligation", "candidate-only yaml registration is an added configuration obligation"], + "expected_transitions": ["registry:legacy-json->removed-in-candidate", "registry:yaml->added-in-candidate"] + }, + { + "id": "high-fanout-equivalent-control", + "scope": "shared", + "expected_consumers": ["form-submit", "form-draft", "form-accessibility"], + "expected_contracts": ["all three preserve null as untouched and empty as required-error"], + "expected_transitions": ["each:null->untouched", "each:empty->required-error"], + "equivalence_permitted": true + } + ] +} diff --git a/tests/change-impact/fixtures/multidomain.v1.json b/tests/change-impact/fixtures/multidomain.v1.json new file mode 100644 index 00000000..35ddc4bb --- /dev/null +++ b/tests/change-impact/fixtures/multidomain.v1.json @@ -0,0 +1,70 @@ +{ + "capture": { + "schema_version": "change-impact-capture/v1", + "capture_id": "capture-20260911", + "snapshot": { "base_id": "base-a", "candidate_id": "candidate-b", "universe_id": "universe-current" }, + "roots": [ + { "id": "root-navigation", "method": "repository search", "source_id": "src-index-current" }, + { "id": "root-registration", "method": "configuration inventory", "source_id": "config-current" } + ], + "edges": [ + { "id": "edge-nav-editor", "consumer_id": "editor", "contract_id": "navigation", "dependency_kind": "call", "presence": "both", "source_id": "editor-current" }, + { "id": "edge-nav-modal", "consumer_id": "modal", "contract_id": "navigation", "dependency_kind": "wrapper", "presence": "both", "source_id": "modal-current" }, + { "id": "edge-config-old", "consumer_id": "legacy-config", "contract_id": "configuration", "dependency_kind": "config", "presence": "base", "source_id": "config-base" }, + { "id": "edge-event-new", "consumer_id": "event-worker", "contract_id": "async-work", "dependency_kind": "event", "presence": "candidate", "source_id": "event-current" }, + { "id": "edge-state", "consumer_id": "draft-store", "contract_id": "state", "dependency_kind": "state", "presence": "both", "source_id": "state-current" }, + { "id": "edge-public", "consumer_id": "external-client", "contract_id": "public-api", "dependency_kind": "public", "presence": "both", "source_id": "api-current" }, + { "id": "edge-cache", "consumer_id": "cache-layer", "contract_id": "cache", "dependency_kind": "registration", "presence": "both", "source_id": "registration-current" } + ], + "requirements": [ + { "id": "req-nav-success", "edge_id": "edge-nav-editor", "contract_id": "navigation", "state_transition": "route-success", "source_id": "editor-current" }, + { "id": "req-nav-dirty", "edge_id": "edge-nav-editor", "contract_id": "navigation", "state_transition": "dirty-cancel", "source_id": "editor-current" }, + { "id": "req-nav-modal", "edge_id": "edge-nav-modal", "contract_id": "navigation", "state_transition": "route-success", "source_id": "modal-current" }, + { "id": "req-config", "edge_id": "edge-config-old", "contract_id": "configuration", "state_transition": "loads-default", "source_id": "config-base" }, + { "id": "req-event", "edge_id": "edge-event-new", "contract_id": "async-work", "state_transition": "stale-completion", "source_id": "event-current" }, + { "id": "req-state", "edge_id": "edge-state", "contract_id": "state", "state_transition": "restore-draft", "source_id": "state-current" }, + { "id": "req-public", "edge_id": "edge-public", "contract_id": "public-api", "state_transition": "compatibility", "source_id": "api-current" }, + { "id": "req-cache", "edge_id": "edge-cache", "contract_id": "cache", "state_transition": "fresh-miss", "source_id": "registration-current" } + ], + "unknown_boundaries": [] + }, + "expected": { + "schema_version": "change-impact-expected-context/v1", + "authority_id": "independent-current-context-1", + "capture_id": "capture-20260911", + "snapshot": { "base_id": "base-a", "candidate_id": "candidate-b", "universe_id": "universe-current" }, + "required_impact_scope": "shared", + "required_edges": [], + "required_requirements": [], + "review_context": { + "scope_manifest_id": "assistant-review-scope-manifest-7", + "coverage_ledger_id": "assistant-review-coverage-ledger-7", + "review_snapshot_id": "assistant-review-snapshot-7", + "snapshot_id": "review-source-current", + "required_bindings": [] + } + }, + "assessment": { + "schema_version": "change-impact-assessment/v1", + "capture_id": "capture-20260911", + "snapshot": { "base_id": "base-a", "candidate_id": "candidate-b", "universe_id": "universe-current" }, + "impact_scope": "shared", + "assessment_kind": "behavior", + "obligations": [], + "verification_plans": [], + "equivalence_groups": [], + "actual_verifications": [] + }, + "review": { + "schema_version": "change-impact-review/v1", + "capture_id": "capture-20260911", + "snapshot": { "base_id": "base-a", "candidate_id": "candidate-b", "universe_id": "universe-current" }, + "review_source": { + "scope_manifest_id": "assistant-review-scope-manifest-7", + "coverage_ledger_id": "assistant-review-coverage-ledger-7", + "review_snapshot_id": "assistant-review-snapshot-7", + "snapshot_id": "review-source-current" + }, + "bindings": [] + } +} diff --git a/tests/change-impact/independent-fixture-adapter.test.cjs b/tests/change-impact/independent-fixture-adapter.test.cjs new file mode 100644 index 00000000..a78f2a43 --- /dev/null +++ b/tests/change-impact/independent-fixture-adapter.test.cjs @@ -0,0 +1,197 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const fsp = require("node:fs/promises"); +const os = require("node:os"); +const path = require("node:path"); +const { pathToFileURL } = require("node:url"); +const { test } = require("node:test"); +const { validate } = require("../../tools/change-impact/validate-change-impact.cjs"); + +const truthRoot = path.join(__dirname, "fixtures", "independent-fixture-truth"); +const truth = JSON.parse(fs.readFileSync(path.join(truthRoot, "truth.expected.json"), "utf8")); +const fixtureAuthority = JSON.parse(fs.readFileSync(path.join(truthRoot, "fixture-authority.json"), "utf8")); + +function clone(value) { return JSON.parse(JSON.stringify(value)); } +function codes(result) { return result.reasons.map((reason) => reason.code); } +function caseId(value) { return value.id.replace(/[^a-z0-9]+/gi, "-"); } + +function sourceInventory(value) { + const contracts = value.expected_contracts || [value.id]; + const consumers = value.expected_consumers; + const transitions = [...value.expected_transitions]; + while (transitions.length < consumers.length) transitions.push(value.expected_transitions[0]); + const sharedHighFanoutSource = value.id === "high-fanout-equivalent-control" ? `fixture-truth:${value.id}:equivalent` : null; + const edges = consumers.map((consumer, index) => ({ + id: `edge-${caseId(value)}-${index + 1}`, + consumer_id: consumer, + contract_id: contracts[index % contracts.length], + dependency_kind: value.id === "serialization-config-public-compatibility" ? (index === 0 ? "public" : "config") : + value.id === "cache-retry-material-state" ? "state" : value.id === "high-fanout-equivalent-control" ? "wrapper" : "call", + presence: value.id === "serialization-config-public-compatibility" ? (index === 0 ? "base" : "candidate") : "both", + source_id: `fixture-source:${value.id}:${consumer}`, + })); + const requirements = transitions.map((transition, index) => { + const edge = edges[index % edges.length]; + return { + id: `requirement-${caseId(value)}-${index + 1}`, + edge_id: edge.id, + contract_id: edge.contract_id, + state_transition: transition, + source_id: `fixture-source:${value.id}:${transition}`, + verification_source_id: sharedHighFanoutSource && transition === value.expected_transitions[0] ? sharedHighFanoutSource : `fixture-truth:${value.id}:${index + 1}`, + }; + }); + return { edges, requirements }; +} + +function documentsFromTruth(value) { + const inventory = sourceInventory(value); + const snapshot = { base_id: `base:${value.id}`, candidate_id: `candidate:${value.id}`, universe_id: `content-sha256/no-git:${value.id}` }; + const capture = { + schema_version: "change-impact-capture/v1", + capture_id: `capture:${value.id}`, + snapshot: clone(snapshot), + roots: value.scope === "shared" ? [{ id: `root:${value.id}`, method: "fixture-source", source_id: `fixture-root:${value.id}` }] : [], + edges: inventory.edges.map(({ verification_source_id, ...edge }) => edge), + requirements: inventory.requirements.map(({ verification_source_id, ...requirement }) => requirement), + unknown_boundaries: [], + }; + const expected = { + schema_version: "change-impact-expected-context/v1", + authority_id: `independent-fixture-truth:${value.id}`, + capture_id: capture.capture_id, + snapshot: clone(snapshot), + required_impact_scope: value.scope === "shared" ? "shared" : "not_applicable", + required_edges: clone(capture.edges), + required_requirements: clone(inventory.requirements), + review_context: null, + }; + if (value.assessment_kind === "cosmetic") return { capture, expected }; + + const groupRequirementIds = value.id === "high-fanout-equivalent-control" ? + inventory.requirements.filter((requirement) => requirement.verification_source_id.endsWith(":equivalent")).map((requirement) => requirement.id) : []; + const groupVerificationId = groupRequirementIds.length > 0 ? `verification:${value.id}:equivalent` : null; + const verificationPlans = []; + const planForRequirement = new Map(); + for (const requirement of inventory.requirements) { + const verificationId = groupRequirementIds.includes(requirement.id) ? groupVerificationId : `verification:${requirement.id}`; + planForRequirement.set(requirement.id, verificationId); + if (!verificationPlans.some((plan) => plan.id === verificationId)) { + verificationPlans.push({ id: verificationId, contract_id: requirement.contract_id, state_transition: requirement.state_transition, + oracle: `fixture-oracle:${requirement.id}`, steps_ref: `fixture-steps:${requirement.id}`, source_identity: requirement.verification_source_id }); + } + } + const obligations = inventory.requirements.map((requirement, index) => ({ + id: `obligation:${value.id}:${index + 1}`, + requirement_id: requirement.id, + disposition: "preserve", + rationale_ref: `fixture-rationale:${requirement.id}`, + authorization_ref: null, + verification_id: groupRequirementIds.includes(requirement.id) ? null : planForRequirement.get(requirement.id), + equivalence_group_id: groupRequirementIds.includes(requirement.id) ? `equivalence:${value.id}` : null, + })); + const reviewBindings = inventory.requirements.map((requirement, index) => ({ + requirement_id: requirement.id, scope_item_id: `scope:${value.id}:${index + 1}`, coverage_concern_id: `coverage:${value.id}:${index + 1}`, + })); + expected.review_context = { + scope_manifest_id: `scope-manifest:${value.id}`, + coverage_ledger_id: `coverage-ledger:${value.id}`, + review_snapshot_id: `review-snapshot:${value.id}`, + snapshot_id: `snapshot:${value.id}`, + required_bindings: reviewBindings, + }; + const assessment = { + schema_version: "change-impact-assessment/v1", + capture_id: capture.capture_id, + snapshot: clone(snapshot), + impact_scope: "shared", + assessment_kind: "behavior", + obligations, + verification_plans: verificationPlans, + equivalence_groups: groupVerificationId ? [{ id: `equivalence:${value.id}`, member_obligation_ids: obligations.filter((obligation) => obligation.equivalence_group_id).map((obligation) => obligation.id), justification_ref: `fixture-equivalence:${value.id}`, verification_id: groupVerificationId }] : [], + actual_verifications: verificationPlans.map((plan) => ({ verification_id: plan.id, outcome: "passed", executed_snapshot: clone(snapshot), executed_source_identity: plan.source_identity, evidence_ref: `fixture-evidence:${plan.id}` })), + }; + const review = { + schema_version: "change-impact-review/v1", + capture_id: capture.capture_id, + snapshot: clone(snapshot), + review_source: { + scope_manifest_id: expected.review_context.scope_manifest_id, + coverage_ledger_id: expected.review_context.coverage_ledger_id, + review_snapshot_id: expected.review_context.review_snapshot_id, + snapshot_id: expected.review_context.snapshot_id, + }, + bindings: obligations.map((obligation, index) => ({ obligation_id: obligation.id, requirement_id: obligation.requirement_id, + scope_item_id: reviewBindings[index].scope_item_id, coverage_concern_id: reviewBindings[index].coverage_concern_id })), + }; + return { capture, expected, assessment, review }; +} + +test("frozen independent fixture truth executes its domain behavior before protocol adaptation", async () => { + const validation = await import(pathToFileURL(path.join(truthRoot, "fixture-src", "validation.mjs")).href); + const retry = await import(pathToFileURL(path.join(truthRoot, "fixture-src", "cache-retry.mjs")).href); + const base = JSON.parse(await fsp.readFile(path.join(truthRoot, "fixture-src", "base-registry.json"), "utf8")); + const candidate = JSON.parse(await fsp.readFile(path.join(truthRoot, "fixture-src", "candidate-registry.json"), "utf8")); + assert.deepEqual(validation.formSubmit(null), { state: "untouched" }); + assert.deepEqual(validation.formSubmit(" "), { state: "required-error" }); + assert.deepEqual(validation.bulkImport(null), { state: "use-default-mapping" }); + assert.deepEqual(validation.bulkImport(" "), { state: "no-column-error" }); + assert.deepEqual(retry.foregroundRead({ stale: true, cancelled: false }), ["stale", "retrying", "fresh"]); + assert.deepEqual(retry.foregroundRead({ stale: true, cancelled: true }), ["stale", "cancelled"]); + assert.deepEqual(retry.backgroundRefresh({ stale: true, cancelled: false }), ["stale", "queued", "fresh"]); + assert.deepEqual(retry.backgroundRefresh({ stale: true, cancelled: true }), ["stale", "cancelled"]); + assert.deepEqual(base.registrations.map((entry) => entry.id), ["legacy-json"]); + assert.deepEqual(candidate.registrations.map((entry) => entry.id), ["yaml"]); + assert.deepEqual(validation.equivalentFormConsumers, ["form-submit", "form-draft", "form-accessibility"]); + for (const consumer of validation.equivalentFormConsumers) assert.deepEqual(validation.equivalentFormConsumer(""), { state: "required-error" }, consumer); +}); + +test("adapted independent truth retains the frozen source authority hashes", () => { + for (const [relativePath, expectedHash] of Object.entries(fixtureAuthority.sha256)) { + const actualHash = crypto.createHash("sha256").update(fs.readFileSync(path.join(truthRoot, relativePath))).digest("hex"); + assert.equal(actualHash, expectedHash, relativePath); + } +}); + +test("independent fixture authority adapts known consumers and transitions without accepting assessment omissions", () => { + for (const value of truth.cases) { + const documents = documentsFromTruth(value); + assert.equal(documents.expected.required_edges.length, value.expected_consumers.length, value.id); + assert.ok(value.expected_transitions.every((transition) => documents.expected.required_requirements.some((requirement) => requirement.state_transition === transition)), value.id); + if (value.assessment_kind === "cosmetic") { + assert.equal(validate({ phase: "discovery", ...documents }).valid, true, value.id); + continue; + } + assert.equal(validate({ phase: "completion", ...documents }).complete, true, value.id); + const missingConsumer = clone(documents); + const omittedEdge = missingConsumer.capture.edges.pop(); + missingConsumer.capture.requirements = missingConsumer.capture.requirements.filter((requirement) => requirement.edge_id !== omittedEdge.id); + assert.ok(codes(validate({ phase: "completion", ...missingConsumer })).includes("CAPTURE_EXPECTED_EDGES_MISMATCH"), `${value.id} consumer omission`); + const missingTransition = clone(documents); + missingTransition.assessment.obligations.pop(); + assert.ok(codes(validate({ phase: "pre_build", ...missingTransition })).includes("CAPTURE_REQUIREMENT_OBLIGATION_MISSING"), `${value.id} transition omission`); + } +}); + +test("independent no-git content capture ignores unrelated dirt and invalidates current relevant content", async () => { + const { capture } = await import(pathToFileURL(path.join(truthRoot, "capture.mjs")).href); + const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "change-impact-independent-truth-")); + const source = path.join(tempRoot, "fixture-src"); + try { + await fsp.cp(path.join(truthRoot, "fixture-src"), source, { recursive: true }); + const initial = await capture(source); + await fsp.writeFile(path.join(source, "unrelated-dirty-note.txt"), "unrelated dirty work\n"); + assert.deepEqual(await capture(source), initial); + await fsp.writeFile(path.join(source, "new-relevant-consumer.mjs"), "export const consumer = 'new';\n"); + const afterAddedRelevant = await capture(source); + assert.notEqual(afterAddedRelevant.universe_sha256, initial.universe_sha256); + await fsp.appendFile(path.join(source, "validation.mjs"), "\n// relevant mutation\n"); + assert.notEqual((await capture(source)).universe_sha256, afterAddedRelevant.universe_sha256); + assert.equal(initial.identity_kind, "content-sha256/no-git"); + } finally { + await fsp.rm(tempRoot, { recursive: true, force: true }); + } +}); diff --git a/tests/p0-p4/assistant-review-reference-contracts.sh b/tests/p0-p4/assistant-review-reference-contracts.sh index adfda576..e1e5acce 100644 --- a/tests/p0-p4/assistant-review-reference-contracts.sh +++ b/tests/p0-p4/assistant-review-reference-contracts.sh @@ -277,7 +277,7 @@ if ! ruby -ryaml -e ' reviewer_challenge = reviewer_checks.fetch("object_fields").find { |field| field["name"] == "independent_challenge_evidence" } output_pack = output.fetch("artifacts").find { |artifact| artifact["name"] == "architecture_decision_pack_review" } output_challenge = output_pack.fetch("object_fields").find { |field| field["name"] == "independent_challenge_evidence" } - valid = input["schema_version"] == "7.1" && handoffs["schema_version"] == "7.1" && output["schema_version"] == "7.1" && index["schema_version"] == "7.1" && + valid = input["schema_version"] == "7.2" && handoffs["schema_version"] == "7.2" && output["schema_version"] == "7.2" && index["schema_version"] == "7.2" && canonical_mode["required"] == "conditional" && canonical_mode["condition"] == "architecture_decision_pack_review_required is true" && canonical_mode["enum_values"] == %w[lightweight required review_intensive] && canonical_mode["on_missing"] == "infer" && @@ -355,7 +355,7 @@ elif ! grep -Fq $'FAIL\tassistant-review\tstandalone-high-risk-record-without-ch architecture_pack_mode_consumer_failures+=("adversarial standalone ADR grader response did not fail structured assertions") fi for file_and_term in \ - "$review_skill::Migration note: assistant-review contracts are v7" \ + "$review_skill::Migration note: assistant-review contracts are v7.2" \ "$review_phase_gates::architecture_design_mode" \ "$review_handoffs::architecture_decision_pack.mode must equal canonical architecture_design_mode"; do file="${file_and_term%%::*}" @@ -588,7 +588,7 @@ if ! ruby -ryaml -e ' pressure = fields.fetch("design_pressure_checks") required_concerns = %w[control_and_early_exit ownership_and_disposal resource_envelope extension_registration representative_path] ref = fields.fetch("ref") - valid = contracts.all? { |contract| contract.fetch("schema_version") == "7.1" } && + valid = contracts.all? { |contract| contract.fetch("schema_version") == "7.2" } && boundaries["required"] == true && boundaries["min_items"] == 1 && pressure["required"] == true && pressure["min_items"] == 5 && pressure["max_items"] == 5 && ref["required"] == true && ref.fetch("validation").include?("selected design") && ref.fetch("validation").include?("rationale") && ref.fetch("validation").include?("viable alternatives") && @@ -597,7 +597,7 @@ if ! ruby -ryaml -e ' ' "$review_input" "$review_output" "$review_phase_gates" "$review_handoffs" "$review_index"; then pack_projection_cardinality_failures+=("v7 input does not preserve recoverable selected design, rationale, viable alternative dispositions, non-empty boundaries, and exact five-concern pressure coverage") fi -if ! grep -Fq 'Migration note: assistant-review contracts are v7' "$review_skill" \ +if ! grep -Fq 'Migration note: assistant-review contracts are v7.2' "$review_skill" \ || ! grep -Fq 'recoverable selected design, rationale, and viable alternatives/dispositions' "$review_skill"; then pack_projection_cardinality_failures+=("v7 migration note does not describe recoverable selected design evidence") fi @@ -908,6 +908,13 @@ test_start "assistant-review models batch lifecycle, overlapping manifest covera if ruby -ryaml -rjson -e ' input, handoffs, output, gates = ARGV.take(4).map { |path| YAML.load_file(path) } evals = JSON.parse(File.read(ARGV.fetch(4))) + root = File.read(ARGV.fetch(5)).split("## Success Criteria", 2).last.split("## Constraints", 2).first.gsub(/\s+/, " ") + abort "root requires completed passes before an incomplete audit can exit" if root.include?("at least two COMPLETED passes") + %w[failed timed-out blocked].each do |state| + abort "root omits terminally incomplete #{state} audit path" unless root.include?(state) + end + abort "root loses planned pass minimum or terminal barrier" unless root.include?("plans and attempts at least two independent passes") && root.include?("All expected passes reach terminal accounting before aggregation, fix, or exit") + abort "root loses clean coverage or bounded audit exit" unless root.include?("`CLEAN`/`ISSUES_FIXED` require complete coverage") && root.include?("Audit exits after one started batch") && root.include?("`HAS_REMAINING_ITEMS`") reviewer = handoffs.fetch("handoffs").find { |entry| entry["name"] == "orchestrator_to_reviewer" } context = reviewer.fetch("context_fields").to_h { |field| [field["name"], field] } batch = context.fetch("review_batch").fetch("object_fields").to_h { |field| [field["name"], field] } @@ -960,7 +967,7 @@ if ruby -ryaml -rjson -e ' gates.fetch("gates").find { |item| item["phase"] == "EVALUATE_STEP" }.fetch("exit_assertions").to_h { |item| [item["id"], item] }.fetch("EV2").fetch("check").include?("needs_context") && audit_eval.fetch("expected_behavior").join(" ").include?("read-only Reviewer pass") && later_eval.fetch("expected_behavior").join(" ").include?("later runtime finding") && mutation_eval.fetch("expected_behavior").join(" ").include?("HAS_REMAINING_ITEMS") exit valid ? 0 : 1 -' "$review_input" "$review_handoffs" "$FRAMEWORK_DIR/skills/assistant-review/contracts/output.yaml" "$review_phase_gates" "$review_evals"; then +' "$review_input" "$review_handoffs" "$FRAMEWORK_DIR/skills/assistant-review/contracts/output.yaml" "$review_phase_gates" "$review_evals" "$review_skill"; then pass else fail "assistant-review lacks lifecycle-safe terminal accounting, overlapping manifest tuple closure, or sibling-blind pre-batch provenance" @@ -1093,8 +1100,8 @@ else fi test_start "assistant-review v7 migration wording preserves source and target direction" -if grep -Fq 'Rebuild persisted 6.0 or incompatible 7.0 under 7.1 from a fresh snapshot' "$FRAMEWORK_DIR/skills/assistant-review/references/review-batch.md" \ - && ! grep -Fq 'Rebuild 7.1 from' "$FRAMEWORK_DIR/skills/assistant-review/references/review-batch.md"; then +if grep -Fq 'Rebuild persisted 6.0 or incompatible 7.0/7.1 under 7.2 from a fresh snapshot' "$FRAMEWORK_DIR/skills/assistant-review/references/review-batch.md" \ + && ! grep -Fq 'Rebuild 7.2 from' "$FRAMEWORK_DIR/skills/assistant-review/references/review-batch.md"; then pass else fail "assistant-review review-batch migration wording reverses the v6-to-v7 rebuild direction" @@ -1176,9 +1183,9 @@ else fail "assistant-review lacks active-attempt, typed-QA, provenance, or incomplete-audit safeguards" fi -test_start "assistant-review v7 invalidates real v6 packets with Unicode-safe collision IDs" +test_start "assistant-review invalidates real legacy and immediate-prior-minor packets with Unicode-safe v6 IDs" if ruby -ryaml -e ' - input, handoffs, output = ARGV.map { |path| YAML.load_file(path) } + input, handoffs, output = ARGV.take(3).map { |path| YAML.load_file(path) } reviewer = handoffs.fetch("handoffs").find { |entry| entry["name"] == "orchestrator_to_reviewer" } bundle = handoffs.fetch("dispatch_context_bundles").find { |entry| entry["name"] == "fresh_reviewer_context" } require "digest" @@ -1203,29 +1210,38 @@ if ruby -ryaml -e ' current_schema_version = input.fetch("schema_version") migration = {"source_schema_version" => legacy_packet.fetch("schema_version"), "batch_disposition" => "invalidated_rebuild_required", "rebuilt_review_snapshot_id" => "fresh-v#{current_schema_version}-snapshot"} legacy_v7_packet = {"schema_version" => "7.0", "final_summary" => {"result" => "CLEAN"}} - can_claim_clean = ->(packet, migration_state) { packet.fetch("schema_version") == "7.1" && migration_state.fetch("batch_disposition") != "invalidated_rebuild_required" } + major, minor = current_schema_version.split(".").map(&:to_i) + immediate_prior_schema_version = "#{major}.#{minor - 1}" + immediate_prior_history = [{"description" => "retained closure", "fixed_in_round" => 1, "aggregate_finding_id" => "aggregate-v7-1", "source_finding_ids" => ["review-pass-v7-1"], "source_provenance" => [{"source_kind" => "review_pass", "source_id" => "review-pass-v7-1"}]}] + immediate_prior_packet = {"schema_version" => immediate_prior_schema_version, "final_summary" => {"result" => "CLEAN"}, "previously_fixed" => immediate_prior_history, "canonical_result_ref" => "legacy-result", "delegation_path_ref" => "legacy-delegation", "final_snapshot_identity_ref" => "legacy-snapshot", "architecture_decision_pack_review_ref" => "legacy-pack", "qa_evaluation_result_ref" => "legacy-qa", "final_batch_plan_ref" => "legacy-plan", "coverage_ledger_ref" => "legacy-coverage"} + can_claim_clean = ->(packet, migration_state) { packet.fetch("schema_version") == "7.2" && migration_state.fetch("batch_disposition") != "invalidated_rebuild_required" } previously_fixed = input.fetch("fields").find { |field| field["name"] == "previously_fixed" }.fetch("object_fields").to_h { |field| [field["name"], field] } migration_contract = input.fetch("fields").find { |field| field["name"] == "persisted_v6_packet_migration" } v7_invalidation = input.fetch("fields").find { |field| field["name"] == "persisted_v7_0_packet_invalidation" } + immediate_prior_invalidation = input.fetch("fields").find { |field| field["name"] == "persisted_v#{immediate_prior_schema_version.tr(".", "_")}_packet_invalidation" } + review_loop = File.read(ARGV.fetch(3)) pass_fields = reviewer.fetch("context_fields").map { |field| field.fetch("name") } final = output.fetch("artifacts").find { |item| item["name"] == "final_summary" }.fetch("object_fields").to_h { |field| [field["name"], field] } valid = legacy_packet.fetch("schema_version") == "6.0" && legacy_packet.dig("final_summary", "result") == "CLEAN" && migrated_ids[0] == base.call(legacy_packet.fetch("previously_fixed")[0]) && migrated_ids[1].end_with?(":1") && migrated_ids[2].end_with?(":2") && migrated_ids[1] != migrated_ids[2] && - migration.fetch("source_schema_version") == "6.0" && migration.fetch("batch_disposition") == "invalidated_rebuild_required" && !can_claim_clean.call(legacy_packet, migration) && !can_claim_clean.call(legacy_v7_packet, {"batch_disposition" => "current"}) && + migration.fetch("source_schema_version") == "6.0" && migration.fetch("batch_disposition") == "invalidated_rebuild_required" && !can_claim_clean.call(legacy_packet, migration) && !can_claim_clean.call(legacy_v7_packet, {"batch_disposition" => "current"}) && !can_claim_clean.call(immediate_prior_packet, {"batch_disposition" => "current"}) && previously_fixed.fetch("aggregate_finding_id").fetch("condition") == "entry was created under producer schema 7.1 or later" && migration_contract.fetch("description").include?(current_schema_version) && migration_contract.fetch("validation").include?("freshly rehashed #{current_schema_version} snapshot") && migration_contract.fetch("object_fields").find { |field| field["name"] == "rebuilt_snapshot_identity" }.fetch("description").include?(current_schema_version) && migration_contract.fetch("validation").include?("UTF-8 NFC") && migration_contract.fetch("validation").include?("SHA-256") && migration_contract.fetch("validation").downcase.include?("invalidate") && migration_contract.fetch("validation").downcase.include?("non-colliding records use") && migration_contract.fetch("validation").include?("every colliding record appends") && - v7_invalidation.fetch("condition").include?("7.0") && v7_invalidation.fetch("validation").include?("do not reinterpret") && v7_invalidation.fetch("validation").include?("7.1") && + v7_invalidation.fetch("condition").include?("7.0") && v7_invalidation.fetch("validation").include?("do not reinterpret") && v7_invalidation.fetch("validation").include?("7.2") && + immediate_prior_invalidation.fetch("condition").include?(immediate_prior_schema_version) && immediate_prior_invalidation.fetch("object_fields").find { |field| field["name"] == "source_schema_version" }.fetch("enum_values") == [immediate_prior_schema_version] && immediate_prior_invalidation.fetch("validation").include?("do not reinterpret") && immediate_prior_invalidation.fetch("validation").include?("7.1-or-later") && + immediate_prior_packet.fetch("previously_fixed") == immediate_prior_history && immediate_prior_history.all? { |entry| entry.fetch("aggregate_finding_id").start_with?("aggregate-") && entry.fetch("source_finding_ids").length == 1 && entry.fetch("source_provenance").length == 1 } && + %w[canonical_result_ref delegation_path_ref final_snapshot_identity_ref architecture_decision_pack_review_ref qa_evaluation_result_ref final_batch_plan_ref coverage_ledger_ref].all? { |ref| immediate_prior_packet.key?(ref) } && review_loop.include?("previously_fixed = validated_entry.previously_fixed || []") && review_loop.include?("invalidating current result/delegation/snapshot/Pack/QA/plan/coverage refs") && review_loop.include?("retain every aggregate_finding_id, source_finding_ids, and source_provenance entry") && bundle.fetch("context_fields_from_dispatch").include?("review_focus") && pass_fields.include?("review_focus") && bundle.fetch("review_evidence_pointer").fetch("required_refs").include?("review_material_snapshot") && !bundle.fetch("review_evidence_pointer").fetch("required_refs").include?("review_material_snapshot_ref") && final.fetch("final_review_snapshot_id").fetch("validation").downcase.include?("exactly equals current review_snapshot_id") exit valid ? 0 : 1 -' "$review_input" "$review_handoffs" "$FRAMEWORK_DIR/skills/assistant-review/contracts/output.yaml"; then +' "$review_input" "$review_handoffs" "$FRAMEWORK_DIR/skills/assistant-review/contracts/output.yaml" "$review_loop"; then pass else - fail "assistant-review lacks v7 invalidate/rebuild migration, Unicode-safe legacy IDs, pass-only focus, or final identity binding" + fail "assistant-review lacks immediate-prior invalidate/rebuild migration, Unicode-safe legacy IDs, pass-only focus, or final identity binding" fi test_start "assistant-review enforces orchestrator terminals, response events, and exact batch closure" @@ -1321,7 +1337,7 @@ if ruby -ryaml -e ' response_terminal = ->(events) { events.count { |event| event["disposition"] == "active_terminal" } == 1 } synthetic_failure = ->(events, kind, evidence) { events.none? { |event| event["disposition"] == "active_terminal" } && %w[timeout transport schema_invalid source_mutation].include?(kind) && !evidence.empty? } truth = returns.fetch("status").fetch("validation") - valid = [input, output, handoffs, gates].all? { |schema| schema.fetch("schema_version") == "7.1" } && + valid = [input, output, handoffs, gates].all? { |schema| schema.fetch("schema_version") == "7.2" } && identity == final_identity && final_identity.find { |field| field["name"] == "basis" }.fetch("type") == "enum" && final.fetch("final_review_snapshot_id").fetch("validation").downcase.include?("exactly equals current review_snapshot_id") && manifest.fetch("scope_item_id").fetch("validation").downcase.include?("unique") && manifest.fetch("applicable_concerns").fetch("validation").downcase.include?("unique") && @@ -1339,7 +1355,7 @@ if ruby -ryaml -e ' qa_return.fetch("approved_feature_preparation_qa_acceptance_obligation_result").fetch("validation").include?("accepted_with_concerns") && qa_gates.fetch("QA5").fetch("check").include?("accepted_with_concerns/ISSUES_FIXED") && qa_gates.fetch("QA5").fetch("check").include?("requested_scope_status=fulfilled") && - handoffs.fetch("worker_status_protocol").fetch("packet_rules").join(" ").include?("6.0") && handoffs.fetch("worker_status_protocol").fetch("packet_rules").join(" ").include?("7.0") && + handoffs.fetch("worker_status_protocol").fetch("packet_rules").join(" ").include?("6.0") && handoffs.fetch("worker_status_protocol").fetch("packet_rules").join(" ").include?("7.0") && handoffs.fetch("worker_status_protocol").fetch("packet_rules").join(" ").include?("7.1") && handoffs.fetch("worker_status_protocol").fetch("packet_rules").join(" ").include?("UTF-8 NFC") && handoffs.fetch("worker_status_protocol").fetch("packet_rules").join(" ").include?("SHA-256") && batch.fetch("expected_passes").fetch("validation").include?("assistant-security checklist/perspective") && review_gates.fetch("RS2").fetch("check").include?("assistant-security checklist/perspective") @@ -1526,7 +1542,7 @@ if ruby -ryaml -e ' "coordinated_invalid_terminal" => ->(trial) { trial["review_batch"]["pass_attempt_ledger"][0]["attempts"][0]["terminal_state"] = "blocked"; trial["reviewer_returns"][0]["status"] = "BLOCKED"; trial["reviewer_returns"][0]["pass_completion_state"] = "blocked"; trial["reviewer_returns"][0]["verdict"] = "not_assessed"; trial["reviewer_returns"][0]["coverage_entries"] = []; trial["reviewer_returns"][0]["open_questions"] = ["blocked"] } } mutation_valid = mutations.all? { |_name, mutate| trial = duplicate.call(packet); mutate.call(trial); !packet_valid.call(trial) } - valid = input.fetch("schema_version") == "7.1" && handoffs.fetch("schema_version") == "7.1" && output.fetch("schema_version") == "7.1" && packet_valid.call(packet) && truth_valid && mutation_valid + valid = input.fetch("schema_version") == "7.2" && handoffs.fetch("schema_version") == "7.2" && output.fetch("schema_version") == "7.2" && packet_valid.call(packet) && truth_valid && mutation_valid exit valid ? 0 : 1 ' "$review_input" "$review_handoffs" "$FRAMEWORK_DIR/skills/assistant-review/contracts/output.yaml"; then pass @@ -1653,13 +1669,21 @@ fi test_start "assistant-review preserves discovery outcomes, closure identities, and final planned topology" if ruby -ryaml -rjson -e ' - input, output, handoffs, index, cases, gates = ARGV.then { |paths| [YAML.load_file(paths[0]), YAML.load_file(paths[1]), YAML.load_file(paths[2]), YAML.load_file(paths[3]), JSON.parse(File.read(paths[4])), YAML.load_file(paths[5])] } + input, output, handoffs, index, cases, gates, review_loop = ARGV.then { |paths| [YAML.load_file(paths[0]), YAML.load_file(paths[1]), YAML.load_file(paths[2]), YAML.load_file(paths[3]), JSON.parse(File.read(paths[4])), YAML.load_file(paths[5]), File.read(paths[6])] } input_fields = input.fetch("fields").to_h { |field| [field.fetch("name"), field] } previous = input_fields.fetch("previously_fixed").fetch("object_fields").to_h { |field| [field.fetch("name"), field] } canonical_identity = input_fields.fetch("review_material_snapshot").fetch("object_fields").find { |field| field.fetch("name") == "snapshot_identity" }.fetch("object_fields") migration_identity = input_fields.fetch("persisted_v6_packet_migration").fetch("object_fields").find { |field| field.fetch("name") == "rebuilt_snapshot_identity" }.fetch("object_fields") + v6_migration = input_fields.fetch("persisted_v6_packet_migration") + v7_0_invalidation = input_fields.fetch("persisted_v7_0_packet_invalidation") invalidation_identity = input_fields.fetch("persisted_v7_0_packet_invalidation").fetch("object_fields").find { |field| field.fetch("name") == "rebuilt_snapshot_identity" }.fetch("object_fields") + major, minor = input.fetch("schema_version").split(".").map(&:to_i) + immediate_prior = "#{major}.#{minor - 1}" + immediate_prior_name = "persisted_v#{immediate_prior.tr(".", "_")}_packet_invalidation" + immediate_prior_invalidation = input_fields.fetch(immediate_prior_name) + immediate_prior_invalidation_identity = input_fields.fetch(immediate_prior_name).fetch("object_fields").find { |field| field.fetch("name") == "rebuilt_snapshot_identity" }.fetch("object_fields") f3 = gates.fetch("gates").flat_map { |phase| phase.fetch("exit_assertions", []) }.find { |gate| gate.fetch("id") == "F3" } + e10 = gates.fetch("gates").flat_map { |phase| phase.fetch("exit_assertions", []) }.find { |gate| gate.fetch("id") == "E10" } reviewer = handoffs.fetch("handoffs").find { |entry| entry.fetch("name") == "orchestrator_to_reviewer" } reviewer_context = reviewer.fetch("context_fields").to_h { |field| [field.fetch("name"), field] } handoff_previous = reviewer_context.fetch("previously_fixed").fetch("object_fields").to_h { |field| [field.fetch("name"), field] } @@ -1688,6 +1712,71 @@ if ruby -ryaml -rjson -e ' expected_names = %w[review_pass_id perspective assigned_scope coverage_obligations prior_finding_visibility] fixed_fields = final.fetch("fixed_items").fetch("object_fields").to_h { |field| [field.fetch("name"), field] } closure_results = final.fetch("closure_results") + rubric = output.fetch("artifacts").find { |artifact| artifact.fetch("name") == "rubric_summary" } + score_progression = rubric.fetch("object_fields").find { |field| field.fetch("name") == "score_progression" } + source_text = { + input_default: input_fields.fetch("previously_fixed").fetch("default").inspect, + v6_input: v6_migration.fetch("validation"), + v7_input: v7_0_invalidation.fetch("validation"), + loop: review_loop, + handoff_aggregate: handoff_previous.fetch("aggregate_finding_id").fetch("validation"), + handoff_source_ids: handoff_previous.fetch("source_finding_ids").fetch("validation"), + handoff_provenance: handoff_previous.fetch("source_provenance").fetch("validation") + } + policy_assertions = { + history_seed: [[:input_default, "[]"], [:loop, "previously_fixed = validated_entry.previously_fixed || []"]], + history_retention: [[:loop, "Retain raw persisted history"]], + complete_v6: [[:v6_input, "preserve each complete identity tuple unchanged"], [:loop, "preserve complete v6, 7.0, 7.1, and current tuples unchanged"], [:handoff_aggregate, "Complete historic tuples are unchanged"]], + complete_v7_0: [[:v7_input, "preserve each complete aggregate_finding_id/source_finding_ids/source_provenance tuple unchanged"], [:loop, "preserve complete v6, 7.0, 7.1, and current tuples unchanged"], [:handoff_aggregate, "Complete historic tuples are unchanged"]], + preserve_aggregate_v6: [[:v6_input, "preserve a supplied aggregate_finding_id"], [:loop, "preserve a supplied aggregate_finding_id"], [:handoff_aggregate, "partial v6/v7.0 tuples preserve a supplied ID"]], + preserve_aggregate_v7_0: [[:v7_input, "preserve a supplied aggregate_finding_id"], [:loop, "preserve a supplied aggregate_finding_id"], [:handoff_aggregate, "partial v6/v7.0 tuples preserve a supplied ID"]], + derive_id_v6: [[:v6_input, "Missing aggregate_finding_id"], [:v6_input, "legacy-v6::"], [:loop, "legacy-v6::"]], + derive_id_v7_0: [[:v7_input, "When aggregate_finding_id is absent"], [:v7_input, "legacy-v7-0::"], [:loop, "legacy-v7-0::"]], + fill_source_ids_v6: [[:v6_input, "fill only missing source_finding_ids"], [:loop, "fill only missing source_finding_ids"], [:handoff_source_ids, "fill only missing source IDs"]], + fill_source_ids_v7_0: [[:v7_input, "fill only missing source_finding_ids"], [:loop, "fill only missing source_finding_ids"], [:handoff_source_ids, "fill only missing source IDs"]], + fill_provenance_v6: [[:v6_input, "missing source_provenance"], [:loop, "missing source_provenance"], [:handoff_provenance, "fill only missing provenance"]], + fill_provenance_v7_0: [[:v7_input, "missing source_provenance"], [:loop, "missing source_provenance"], [:handoff_provenance, "fill only missing provenance"]] + } + policy_checker = ->(policies, texts) { policies.all? { |policy| policy_assertions.fetch(policy).all? { |source, term| texts.fetch(source).include?(term) } } } + history_lifecycle_cases = [ + {"name" => "absent", "policies" => [:history_seed]}, + {"name" => "empty", "policies" => [:history_seed, :history_retention]}, + {"name" => "nonempty", "policies" => [:history_seed, :history_retention]} + ] + complete_history_cases = [ + {"schema_version" => "6.0", "policies" => [:complete_v6]}, + {"schema_version" => "7.0", "policies" => [:complete_v7_0]}, + {"schema_version" => immediate_prior, "policies" => [:history_retention]}, + {"schema_version" => input.fetch("schema_version"), "policies" => [:history_retention]} + ] + partial_presence_cases = [ + {"name" => "none", "present" => []}, + {"name" => "aggregate only", "present" => ["aggregate_finding_id"]}, + {"name" => "source IDs only", "present" => ["source_finding_ids"]}, + {"name" => "provenance only", "present" => ["source_provenance"]}, + {"name" => "aggregate and source IDs", "present" => ["aggregate_finding_id", "source_finding_ids"]}, + {"name" => "aggregate and provenance", "present" => ["aggregate_finding_id", "source_provenance"]}, + {"name" => "source IDs and provenance", "present" => ["source_finding_ids", "source_provenance"]} + ] + partial_history_cases = %w[6.0 7.0].flat_map do |schema_version| + partial_presence_cases.map do |presence| + version_key = schema_version == "6.0" ? "v6" : "v7_0" + aggregate_policy = presence.fetch("present").include?("aggregate_finding_id") ? "preserve_aggregate_#{version_key}".to_sym : "derive_id_#{version_key}".to_sym + {"schema_version" => schema_version, "name" => presence.fetch("name"), "policies" => [:history_retention, aggregate_policy, "fill_source_ids_#{version_key}".to_sym, "fill_provenance_#{version_key}".to_sym]} + end + end + policy_cases_valid = history_lifecycle_cases.map { |fixture| fixture.fetch("name") } == %w[absent empty nonempty] && + complete_history_cases.map { |fixture| fixture.fetch("schema_version") }.sort == ["6.0", "7.0", immediate_prior, input.fetch("schema_version")].sort && + partial_history_cases.length == 14 && + (history_lifecycle_cases + complete_history_cases + partial_history_cases).all? { |fixture| policy_checker.call(fixture.fetch("policies"), source_text) } + mutation_controls_rejected = [ + [:complete_v6, :v6_input, "preserve each complete identity tuple unchanged"], + [:fill_source_ids_v7_0, :v7_input, "fill only missing source_finding_ids"], + [:history_seed, :loop, "previously_fixed = validated_entry.previously_fixed || []"] + ].all? do |policy, source, term| + mutated = source_text.merge(source => source_text.fetch(source).gsub(term, "")) + !policy_checker.call([policy], mutated) + end template_scope_authority_valid = batch_expectations.fetch("templates").all? do |template_ref, template| manifest = scope_manifests[template_ref] next false unless manifest.is_a?(Array) && !manifest.empty? @@ -1704,12 +1793,17 @@ if ruby -ryaml -rjson -e ' actual = template.fetch("required_coverage_tuples").map { |tuple| [tuple["review_pass_id"], tuple["scope_item_id"], tuple["applicable_concern"], tuple["review_perspective"], tuple["coverage_obligation"]] } actual.uniq.length == actual.length && actual.sort == expected.sort end - valid = [input, output, handoffs, index].all? { |schema| schema.fetch("schema_version") == "7.1" } && - entry_names.include?("previously_fixed") && entry_names.include?("persisted_v6_packet_migration") && entry_names.include?("persisted_v7_0_packet_invalidation") && + valid = [input, output, handoffs, index].all? { |schema| schema.fetch("schema_version") == "7.2" } && + entry_names.include?("previously_fixed") && entry_names.include?("persisted_v6_packet_migration") && entry_names.include?("persisted_v7_0_packet_invalidation") && entry_names.include?(immediate_prior_name) && previous.key?("aggregate_finding_id") && previous.fetch("source_finding_ids").fetch("condition").include?("producer schema 7.1") && previous.fetch("source_provenance").fetch("condition").include?("producer schema 7.1") && !previous.key?("finding_id") && - migration_identity == canonical_identity && invalidation_identity == canonical_identity && - f3.fetch("check").include?("source_finding_ids") && f3.fetch("check").include?("source_provenance") && - handoff_previous.key?("aggregate_finding_id") && handoff_previous.fetch("source_finding_ids").fetch("required") == true && handoff_previous.fetch("source_provenance").fetch("required") == true && !handoff_previous.key?("finding_id") && + migration_identity == canonical_identity && invalidation_identity == canonical_identity && immediate_prior_invalidation_identity == canonical_identity && + v6_migration.fetch("validation").include?("legacy-v6::") && + v7_0_invalidation.fetch("validation").include?("Retain every raw 7.0 previously_fixed record") && v7_0_invalidation.fetch("validation").include?("legacy-v7-0::") && v7_0_invalidation.fetch("validation").include?("persisted-v7-0-migration") && + immediate_prior_invalidation.fetch("validation").include?("Preserve existing 7.1-or-later") && + f3.fetch("check").include?("source_finding_ids") && f3.fetch("check").include?("source_provenance") && e10.fetch("check").include?(immediate_prior) && e10.fetch("check").include?("7.1-or-later") && + handoff_previous.key?("aggregate_finding_id") && handoff_previous.fetch("aggregate_finding_id").fetch("validation").include?("v7.0") && handoff_previous.fetch("source_finding_ids").fetch("required") == true && handoff_previous.fetch("source_finding_ids").fetch("validation").include?("persisted-v7-0-migration") && handoff_previous.fetch("source_provenance").fetch("required") == true && handoff_previous.fetch("source_provenance").fetch("validation").include?("persisted-v7-0-migration") && !handoff_previous.key?("finding_id") && + policy_cases_valid && mutation_controls_rejected && + score_progression.fetch("validation").include?("Persisted 6.0/7.0/7.1 packets are invalidated and rebuilt before score tracking") && batch.fetch("scope_size").fetch("enum_values") == %w[trivial small medium large] && coverage.fetch("coverage_disposition").fetch("enum_values") == %w[inspected_no_risk finding incomplete] && coverage.fetch("finding_ids").fetch("condition") == "coverage_disposition == finding" && coverage.fetch("finding_ids").fetch("min_items") == 1 && @@ -1720,7 +1814,7 @@ if ruby -ryaml -rjson -e ' review_delegation.fetch("validation").include?("not_required requires direct_fallback") && !review_trigger_scope.key?("min_items") && review_trigger_scope.fetch("validation").include?("empty only when not_required + direct_fallback") && qa_delegation.fetch("validation").include?("delegation_triggered requires delegated") && !qa_trigger_scope.key?("min_items") && qa_trigger_scope.fetch("validation").include?("empty only when not_required + not_applicable") && batch_expectations.keys.sort == %w[case_requirements case_template_refs scope_manifests templates] && scope_manifests.keys.sort == batch_expectations.fetch("templates").keys.sort && case_requirements.keys.sort == expectation_refs.keys.sort && template_scope_authority_valid && - expectation_refs.keys.sort == %w[audit-batch-waits-for-all-pass-results audit-spec-review-fail-continues-complete-batch in-flight-mutation-invalidates-review-batch incomplete-review-batch-never-cleans post-fix-review-regression-remains-open post-fix-review-uses-fresh-snapshot-batch post-fix-verified-closure-with-incomplete-coverage trivial-audit-uses-two-isolated-passes].sort && + expectation_refs.keys.sort == %w[audit-batch-waits-for-all-pass-results audit-spec-review-fail-continues-complete-batch compact-cosmetic-review-retains-applicability compact-local-review-retains-causal-applicability in-flight-mutation-invalidates-review-batch incomplete-review-batch-never-cleans post-fix-review-regression-remains-open post-fix-review-uses-fresh-snapshot-batch post-fix-verified-closure-with-incomplete-coverage trivial-audit-uses-two-isolated-passes].sort && case_requirements.all? { |case_id, requirement| requirement.keys.sort == %w[mode required_artifacts required_envelope_alias] && requirement.fetch("required_envelope_alias") == "final_summary" && requirement.fetch("required_artifacts").include?("final_summary") && requirement.fetch("required_artifacts").include?("review_delegation_path") && (requirement.fetch("mode") == "audit") == requirement.fetch("required_artifacts").include?("audit_report") } && case_requirements.fetch("incomplete-review-batch-never-cleans") == {"mode" => "audit", "required_artifacts" => ["final_summary", "audit_report", "review_delegation_path"], "required_envelope_alias" => "final_summary"} && qa_case_requirements == { @@ -1736,7 +1830,7 @@ if ruby -ryaml -rjson -e ' final_artifact.fetch("validation").include?("final_batch_plan") && post_fix_paths.include?(["final_summary", "coverage_ledger", 0, "coverage_gap_id"]) && post_fix_paths.include?(["final_summary", "coverage_ledger", 1, "coverage_gap_id"]) exit valid ? 0 : 1 -' "$review_input" "$FRAMEWORK_DIR/skills/assistant-review/contracts/output.yaml" "$review_handoffs" "$review_index" "$review_evals" "$review_phase_gates" \ +' "$review_input" "$FRAMEWORK_DIR/skills/assistant-review/contracts/output.yaml" "$review_handoffs" "$review_index" "$review_evals" "$review_phase_gates" "$review_loop" \ && ! grep -Fq -- "medium+ scope only" "$review_handoffs" \ && grep -Fq -- "including triggered trivial/small scope" "$review_handoffs"; then pass diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh new file mode 100644 index 00000000..7f678f90 --- /dev/null +++ b/tests/p0-p4/change-impact-contracts.sh @@ -0,0 +1,825 @@ +#!/usr/bin/env bash + +if [[ -z "${P0P4_HARNESS_LOADED:-}" ]]; then + source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/p0p4-harness.sh" +fi +p0p4_bootstrap_suite "${BASH_SOURCE[0]}" +source "$FRAMEWORK_DIR/tests/p0-p4/lib/feature-preparation-response-fixtures.sh" + +validator="$FRAMEWORK_DIR/tools/change-impact/validate-change-impact.cjs" +protocol="$FRAMEWORK_DIR/tools/change-impact/protocol.v1.json" +example="$FRAMEWORK_DIR/tools/change-impact/example.completion.v1.json" +adapter="$FRAMEWORK_DIR/tests/change-impact/independent-fixture-adapter.test.cjs" +unit_suite="$FRAMEWORK_DIR/tests/change-impact/change-impact.test.cjs" +skill_validator="$FRAMEWORK_DIR/tools/skills/validate-skills.sh" + +test_start "change-impact common protocol resources are present" +if [[ -f "$validator" && -f "$protocol" && -f "$example" && -f "$adapter" && -f "$unit_suite" ]]; then + pass +else + fail "missing a common validator, protocol/example, or independent fixture adapter resource" +fi + +test_start "change-impact validation has an explicit Node runtime gate" +if ! command -v node >/dev/null 2>&1; then + fail "Node runtime is unavailable; triggered change-impact validation is blocked and must not become a manual pass" +elif ! node --version >/dev/null 2>&1; then + fail "Node runtime could not execute; triggered change-impact validation is blocked and must not install a dependency automatically" +else + pass +fi + +test_start "change-impact validator and independent source-backed adapter pass meaningful positives and negatives" +if node --test "$unit_suite" "$adapter" >/tmp/p0p4-change-impact-node.out 2>/tmp/p0p4-change-impact-node.err; then + pass +else + fail "change-impact Node tests failed; see /tmp/p0p4-change-impact-node.err" +fi + +test_start "change-impact source contracts validate without relying on scenario prompt text" +if "$skill_validator" --skill assistant-workflow --skill assistant-debugging --skill assistant-review >/tmp/p0p4-change-impact-skills.out 2>/tmp/p0p4-change-impact-skills.err; then + pass +else + fail "affected skill contracts failed validation; see /tmp/p0p4-change-impact-skills.err" +fi + +test_start "change-impact uses canonical pre_build through protocol, runtime, and phase carriers" +if ruby -ryaml -rjson - "$FRAMEWORK_DIR" <<'RUBY' +framework = ARGV.fetch(0) +field = ->(fields, name) { Array(fields).find { |item| item["name"] == name } } +protocol = JSON.parse(File.read(File.join(framework, "tools/change-impact/protocol.v1.json"))) +abort "protocol phase enum is not canonical" unless protocol.fetch("phases").keys == %w[discovery pre_build completion] +abort "protocol retains legacy phase spelling" if protocol.fetch("phases").key?("pre-build") + +%w[assistant-workflow assistant-debugging assistant-review].each do |skill| + input = YAML.load_file(File.join(framework, "skills", skill, "contracts/input.yaml")) + context = field.call(input.fetch("fields"), "change_impact_context") + phase = field.call(context.fetch("object_fields"), "phase") + abort "#{skill} input phase enum drifted" unless phase.fetch("enum_values") == %w[discovery pre_build completion] +end + +%w[assistant-workflow assistant-debugging].each do |skill| + output = YAML.load_file(File.join(framework, "skills", skill, "contracts/output.yaml")) + evidence = field.call(output.fetch("artifacts"), "change_impact_evidence") + phase = field.call(evidence.fetch("object_fields"), "phase") + abort "#{skill} output phase enum drifted" unless phase.fetch("enum_values") == %w[discovery pre_build completion] +end + +handoff_specs = [ + ["assistant-workflow", "orchestrator_to_architect", %w[discovery pre_build completion]], + ["assistant-workflow", "orchestrator_to_code_writer", %w[pre_build]], + ["assistant-workflow", "orchestrator_to_builder_tester", %w[pre_build]], + ["assistant-debugging", "debugging_fix", %w[pre_build]], + ["assistant-review", "orchestrator_to_reviewer", %w[discovery pre_build completion]] +] +handoff_specs.each do |skill, handoff_name, expected_phases| + contract = YAML.load_file(File.join(framework, "skills", skill, "contracts/handoffs.yaml")) + handoff = contract.fetch("handoffs").find { |item| item["name"] == handoff_name } + evidence = field.call(handoff.fetch("context_fields"), "change_impact_evidence") + phase = field.call(evidence.fetch("object_fields"), "phase") + abort "#{skill}/#{handoff_name} handoff phase enum drifted" unless phase.fetch("enum_values") == expected_phases +end + +readme = File.read(File.join(framework, "tools/change-impact/README.md")) +abort "README command retains legacy phase spelling" if readme.include?("--phase pre-build") +abort "README command omits canonical phase" unless readme.include?("--phase pre_build") +validator = File.read(File.join(framework, "tools/change-impact/validate-change-impact.cjs")) +abort "runtime phase enum drifted" unless validator.include?('new Set(["discovery", "pre_build", "completion"])') +abort "runtime result schema or receipt reuse drifted" unless validator.include?("change-impact-validation-result/v1") && validator.include?("change-impact-validation-receipt/v1") && validator.include?("RECEIPT_SCHEMA_INVALID") && validator.include?("RECEIPT_MISMATCH") +abort "behavior pre-build still permits no planned verification" unless validator.include?("BEHAVIOR_PREBUILD_VERIFICATION_MISSING") +protocol_rules = protocol.fetch("identity_rules") +abort "protocol omits receipt reuse semantics" unless protocol_rules.any? { |rule| rule.include?("--receipt") && rule.include?("canonical parsed JSON") } +[ + ["skills/assistant-workflow/references/change-impact.md", "--receipt validator_result_ref"], + ["skills/assistant-debugging/references/change-impact.md", "reuse reruns the checker with"], + ["skills/assistant-review/references/change-impact.md", "reuse reruns the checker with `--receipt`"] +].each do |path, expectation| + content = File.read(File.join(framework, path)) + abort "#{path} omits receipt-bound reuse" unless content.include?(expectation) && content.include?("--receipt") && content.include?("parsed inputs") +end +RUBY +then + pass +else + fail "change-impact canonical pre_build protocol, CLI, or contract carrier drifted" +fi + +test_start "evidenced local behavior remains compact when Node is unavailable, while shared and unresolved paths expand" +local_proportionality_failures=() +for local_contract in \ + "$FRAMEWORK_DIR/skills/assistant-workflow/contracts/input.yaml" \ + "$FRAMEWORK_DIR/skills/assistant-debugging/contracts/input.yaml" \ + "$FRAMEWORK_DIR/skills/assistant-review/contracts/input.yaml"; do + grep -Fq 'name: change_impact_applicability' "$local_contract" \ + || local_proportionality_failures+=("missing compact applicability in $(basename "$(dirname "$local_contract")")") + grep -Fq 'condition: "impact_scope == local"' "$local_contract" \ + || local_proportionality_failures+=("missing local causal-evidence condition in $local_contract") +done +for expanded_contract in \ + "$FRAMEWORK_DIR/skills/assistant-workflow/contracts/phase-gates.yaml" \ + "$FRAMEWORK_DIR/skills/assistant-debugging/contracts/phase-gates.yaml" \ + "$FRAMEWORK_DIR/skills/assistant-review/contracts/phase-gates.yaml"; do + grep -Fq 'impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried' "$expanded_contract" \ + || local_proportionality_failures+=("missing shared/unresolved-only common gate in $expanded_contract") +done +for local_reference in \ + "$FRAMEWORK_DIR/skills/assistant-workflow/references/change-impact.md" \ + "$FRAMEWORK_DIR/skills/assistant-debugging/references/change-impact.md" \ + "$FRAMEWORK_DIR/skills/assistant-review/references/change-impact.md"; do + grep -Eqi 'local' "$local_reference" && grep -Eqi 'Node' "$local_reference" \ + || local_proportionality_failures+=("local no-Node path absent in $local_reference") +done +if [[ "${#local_proportionality_failures[@]}" -eq 0 ]]; then + pass +else + fail "$(IFS='; '; printf '%s' "${local_proportionality_failures[*]}")" +fi + +test_start "all change-impact outputs preserve compact local and cosmetic applicability without relaxing expanded obligations" +if ruby -ryaml - "$FRAMEWORK_DIR" <<'RUBY' +framework = ARGV.fetch(0) +field = ->(fields, name) { Array(fields).find { |item| item["name"] == name } } +canonical_scopes = %w[not_applicable local shared unresolved] +selector_specs = { + "assistant-workflow" => ["workflow-change-impact-output", "workflow-completion-artifact"], + "assistant-debugging" => ["debugging-change-impact-output", "debugging-completion-artifact"], + "assistant-review" => ["review-change-impact-output", "review-completion-artifact"] +} +accepts = lambda do |fields, payload| + fields.all? do |schema| + value = payload[schema["name"]] + present = payload.key?(schema["name"]) && !value.to_s.empty? + required = schema["required"] == true || (schema["condition"] == "impact_scope == local" && payload["impact_scope"] == "local") + enum_valid = schema["type"] != "enum" || !payload.key?(schema["name"]) || Array(schema["enum_values"]).include?(value) + (!required || present) && enum_valid + end +end +selector_specs.each do |skill, (output_selector_id, completion_selector_id)| + input = YAML.load_file(File.join(framework, "skills", skill, "contracts/input.yaml")) + input_applicability = field.call(input.fetch("fields"), "change_impact_applicability") + output = YAML.load_file(File.join(framework, "skills", skill, "contracts/output.yaml")) + output_applicability = field.call(output.fetch("artifacts"), "change_impact_applicability") + abort "#{skill} output omits compact applicability" unless output_applicability + abort "#{skill} output applicability is not conditional" unless output_applicability.fetch("required") == "conditional" + abort "#{skill} output applicability shape drifted from input" unless output_applicability.fetch("object_fields") == input_applicability.fetch("object_fields") + abort "#{skill} output applicability trigger drifted from input" unless output_applicability.fetch("condition") == input_applicability.fetch("condition") + scope = field.call(output_applicability.fetch("object_fields"), "impact_scope") + causal = field.call(output_applicability.fetch("object_fields"), "causal_evidence_ref") + abort "#{skill} omits canonical cosmetic scope" unless scope.fetch("enum_values") == canonical_scopes + abort "#{skill} output applicability condition excludes cosmetic decisions" unless output_applicability.fetch("condition").include?("cosmetic") + abort "#{skill} relaxed local causal evidence" unless causal.fetch("required") == "conditional" && causal.fetch("condition") == "impact_scope == local" + abort "#{skill} input loses explicit expansion control" unless input_applicability.fetch("validation").include?("expanded_artifact_carried") + abort "#{skill} output loses shared/unresolved explicit expansion control" unless output_applicability.fetch("condition").include?("shared") && output_applicability.fetch("validation").include?("expanded_artifact_carried=true") + compact_no_checker = output_applicability.fetch("validation") + abort "#{skill} does not retain compact no-checker local output" unless compact_no_checker.include?("no checker") || compact_no_checker.include?("without an expanded artifact or checker refs") + if %w[assistant-workflow assistant-review].include?(skill) + recovery = output_applicability.fetch("on_fail") + recovery_artifact = skill == "assistant-review" ? "change_impact_review_projection" : "change_impact_evidence" + %w[scope/reason local causal shared/unresolved/expanded blocked].append(recovery_artifact).each do |term| + abort "#{skill} compact applicability recovery omits #{term}" unless recovery.include?(term) + end + end + local = {"impact_scope" => "local", "applicability_reason" => "causal trace limits the affected route", "causal_evidence_ref" => "tests/local-route", "expanded_artifact_carried" => false} + cosmetic = {"impact_scope" => "not_applicable", "applicability_reason" => "copy-only correction", "expanded_artifact_carried" => false} + abort "#{skill} rejects compact local applicability" unless accepts.call(output_applicability.fetch("object_fields"), local) + abort "#{skill} accepts local applicability without causal evidence" if accepts.call(output_applicability.fetch("object_fields"), local.reject { |key, _| key == "causal_evidence_ref" }) + abort "#{skill} rejects compact cosmetic applicability" unless accepts.call(output_applicability.fetch("object_fields"), cosmetic) + abort "#{skill} accepts cosmetic applicability without a reason" if accepts.call(output_applicability.fetch("object_fields"), cosmetic.reject { |key, _| key == "applicability_reason" }) + + index = YAML.load_file(File.join(framework, "skills", skill, "contracts/index.yaml")) + output_selector = index.fetch("load_sets").fetch("change_impact").fetch("selectors").find { |item| item["id"] == output_selector_id } + completion_selector = index.fetch("load_sets").fetch("completion").fetch("selectors").find { |item| item["id"] == completion_selector_id } + abort "#{skill} output selector omits compact applicability" unless output_selector.fetch("names").include?("change_impact_applicability") + abort "#{skill} completion selector omits compact applicability" unless completion_selector.fetch("allowed_names").include?("change_impact_applicability") +end +workflow_output = YAML.load_file(File.join(framework, "skills/assistant-workflow/contracts/output.yaml")) +tiers = workflow_output.fetch("completion_tiers") +%w[preparation_only small small_elevated medium large_critical].each do |name| + abort "#{name} omits conditional change-impact evidence" unless tiers.fetch(name).fetch("conditional_artifacts").include?("change_impact_evidence") + abort "#{name} omits conditional compact applicability" unless tiers.fetch(name).fetch("conditional_artifacts").include?("change_impact_applicability") +end +RUBY +then + pass +else + fail "compact applicability output shape, selectors, bounded field-shape omission guards, or workflow tiers regressed" +fi + +test_start "prepare-only impact discovery preserves applicability without implementation completion claims" +if ruby -ryaml - "$FRAMEWORK_DIR" <<'RUBY' +framework = ARGV.fetch(0) +gates = YAML.load_file(File.join(framework, "skills/assistant-workflow/contracts/phase-gates.yaml")) +invariant = gates.fetch("invariants").find { |item| item["id"] == "INV_CHANGE_IMPACT" } +abort "missing workflow change-impact invariant" unless invariant +check = invariant.fetch("check") +abort "prepare-only does not preserve change-impact discovery identity" unless check.include?("prepare_only") && check.include?("discovery") && check.include?("artifact identity") +abort "prepare-only still inherits implementation completion coverage" unless check.include?("execution_intent != prepare_only") && check.include?("common completion validation and assistant-review terminal coverage") +RUBY +then + pass +else + fail "prepare-only change-impact discovery still inherits implementation terminal review" +fi + +test_start "change-impact semantic evals retain prepare-only discovery and compact local causality" +semantic_eval_root="$(mktemp -d "${TMPDIR:-/tmp}/change-impact-semantic-evals.XXXXXX")" +p0p4_register_cleanup "$semantic_eval_root" +semantic_eval_responses="$semantic_eval_root/responses" +semantic_eval_output="$semantic_eval_root/output" +mkdir -p "$semantic_eval_responses/assistant-workflow" "$semantic_eval_responses/assistant-debugging" +workflow_semantic_case="prepare-only-shared-impact-retains-discovery-evidence" +debugging_semantic_case="local-debugging-retains-compact-causal-impact" +workflow_semantic_response="$semantic_eval_responses/assistant-workflow/$workflow_semantic_case.txt" +debugging_semantic_response="$semantic_eval_responses/assistant-debugging/$debugging_semantic_case.txt" +build_medium_prepare_only_shared_impact_response "$workflow_semantic_response" "prepare_only change_impact_evidence discovery capture-prepare-only-current" +build_local_debugging_compact_causal_response "$debugging_semantic_response" "local change_impact_applicability causal_evidence_ref" +semantic_eval_failures=() +run_semantic_eval() { + local skill="$1" + local case_id="$2" + local response="$3" + local expected="$4" + + if "$FRAMEWORK_DIR/tools/evals/run-skill-evals.sh" --responses "$semantic_eval_responses" --skill "$skill" --case "$case_id" >"$semantic_eval_output" 2>&1; then + [[ "$expected" == "PASS" ]] || return 1 + else + [[ "$expected" == "FAIL" ]] || return 1 + fi +} +if ! run_semantic_eval assistant-workflow "$workflow_semantic_case" "$workflow_semantic_response" PASS; then + semantic_eval_failures+=("workflow baseline") +fi +for mutation in \ + 'del(.change_impact_applicability)' \ + 'del(.change_impact_applicability.applicability_reason)' \ + '.change_impact_applicability.impact_scope = "local"' \ + '.change_impact_applicability.expanded_artifact_carried = false' \ + 'del(.change_impact_evidence)' \ + '.change_impact_evidence.phase = "pre_build"' \ + '.change_impact_evidence.assessment_ref = "assessment.json"' \ + '.fresh_review_result = {status:"CLEAN"}'; do + jq "$mutation" "$workflow_semantic_response" >"$semantic_eval_root/mutated.json" + mv "$semantic_eval_root/mutated.json" "$workflow_semantic_response" + if ! run_semantic_eval assistant-workflow "$workflow_semantic_case" "$workflow_semantic_response" FAIL; then + semantic_eval_failures+=("workflow:$mutation") + fi + build_medium_prepare_only_shared_impact_response "$workflow_semantic_response" "prepare_only change_impact_evidence discovery capture-prepare-only-current" +done +if ! run_semantic_eval assistant-debugging "$debugging_semantic_case" "$debugging_semantic_response" PASS; then + semantic_eval_failures+=("debugging baseline") +fi +for mutation in \ + 'del(.symptom_summary)' \ + 'del(.reproduction)' \ + 'del(.hypotheses)' \ + 'del(.root_cause)' \ + 'del(.confidence)' \ + 'del(.verification)' \ + 'del(.residual_risks)' \ + 'del(.change_impact_applicability.causal_evidence_ref)' \ + '.change_impact_applicability.expanded_artifact_carried = true' \ + '.change_impact_evidence = {artifact_identity:"unexpected"}'; do + jq "$mutation" "$debugging_semantic_response" >"$semantic_eval_root/mutated.json" + mv "$semantic_eval_root/mutated.json" "$debugging_semantic_response" + if ! run_semantic_eval assistant-debugging "$debugging_semantic_case" "$debugging_semantic_response" FAIL; then + semantic_eval_failures+=("debugging:$mutation") + fi + build_local_debugging_compact_causal_response "$debugging_semantic_response" "local change_impact_applicability causal_evidence_ref" +done +if [[ "${#semantic_eval_failures[@]}" -eq 0 ]]; then + pass +else + fail "semantic change-impact eval gaps: ${semantic_eval_failures[*]}" +fi + +test_start "debugging returns compact local applicability and harness templates match the canonical artifact enum" +if ruby -ryaml - "$FRAMEWORK_DIR" <<'RUBY' +framework = ARGV.fetch(0) +field = ->(fields, name) { Array(fields).find { |item| item["name"] == name } } +debug_input = YAML.load_file(File.join(framework, "skills/assistant-debugging/contracts/input.yaml")) +debug_output = YAML.load_file(File.join(framework, "skills/assistant-debugging/contracts/output.yaml")) +input_applicability = field.call(debug_input.fetch("fields"), "change_impact_applicability") +output_applicability = field.call(debug_output.fetch("artifacts"), "change_impact_applicability") +abort "debugging output omits compact applicability" unless output_applicability +abort "debugging output applicability does not cover behavior decisions" unless output_applicability.fetch("condition").include?("behavior") +abort "debugging output applicability does not retain local causal evidence" unless field.call(output_applicability.fetch("object_fields"), "causal_evidence_ref").fetch("condition") == "impact_scope == local" +abort "debugging applicability output drifted from input shape" unless output_applicability.fetch("object_fields") == input_applicability.fetch("object_fields") +index = YAML.load_file(File.join(framework, "skills/assistant-debugging/contracts/index.yaml")) +output_selector = index.fetch("load_sets").fetch("change_impact").fetch("selectors").find { |item| item["id"] == "debugging-change-impact-output" } +completion_selector = index.fetch("load_sets").fetch("completion").fetch("selectors").find { |item| item["id"] == "debugging-completion-artifact" } +abort "debugging output selector omits compact applicability" unless output_selector.fetch("names").include?("change_impact_applicability") +abort "debugging completion selector omits compact applicability" unless completion_selector.fetch("allowed_names").include?("change_impact_applicability") + +workflow_output = YAML.load_file(File.join(framework, "skills/assistant-workflow/contracts/output.yaml")) +ledger = field.call(workflow_output.fetch("artifacts"), "artifact_reference_ledger") +canonical = field.call(ledger.fetch("object_fields"), "artifact_type").fetch("enum_values") +extract = ->(text) { text.scan(/\[([^\]]*done_contract[^\]]*)\]/).map { |match| match.fetch(0).split(/[|\/]/).map(&:strip) } } +plan = File.read(File.join(framework, "skills/assistant-workflow/references/plan-harness-appendix.md")) +journal = File.read(File.join(framework, "skills/assistant-workflow/references/task-journal-harness-appendix.md")) +plan_lists = extract.call(plan) +journal_lists = extract.call(journal) +abort "plan harness appendix lacks typed artifact and ledger lists" unless plan_lists.length == 2 +abort "journal harness appendix lacks ledger list" unless journal_lists.length == 1 +(plan_lists + journal_lists).each { |list| abort "harness artifact enum drifted: #{list.inspect}" unless list == canonical } +RUBY +then + pass +else + fail "debugging local applicability or harness artifact enum parity regressed" +fi + +test_start "impact selectors record cosmetic applicability and shared fanout keeps the light Build lane" +if ruby - "$FRAMEWORK_DIR" <<'RUBY' +framework = ARGV.fetch(0) +section = lambda do |path, start_marker, end_marker| + content = File.read(File.join(framework, path)) + start = content.index(start_marker) or abort "missing #{start_marker} in #{path}" + tail = content[start..] + tail.split(end_marker, 2).first +end + +workflow_root = section.call("skills/assistant-workflow/SKILL.md", "## Contracts", "Selectors resolve") +workflow_phases = section.call("skills/assistant-workflow/references/phases.md", "## Shared Controller Decisions", "## Progress Updates") +debugging_root = section.call("skills/assistant-debugging/SKILL.md", "## Progressive Contract Loading", "## Ownership") +debugging_ref = section.call("skills/assistant-debugging/references/change-impact.md", "# Change-impact during debugging", "##") +review_ref = section.call("skills/assistant-review/references/change-impact.md", "# Change-impact review projection", "##") +abort "workflow root selector omits cosmetic" unless workflow_root.include?("when behavior, cosmetic, or local/shared/unresolved impact") +abort "workflow phase trigger omits cosmetic" unless workflow_phases.include?("behavior-bearing, cosmetic, or locality/shared-impact claim") +abort "debugging root selector omits cosmetic" unless debugging_root.include?("repair, cosmetic, or locality claim") +abort "debugging reference trigger omits cosmetic" unless debugging_ref.include?("fix, or cosmetic decision") +abort "review reference trigger omits cosmetic" unless review_ref.include?("behavior or cosmetic change") + +controller = section.call("skills/assistant-workflow/references/workflow-controller.md", "- `light`:", "- `standard`:") +build = section.call("skills/assistant-workflow/references/build-worker-protocol.md", "## Adaptive Source-Changing Role Ownership", "## Expanded-impact mutation authorization") +triage = section.call("skills/assistant-workflow/references/triage-rubric.md", "| Intensity | Use when |", "## Candidate Scope Scan") +abort "light controller promotes shared fanout" unless controller.include?("Shared fanout alone keeps this Build policy") +abort "Build protocol promotes shared fanout" unless build.include?("Shared fanout alone does not promote this Build lane") +abort "triage light selection promotes shared fanout" unless triage.include?("shared fanout alone does not promote Build") +RUBY +then + pass +else + fail "cosmetic impact activation or light shared-fanout Build selection regressed" +fi + +test_start "read-only impact carriers retain discovery while mutation handoffs require current pre_build evidence" +if ruby -ryaml - "$FRAMEWORK_DIR" <<'RUBY' +framework = ARGV.fetch(0) +load = ->(path) { YAML.load_file(path) } +field = ->(fields, name) { Array(fields).find { |item| item["name"] == name } } + +debug_output = load.call(File.join(framework, "skills/assistant-debugging/contracts/output.yaml")) +expanded = field.call(debug_output.fetch("artifacts"), "change_impact_evidence") +abort "missing debugging change-impact output" unless expanded +abort "local debugging still requires expanded evidence" unless expanded["condition"] == "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" +abort "old broad debugging condition survived" if expanded["condition"] == "a diagnosis or proposed fix triggers change-impact accounting" +local_output = {"status" => "root_cause_found", "symptom_summary" => "local", "reproduction" => {}, "hypotheses" => [], "confidence" => "medium", "verification" => [], "residual_risks" => [], "change_impact_applicability" => {"impact_scope" => "local", "applicability_reason" => "causal evidence bounds the diagnosis to this route", "causal_evidence_ref" => "tests/local-route", "expanded_artifact_carried" => false}} +abort "local output fabricated expanded refs" if local_output.keys.any? { |key| key.end_with?("_ref") || key == "change_impact_evidence" } +local_applicability = field.call(debug_output.fetch("artifacts"), "change_impact_applicability") +local_fields = local_applicability.fetch("object_fields") +accepts_local = lambda do |payload| + local_fields.all? do |schema| + value = payload[schema["name"]] + present = payload.key?(schema["name"]) && !value.to_s.empty? + required = schema["required"] == true || (schema["condition"] == "impact_scope == local" && payload["impact_scope"] == "local") + enum_valid = schema["type"] != "enum" || !payload.key?(schema["name"]) || Array(schema["enum_values"]).include?(value) + (!required || present) && enum_valid + end +end +abort "local output omits valid compact applicability" unless accepts_local.call(local_output.fetch("change_impact_applicability")) +abort "local output accepts missing causal evidence" if accepts_local.call(local_output.fetch("change_impact_applicability").reject { |key, _| key == "causal_evidence_ref" }) + +review_output = load.call(File.join(framework, "skills/assistant-review/contracts/output.yaml")) +[load.call(File.join(framework, "skills/assistant-workflow/contracts/output.yaml")), debug_output, review_output].each do |contract| + artifact_name = contract["skill"] == "assistant-review" ? "change_impact_review_projection" : "change_impact_evidence" + envelope = field.call(contract.fetch("artifacts"), artifact_name) + fields = envelope.fetch("object_fields") + result_ref = field.call(fields, "validator_result_ref") + blocker_ref = field.call(fields, "gap_or_blocker_ref") + abort "valid result ref is not conditional" unless result_ref && result_ref["required"] == "conditional" && result_ref["condition"] == "status == valid" + abort "non-valid blocker ref is not conditional" unless blocker_ref && blocker_ref["required"] == "conditional" && blocker_ref["condition"] == "status in [gaps_reported, blocked, invalid]" + + accepts = lambda do |payload| + fields.all? do |schema| + value = payload[schema["name"]] + present = payload.key?(schema["name"]) && !value.to_s.empty? + required = schema["required"] == true || + (schema["condition"] == "status == valid" && payload["status"] == "valid") || + (schema["condition"] == "status in [gaps_reported, blocked, invalid]" && ["gaps_reported", "blocked", "invalid"].include?(payload["status"])) + enum_valid = schema["type"] != "enum" || !payload.key?(schema["name"]) || Array(schema["enum_values"]).include?(value) + (!required || present) && enum_valid + end + end + blocked = {"artifact_identity" => "impact-1", "phase" => "discovery", "impact_scope" => "shared", "status" => "blocked", "gap_or_blocker_ref" => "node-runtime-unavailable"} + valid = {"artifact_identity" => "impact-1", "phase" => "discovery", "impact_scope" => "shared", "status" => "valid", "validator_result_ref" => "result.json", "capture_ref" => "capture.json", "expected_context_ref" => "expected.json"} + if contract["skill"] == "assistant-review" + valid = {"artifact_identity" => "impact-1", "status" => "valid", "validator_result_ref" => "completion-result.json", "capture_ref" => "capture.json", "expected_context_ref" => "expected.json", "assessment_ref" => "assessment.json", "review_projection_ref" => "review.json", "scope_manifest_id" => "manifest-1", "coverage_ledger_id" => "ledger-1", "review_snapshot_id" => "review-1", "snapshot_id" => "source-1", "canonical_final_summary_ref" => "final-summary.json"} + end + abort "valid output rejects complete checker evidence" unless accepts.call(valid) + %w[gaps_reported blocked invalid].each do |status| + payload = {"artifact_identity" => "impact-1", "phase" => "discovery", "impact_scope" => "shared", "status" => status, "gap_or_blocker_ref" => "gap.json"} + abort "non-valid output requires unavailable checker result" unless accepts.call(payload) + end + abort "blocked output requires unavailable checker result" unless accepts.call(blocked) + abort "blocked output accepts no blocker" if accepts.call(blocked.reject { |key, _| key == "gap_or_blocker_ref" }) + abort "valid output accepts no checker result" if accepts.call(valid.reject { |key, _| key == "validator_result_ref" }) + abort "valid output accepts empty checker result" if accepts.call(valid.merge("validator_result_ref" => "")) + if contract["skill"] == "assistant-review" + validation = envelope.fetch("validation") + abort "review output loses completion receipt semantics" unless validation.include?("validator_result_ref") && validation.include?("current valid completion validator result") && validation.include?("receipt") && validation.include?("current parsed inputs") + abort "review output loses final-summary authority" unless validation.include?("final_summary coverage_complete") && validation.include?("CLEAN/ISSUES_FIXED") + abort "review output lacks completion result recovery" unless envelope.fetch("on_fail").include?("completion checker") + end + if contract["skill"] == "assistant-workflow" + validation = envelope.fetch("validation") + abort "workflow completion loses original review bindings" unless validation.include?("original scope_manifest") && validation.include?("coverage_ledger") && validation.include?("current snapshot") + end +end + +%w[assistant-workflow assistant-debugging assistant-review].each do |skill| + input = load.call(File.join(framework, "skills", skill, "contracts/input.yaml")) + context = field.call(input.fetch("fields"), "change_impact_context") + phase = field.call(context.fetch("object_fields"), "phase") + assessment = field.call(context.fetch("object_fields"), "assessment_ref") + result_ref = field.call(context.fetch("object_fields"), "validator_result_ref") + review_ref = field.call(context.fetch("object_fields"), "review_projection_ref") + abort "#{skill} discovery phase is absent" unless phase && phase["required"] == "conditional" && phase["condition"] == "status == valid" && phase["enum_values"] == ["discovery", "pre_build", "completion"] + abort "#{skill} discovery still fabricates assessment" unless assessment && assessment["condition"] == "status == valid and phase in [pre_build, completion]" + abort "#{skill} valid context omits validator result ref" unless result_ref && result_ref["required"] == "conditional" && result_ref["condition"] == "status == valid" && context.fetch("validation").include?("validator result") + abort "#{skill} completion omits review projection ref" unless review_ref && review_ref["required"] == "conditional" && review_ref["condition"] == "status == valid and phase == completion" && context.fetch("validation").include?("review projection") + accepts_context = lambda do |payload| + context.fetch("object_fields").all? do |schema| + required = schema["required"] == true || (schema["condition"] == "status == valid" && payload["status"] == "valid") || (schema["condition"] == "status == valid and phase in [pre_build, completion]" && payload["status"] == "valid" && ["pre_build", "completion"].include?(payload["phase"])) || (schema["condition"] == "status == valid and phase == completion" && payload["status"] == "valid" && payload["phase"] == "completion") || (schema["condition"] == "status in [gaps_reported, blocked, invalid]" && ["gaps_reported", "blocked", "invalid"].include?(payload["status"])) + !required || (payload.key?(schema["name"]) && !payload[schema["name"]].to_s.empty?) + end + end + valid_context = {"artifact_identity" => "impact-1", "status" => "valid", "phase" => "pre_build", "capture_ref" => "capture.json", "expected_context_ref" => "expected.json", "assessment_ref" => "assessment.json", "validator_result_ref" => "result.json"} + abort "#{skill} accepts valid context without result ref" if accepts_context.call(valid_context.reject { |key, _| key == "validator_result_ref" }) + abort "#{skill} rejects valid context with result ref" unless accepts_context.call(valid_context) + completion_context = valid_context.merge("phase" => "completion", "review_projection_ref" => "review.json") + abort "#{skill} rejects valid completion context with review projection" unless accepts_context.call(completion_context) + abort "#{skill} accepts valid completion context without review projection" if accepts_context.call(completion_context.reject { |key, _| key == "review_projection_ref" }) + abort "#{skill} accepts blank completion review projection" if accepts_context.call(completion_context.merge("review_projection_ref" => "")) + abort "#{skill} requires review projection outside valid completion" if !accepts_context.call(valid_context) || !accepts_context.call({"artifact_identity" => "impact-1", "status" => "valid", "phase" => "discovery", "capture_ref" => "capture.json", "expected_context_ref" => "expected.json", "validator_result_ref" => "result.json"}) + %w[gaps_reported blocked invalid].each do |status| + payload = {"artifact_identity" => "impact-1", "status" => status, "gap_or_blocker_ref" => "gap.json"} + abort "#{skill} non-valid context requires result ref" unless accepts_context.call(payload) + end +end + +review_gates = load.call(File.join(framework, "skills/assistant-review/contracts/phase-gates.yaml")) +entry = review_gates.fetch("gates").find { |gate| gate["phase"] == "ENTRY" } +entry_impact = entry.fetch("exit_assertions").find { |assertion| assertion["id"] == "E_CHANGE_IMPACT" } +entry_applicability = entry.fetch("exit_assertions").find { |assertion| assertion["id"] == "E_CHANGE_IMPACT_APPLICABILITY" } +abort "review ENTRY can skip missing/local/cosmetic applicability" unless entry_applicability && !entry_applicability.key?("condition") +abort "review ENTRY selects expanded scope before applicability" unless entry.fetch("exit_assertions").index(entry_applicability) < entry.fetch("exit_assertions").index(entry_impact) +%w[change_impact_applicability impact_scope applicability_reason causal_evidence_ref expanded_artifact_carried not_applicable].each do |term| + abort "review ENTRY applicability omits #{term}" unless entry_applicability.fetch("check").include?(term) +end +abort "review ENTRY applicability has no pre-dispatch recovery" unless entry_applicability.fetch("on_fail").include?("before dispatch") +abort "review ENTRY expands compact applicability" unless entry_impact["condition"] == "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" +review_input = load.call(File.join(framework, "skills/assistant-review/contracts/input.yaml")) +applicability = field.call(review_input.fetch("fields"), "change_impact_applicability") +accepts_applicability = lambda do |payload| + payload.is_a?(Hash) && applicability.fetch("object_fields").all? do |schema| + required = schema["required"] == true || (schema["condition"] == "impact_scope == local" && payload["impact_scope"] == "local") + (!required || (payload.key?(schema["name"]) && !payload[schema["name"]].to_s.empty?)) && + (schema["type"] != "enum" || !payload.key?(schema["name"]) || schema.fetch("enum_values").include?(payload[schema["name"]])) + end +end +local = {"impact_scope" => "local", "applicability_reason" => "bounded causal path", "causal_evidence_ref" => "tests/local-route", "expanded_artifact_carried" => false} +abort "review accepts missing applicability" if accepts_applicability.call(nil) || accepts_applicability.call({}) +abort "review accepts local without causal evidence" if accepts_applicability.call(local.reject { |key, _| key == "causal_evidence_ref" }) +abort "review rejects compact local applicability" unless accepts_applicability.call(local) +abort "review rejects compact cosmetic applicability" unless accepts_applicability.call({"impact_scope" => "not_applicable", "applicability_reason" => "cosmetic only", "expanded_artifact_carried" => false}) +abort "review ENTRY requires discovery assessment" unless entry_impact["check"].include?("discovery requires identity/capture/expected refs") && entry_impact["check"].include?("assessment is required only for pre-build or completion") + +handoffs = load.call(File.join(framework, "skills/assistant-workflow/contracts/handoffs.yaml")) +architect = handoffs.fetch("handoffs").find { |item| item["name"] == "orchestrator_to_architect" } +architect_impact = field.call(architect.fetch("context_fields"), "change_impact_evidence") +abort "Architect does not receive expanded impact evidence" unless architect_impact && architect_impact["condition"] == "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" +canonical_scopes = ["not_applicable", "local", "shared", "unresolved"] +architect_scope = field.call(architect_impact.fetch("object_fields"), "impact_scope") +abort "Architect rejects a carried local impact artifact" unless architect_scope && architect_scope["enum_values"] == canonical_scopes +review_handoffs = load.call(File.join(framework, "skills/assistant-review/contracts/handoffs.yaml")) +reviewer = review_handoffs.fetch("handoffs").find { |item| item["name"] == "orchestrator_to_reviewer" } +reviewer_impact = field.call(reviewer.fetch("context_fields"), "change_impact_evidence") +[ + ["Architect", architect_impact, true], + ["Reviewer", reviewer_impact, false] +].each do |role, carrier, valid_only| + fields = carrier.fetch("object_fields") + phase = field.call(carrier.fetch("object_fields"), "phase") + assessment = field.call(carrier.fetch("object_fields"), "assessment_ref") + scope = field.call(carrier.fetch("object_fields"), "impact_scope") + review_ref = field.call(fields, "review_projection_ref") + expected_review_condition = valid_only ? "phase == completion" : "status == valid and phase == completion" + abort "#{role} carrier loses discovery/completion evidence" unless phase && phase["enum_values"] == ["discovery", "pre_build", "completion"] + abort "#{role} carrier requires discovery assessment" unless assessment && assessment["condition"].include?("phase in [pre_build, completion]") + abort "#{role} carrier rejects a carried local impact artifact" unless scope && scope["enum_values"] == canonical_scopes + abort "#{role} completion carrier omits review projection ref" unless review_ref && review_ref["type"] == "string" && review_ref["required"] == "conditional" && review_ref["condition"] == expected_review_condition + abort "#{role} completion carrier loses review pointer reuse semantics" unless review_ref["validation"].to_s.include?("actual review document pointer for rerun or receipt reuse") + + accepts = lambda do |payload| + fields.all? do |schema| + value = payload[schema["name"]] + present = payload.key?(schema["name"]) && !value.to_s.empty? + required = schema["required"] == true || + (schema["condition"] == "status == valid" && payload["status"] == "valid") || + (schema["condition"] == "phase in [pre_build, completion]" && ["pre_build", "completion"].include?(payload["phase"])) || + (schema["condition"] == "status == valid and phase in [pre_build, completion]" && payload["status"] == "valid" && ["pre_build", "completion"].include?(payload["phase"])) || + (schema["condition"] == "phase == completion" && payload["phase"] == "completion") || + (schema["condition"] == "status == valid and phase == completion" && payload["status"] == "valid" && payload["phase"] == "completion") || + (schema["condition"] == "status in [gaps_reported, blocked, invalid]" && ["gaps_reported", "blocked", "invalid"].include?(payload["status"])) + enum_valid = schema["type"] != "enum" || !payload.key?(schema["name"]) || Array(schema["enum_values"]).include?(value) + (!required || present) && enum_valid + end + end + + discovery = {"artifact_identity" => "impact-1", "status" => "valid", "impact_scope" => "shared", "phase" => "discovery", "capture_ref" => "capture.json", "expected_context_ref" => "expected.json", "validator_result_ref" => "result.json"} + pre_build = discovery.merge("phase" => "pre_build", "assessment_ref" => "assessment.json") + completion = pre_build.merge("phase" => "completion", "review_projection_ref" => "review.json") + abort "#{role} rejects valid discovery carrier" unless accepts.call(discovery) + abort "#{role} rejects valid pre-build carrier" unless accepts.call(pre_build) + abort "#{role} rejects valid completion carrier" unless accepts.call(completion) + abort "#{role} accepts completion without review projection" if accepts.call(completion.reject { |key, _| key == "review_projection_ref" }) + abort "#{role} accepts blank completion review projection" if accepts.call(completion.merge("review_projection_ref" => "")) + + if valid_only + gaps = {"artifact_identity" => "impact-1", "status" => "gaps_reported", "impact_scope" => "shared", "gap_or_blocker_ref" => "gaps.json"} + abort "Architect carrier permits non-valid gaps evidence" if accepts.call(gaps) + else + %w[gaps_reported blocked invalid].each do |status| + gaps = {"artifact_identity" => "impact-1", "status" => status, "impact_scope" => "shared", "gap_or_blocker_ref" => "gaps.json"} + abort "Reviewer rejects #{status} gaps evidence" unless accepts.call(gaps) + end + end +end + +mutation_handoffs = { + "CodeWriter" => [handoffs, "orchestrator_to_code_writer"], + "BuilderTester" => [handoffs, "orchestrator_to_builder_tester"], + "Fixer" => [load.call(File.join(framework, "skills/assistant-debugging/contracts/handoffs.yaml")), "debugging_fix"] +} +code_writer = handoffs.fetch("handoffs").find { |item| item["name"] == "orchestrator_to_code_writer" } +builder_tester = handoffs.fetch("handoffs").find { |item| item["name"] == "orchestrator_to_builder_tester" } +code_writer_applicability = field.call(code_writer.fetch("context_fields"), "change_impact_applicability") +builder_tester_applicability = field.call(builder_tester.fetch("context_fields"), "change_impact_applicability") +abort "BuilderTester omits compact impact applicability" unless builder_tester_applicability +abort "BuilderTester applicability differs from CodeWriter" unless builder_tester_applicability == code_writer_applicability +mutation_handoffs.each do |role, (contract, handoff_name)| + handoff = contract.fetch("handoffs").find { |item| item["name"] == handoff_name } + carrier = field.call(handoff.fetch("context_fields"), "change_impact_evidence") + fields = carrier.fetch("object_fields") + phase = field.call(fields, "phase") + assessment = field.call(fields, "assessment_ref") + status = field.call(fields, "status") + review_ref = field.call(fields, "review_projection_ref") + abort "#{role} accepts discovery/completion mutation authorization" unless phase && phase["enum_values"] == ["pre_build"] + abort "#{role} carries completion review projection into a pre-build-only mutation handoff" if review_ref + abort "#{role} permits optional pre-build assessment" unless assessment && assessment["required"] == true + abort "#{role} permits non-valid mutation evidence" unless status && status["enum_values"] == ["valid"] + abort "#{role} does not require current resolved pre-build bindings" unless carrier["validation"].to_s.include?("current valid pre_build") && carrier["validation"].to_s.include?("resolve") + abort "#{role} permits stale pre-build evidence reuse" unless carrier.fetch("validation").include?("--receipt validator_result_ref") && carrier.fetch("validation").include?("current valid pre_build") + abort "BuilderTester permits evidence reuse after CodeWriter mutation" if role == "BuilderTester" && !carrier.fetch("validation").include?("BuilderTester must not reuse CodeWriter's pre-build result after earlier mutations") + + valid_payload = {"phase" => "pre_build", "status" => "valid", "assessment_ref" => "assessment.json", "artifact_identity" => "impact-1", "capture_ref" => "capture.json", "expected_context_ref" => "expected.json", "validator_result_ref" => "result.json", "impact_scope" => "shared"} + invalid_payloads = [ + valid_payload.merge("phase" => "discovery"), + valid_payload.merge("phase" => "completion"), + valid_payload.reject { |key, _| key == "assessment_ref" }, + valid_payload.merge("impact_scope" => "invalid") + ] + accepts = lambda do |payload| + fields.all? do |schema| + value = payload[schema["name"]] + required = schema["required"] == true + present = payload.key?(schema["name"]) && !value.to_s.empty? + enum_valid = schema["type"] != "enum" || !payload.key?(schema["name"]) || Array(schema["enum_values"]).include?(value) + (!required || present) && enum_valid + end + end + abort "#{role} rejects a valid pre-build authorization payload" unless accepts.call(valid_payload) + abort "#{role} accepts discovery, completion, or assessment-free mutation payload" if invalid_payloads.any? { |payload| accepts.call(payload) } +end +artifact_type_fields = [] +walk = lambda do |value| + case value + when Hash + artifact_type_fields << value if value["name"] == "artifact_type" + value.each_value { |child| walk.call(child) } + when Array + value.each { |child| walk.call(child) } + end +end +walk.call(handoffs.fetch("handoffs")) +abort "missing artifact-ref carry-through" if artifact_type_fields.length < 5 +abort "change-impact evidence is rejected by a nested handoff" unless artifact_type_fields.all? { |item| Array(item["enum_values"]).include?("change_impact_evidence") } +RUBY +then + pass +else + fail "local output, read-only carriers, or mutation-handoff change-impact applicability regressed" +fi + +test_start "expanded impact authorization is enforced at mutation entry, not Build or Fix exit" +if ruby -ryaml - "$FRAMEWORK_DIR" <<'RUBY' +framework = ARGV.fetch(0) +checks = [ + ["skills/assistant-workflow/contracts/phase-gates.yaml", "BUILD", "B_CHANGE_IMPACT_PREBUILD"], + ["skills/assistant-debugging/contracts/phase-gates.yaml", "FIX", "FX_CHANGE_IMPACT_PREBUILD"], + ["skills/assistant-review/contracts/phase-gates.yaml", "FIX_STEP", "F_CHANGE_IMPACT_PREBUILD"] +] +checks.each do |path, phase_name, assertion_id| + contract = YAML.load_file(File.join(framework, path)) + gate = contract.fetch("gates").find { |item| item["phase"] == phase_name } + entry = gate.fetch("entry_assertions").find { |item| item["id"] == assertion_id } + exit = Array(gate["exit_assertions"]).find { |item| item["id"] == assertion_id } + abort "#{phase_name} lacks an expanded-impact mutation entry assertion" unless entry + abort "#{phase_name} keeps expanded-impact authorization at exit" if exit + abort "#{phase_name} entry assertion does not precede every source/test mutation or dispatch" unless entry.fetch("check").downcase.include?("before any source/test mutation or mutation dispatch") + abort "#{phase_name} entry assertion lost shared/unresolved scope" unless entry["condition"] == "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + applicability_id = assertion_id.sub("PREBUILD", "APPLICABILITY") + applicability = gate.fetch("entry_assertions").find { |item| item["id"] == applicability_id } + abort "#{phase_name} lacks applicability before scope selection" unless applicability + abort "#{phase_name} applicability can skip missing/unset scope" if applicability.key?("condition") + abort "#{phase_name} checks expanded scope before establishing applicability" unless gate.fetch("entry_assertions").index(applicability) < gate.fetch("entry_assertions").index(entry) + %w[change_impact_applicability impact_scope applicability_reason causal_evidence_ref expanded_artifact_carried].each do |field| + abort "#{phase_name} applicability omits #{field}" unless applicability.fetch("check").include?(field) + end + abort "#{phase_name} applicability gate runs too late" unless applicability.fetch("check").downcase.include?("before any source/test mutation or mutation dispatch") + abort "#{phase_name} applicability has no pre-mutation recovery" unless applicability.fetch("on_fail").include?("Stop before") +end + +guide = File.read(File.join(framework, "docs/skill-contract-design-guide.md")) +abort "guide does not define mutation entry assertions" unless guide.include?("entry_assertions") && guide.include?("before any mutation or mutation dispatch") +RUBY +then + pass +else + fail "expanded impact authorization must remain a pre-mutation entry gate" +fi + +test_start "light expanded impact requires a canonical review projection without Pack refs or copied Build policy" +expanded_fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/workflow-light-expanded-impact.XXXXXX")" +p0p4_register_cleanup "$expanded_fixture_root" +expanded_fixture="$expanded_fixture_root/cases.json" +expanded_completion="$expanded_fixture_root/completion.json" +expanded_capture="$expanded_fixture_root/capture.json" +expanded_expected="$expanded_fixture_root/expected.json" +expanded_assessment="$expanded_fixture_root/assessment.json" +expanded_review="$expanded_fixture_root/review.json" +expanded_skill="$expanded_fixture_root/assistant-workflow" +expanded_responses="$expanded_fixture_root/responses" +mkdir -p "$expanded_skill/evals" "$expanded_responses/assistant-workflow" +cp "$FRAMEWORK_DIR/skills/assistant-workflow/SKILL.md" "$expanded_skill/SKILL.md" +p0p4_filter_workflow_eval_cases "$FRAMEWORK_DIR/skills/assistant-workflow/evals/cases.json" "$expanded_fixture" light-pack-review-result-retains-current-snapshot +jq ' + .expected.review_context.scope_manifest_id = "current-scope-digest" + | .expected.review_context.coverage_ledger_id = "journal#final-summary/coverage_ledger" + | .expected.review_context.review_snapshot_id = "review-current" + | .expected.review_context.snapshot_id = "current-review-snapshot" + | .review.review_source.scope_manifest_id = "current-scope-digest" + | .review.review_source.coverage_ledger_id = "journal#final-summary/coverage_ledger" + | .review.review_source.review_snapshot_id = "review-current" + | .review.review_source.snapshot_id = "current-review-snapshot" + | .expected.review_context.required_bindings[0].scope_item_id = "workflow-terminal-evidence" + | .expected.review_context.required_bindings[0].coverage_concern_id = "canonical producer consumption" + | .review.bindings[0].scope_item_id = "workflow-terminal-evidence" + | .review.bindings[0].coverage_concern_id = "canonical producer consumption" +' "$FRAMEWORK_DIR/tools/change-impact/example.completion.v1.json" >"$expanded_completion" +jq '.capture' "$expanded_completion" >"$expanded_capture" +jq '.expected' "$expanded_completion" >"$expanded_expected" +jq '.assessment' "$expanded_completion" >"$expanded_assessment" +jq '.review' "$expanded_completion" >"$expanded_review" +node "$FRAMEWORK_DIR/tools/change-impact/validate-change-impact.cjs" --phase completion --capture "$expanded_capture" --expected "$expanded_expected" --assessment "$expanded_assessment" --review "$expanded_review" >"$expanded_fixture_root/completion-checker.log" +jq ' + .cases[0].title = "Light expanded impact retains canonical review evidence" + | .cases[0].setup_context = ["controller_intensity=light; risk_tier=low.", "architecture_design_mode=not_applicable.", "impact_scope=shared and expanded_artifact_carried=true; shared fanout alone does not promote Build."] + | .cases[0].machine_expectations.required_substrings = ["fresh_review_result", "change_impact_evidence", "canonical_final_summary", "current_review_delegation_path"] + | .cases[0].machine_expectations.structured_json_assertions |= map(select((((.path // []) | join(".")) | contains("architecture_decision_pack_review")) | not) | select(. != {"operator":"equals","path":["current_review_delegation_path","artifact","subagent_execution_mode"],"expected":"delegated"})) + [ + {"operator":"equals","path":["impact_scope"],"expected":"shared"}, + {"operator":"equals","path":["controller_intensity"],"expected":"light"}, + {"operator":"equals","path":["risk_tier"],"expected":"low"}, + {"operator":"equals","path":["expanded_artifact_carried"],"expected":true}, + {"operator":"equals","path":["architecture_design_mode"],"expected":"not_applicable"}, + {"operator":"equals","path":["change_impact_evidence","phase"],"expected":"completion"}, + {"operator":"equals","path":["change_impact_evidence","artifact_identity"],"expected":"capture-example-current"}, + {"operator":"equals","path":["change_impact_evidence","validator_result_ref"],"expected":"fixture#completion-checker"}, + {"operator":"equals","path":["change_impact_evidence","capture_ref"],"expected":"fixture#capture"}, + {"operator":"equals","path":["change_impact_evidence","expected_context_ref"],"expected":"fixture#expected"}, + {"operator":"equals","path":["change_impact_evidence","assessment_ref"],"expected":"fixture#assessment"}, + {"operator":"equals","path":["change_impact_evidence","review_projection_ref"],"expected":"fixture#review"}, + {"operator":"equals","path":["change_impact_review","schema_version"],"expected":"change-impact-review/v1"}, + {"operator":"equals","path":["current_review_delegation_path","artifact","subagent_policy_state"],"expected":"not_required"}, + {"operator":"equals","path":["current_review_delegation_path","artifact","subagent_execution_mode"],"expected":"direct_fallback"}, + {"operator":"path_absent","path":["fresh_review_result","architecture_decision_pack_review_ref"]}, + {"operator":"path_absent","path":["fresh_review_result","architecture_decision_pack_review_contract"]} + ] +' "$expanded_fixture" >"$expanded_fixture.rewritten" && mv "$expanded_fixture.rewritten" "$expanded_fixture" +cp "$expanded_fixture" "$expanded_skill/evals/cases.json" +expanded_summary="$(jq -r '.cases[0].machine_expectations.required_substrings[]' "$expanded_fixture" | paste -sd ' ' -)" +expanded_response="$expanded_responses/assistant-workflow/light-pack-review-result-retains-current-snapshot.txt" +build_workflow_review_lifecycle_eval_response light-pack-review-result-retains-current-snapshot "$expanded_response" "$expanded_summary" +jq --slurpfile completion "$expanded_completion" ' + .impact_scope = "shared" + | .controller_intensity = "light" + | .risk_tier = "low" + | .expanded_artifact_carried = true + | .architecture_design_mode = "not_applicable" + | .change_impact_evidence = {artifact_identity:"capture-example-current",phase:"completion",impact_scope:"shared",validator_result_ref:"fixture#completion-checker",status:"valid",capture_ref:"fixture#capture",expected_context_ref:"fixture#expected",assessment_ref:"fixture#assessment",review_projection_ref:"fixture#review"} + | .change_impact_review = $completion[0].review + | .current_review_delegation_path.artifact.subagent_policy_state = "not_required" + | .current_review_delegation_path.artifact.subagent_execution_mode = "direct_fallback" + | .current_review_delegation_path.artifact.subagent_trigger_scope = [] + | del(.fresh_review_result.architecture_decision_pack_review_ref, .fresh_review_result.architecture_decision_pack_review_contract) +' "$expanded_response" >"$expanded_response.rewritten" && mv "$expanded_response.rewritten" "$expanded_response" +run_light_expanded_fixture() { + local expected="$2" + jq '.change_impact_review' "$expanded_response" >"$expanded_fixture_root/candidate-review.json" + if ! node "$FRAMEWORK_DIR/tools/change-impact/validate-change-impact.cjs" --phase completion --capture "$expanded_capture" --expected "$expanded_expected" --assessment "$expanded_assessment" --review "$expanded_fixture_root/candidate-review.json" >"$expanded_fixture_root/candidate-checker.log"; then + [[ "$expected" == FAIL ]] && return 0 + cat "$expanded_fixture_root/candidate-checker.log" >&2 + return 1 + fi + if "$FRAMEWORK_DIR/tools/evals/run-skill-evals.sh" --responses "$expanded_responses" --skill "$expanded_skill" >"$expanded_fixture_root/runner.out" 2>&1; then + [[ "$expected" == PASS ]] + else + [[ "$expected" == FAIL ]] || cat "$expanded_fixture_root/runner.out" >&2 + [[ "$expected" == FAIL ]] + fi +} +expanded_failures=() +if ! run_light_expanded_fixture "$expanded_response" PASS; then + expanded_failures+=("valid non-Pack expanded review rejected") +fi +for mutation in \ + 'del(.canonical_final_summary)' \ + 'del(.change_impact_evidence.review_projection_ref)' \ + 'del(.change_impact_evidence.capture_ref)' \ + '.current_final_batch.final_snapshot_identity.value = "foreign-current-snapshot"' \ + '.current_review_delegation_path.artifact.subagent_execution_mode = "not_applicable"' \ + '.change_impact_review.bindings[0].scope_item_id = "foreign-scope"'; do + jq "$mutation" "$expanded_response" >"$expanded_response.mutated" + mv "$expanded_response.mutated" "$expanded_response" + if ! run_light_expanded_fixture "$expanded_response" FAIL; then + expanded_failures+=("accepted $mutation") + fi + build_workflow_review_lifecycle_eval_response light-pack-review-result-retains-current-snapshot "$expanded_response" "$expanded_summary" + jq --slurpfile completion "$expanded_completion" ' + .impact_scope = "shared" | .controller_intensity = "light" | .risk_tier = "low" | .expanded_artifact_carried = true | .architecture_design_mode = "not_applicable" + | .change_impact_evidence = {artifact_identity:"capture-example-current",phase:"completion",impact_scope:"shared",validator_result_ref:"fixture#completion-checker",status:"valid",capture_ref:"fixture#capture",expected_context_ref:"fixture#expected",assessment_ref:"fixture#assessment",review_projection_ref:"fixture#review"} + | .change_impact_review = $completion[0].review + | .current_review_delegation_path.artifact.subagent_policy_state = "not_required" + | .current_review_delegation_path.artifact.subagent_execution_mode = "direct_fallback" + | .current_review_delegation_path.artifact.subagent_trigger_scope = [] + | del(.fresh_review_result.architecture_decision_pack_review_ref, .fresh_review_result.architecture_decision_pack_review_contract) + ' "$expanded_response" >"$expanded_response.rewritten" && mv "$expanded_response.rewritten" "$expanded_response" +done +if [[ ${#expanded_failures[@]} -eq 0 ]]; then + pass +else + fail "light expanded-impact canonical review fixture gaps: ${expanded_failures[*]}" +fi + +test_start "standalone review repairs enter FIX_STEP before mutation" +if ruby -ryaml - "$FRAMEWORK_DIR" <<'RUBY' +framework = ARGV.fetch(0) +gates = YAML.load_file(File.join(framework, "skills/assistant-review/contracts/phase-gates.yaml")) +entry = gates.fetch("gates").find { |gate| gate["phase"] == "ENTRY" } +e8 = entry.fetch("exit_assertions").find { |assertion| assertion["id"] == "E8" } +fix_step = gates.fetch("gates").find { |gate| gate["phase"] == "FIX_STEP" } +impact_entry = fix_step.fetch("entry_assertions").find { |assertion| assertion["id"] == "F_CHANGE_IMPACT_PREBUILD" } +abort "E8 bypasses FIX_STEP entry assertions on repair" unless e8.fetch("check").include?("FIX_STEP entry_assertions before repair or mutation dispatch") +abort "E8 recovery bypasses FIX_STEP entry assertions on repair" unless e8.fetch("on_fail").include?("FIX_STEP entry_assertions before repair or mutation dispatch") +abort "FIX_STEP lost the pre-mutation impact entry gate" unless impact_entry + +loop = File.read(File.join(framework, "skills/assistant-review/references/review-loop.md")) +prepare = loop.split(/^while round <= 10:/, 2).first +abort "PREPARE standalone repair bypasses FIX_STEP" unless prepare.include?("FIX_STEP entry_assertions before repair or mutation dispatch") +RUBY +then + pass +else + fail "standalone Spec Review repair must use FIX_STEP entry assertions before mutation" +fi + +test_start "assistant-review routes triggered change-impact before planning and excludes it from Reviewer bundles" +if ruby - "$FRAMEWORK_DIR/skills/assistant-review/SKILL.md" <<'RUBY' +source = File.read(ARGV.fetch(0)) +routing = source.split(/^Migration note:/, 2).first +bullets = routing.scan(/^- .*?(?=^- |\z)/m).map { |bullet| bullet.gsub(/\s+/, " ").strip } +positive_routing = bullets.any? do |bullet| + bullet.match?(/\bTriggered\b/) && bullet.match?(/`change_impact`/) && bullet.match?(/\b(?:load|resolve)\b/) && bullet.match?(/before planning or dispatch/) +end +abort "missing triggered change-impact routing" unless positive_routing +normalized_routing = routing.gsub(/\s+/, " ") +abort "missing Reviewer-bundle change-impact exclusion" unless normalized_routing.match?(/keep change-impact guidance out of Reviewer bundles/) +RUBY +then + pass +else + fail "assistant-review must route triggered change-impact before planning and exclude it from Reviewer bundles" +fi + +p0p4_finish_suite "${BASH_SOURCE[0]}" diff --git a/tests/p0-p4/context-budget-report-contracts.sh b/tests/p0-p4/context-budget-report-contracts.sh index 85c3df58..2dcf2a9f 100755 --- a/tests/p0-p4/context-budget-report-contracts.sh +++ b/tests/p0-p4/context-budget-report-contracts.sh @@ -425,7 +425,7 @@ if HOME="$report_home" \ .selected_load_set_context.name == "reviewer_context" and .selected_load_set_context.measurement_scope == "static_selected_skill_instruction_surface" and .selected_load_set_context.declared_budget_words == 5653 - and .selected_load_set_context.declared_boundary_closure == {words: 5649, bytes: 45519} + and .selected_load_set_context.declared_boundary_closure == {words: 5224, bytes: 42810} and (.selected_load_set_context.transitive_worker_additions.worker_return_schema_projection.selectors_resolved == 1) and (.selected_load_set_context.transitive_worker_additions.worker_return_schema_projection.words > 0) and (.selected_load_set_context.worker_instruction_closure.words == diff --git a/tests/p0-p4/feature-preparation-evidence-contracts.sh b/tests/p0-p4/feature-preparation-evidence-contracts.sh index 17d68159..a23742b9 100644 --- a/tests/p0-p4/feature-preparation-evidence-contracts.sh +++ b/tests/p0-p4/feature-preparation-evidence-contracts.sh @@ -807,7 +807,7 @@ if grep -Fq '"completion_policy", "plan_mode"' "$workflow_dir/evals/cases.json" test_start "shared helper owns executable case-root manifest and bounded grader-call accounting" helper="$FRAMEWORK_DIR/tests/p0-p4/lib/feature-preparation-response-fixtures.sh" -if [[ -f "$helper" ]] && grep -Fq 'case_roots' "$helper" && grep -Fq 'forbidden_paths' "$helper" && grep -Fq 'count_structured_json_assertion_failures' "$FRAMEWORK_DIR/tests/p0-p4/skill-eval-contracts.sh" && grep -Fq 'count_structured_json_assertion_failures' "$FRAMEWORK_DIR/tests/p0-p4/progressive-discovery-contracts.sh" && grep -Fq 'prepare_only_direct_structured_probe_count" -eq 375' "$FRAMEWORK_DIR/tests/p0-p4/progressive-discovery-contracts.sh" && grep -Fq 'prepare_only_representative_cli_probe_count" -eq 3' "$FRAMEWORK_DIR/tests/p0-p4/progressive-discovery-contracts.sh" && grep -Fq 'prepare_only_plan_mode_mutation_count" -eq 12' "$FRAMEWORK_DIR/tests/p0-p4/progressive-discovery-contracts.sh" && grep -Fq 'prepare_only_mutation_invocation_count" -eq 15' "$FRAMEWORK_DIR/tests/p0-p4/progressive-discovery-contracts.sh" && grep -Fq 'prepare_only_direct_structured_probe_count" -eq 375' "$FRAMEWORK_DIR/tests/p0-p4/skill-eval-contracts.sh" && grep -Fq 'prepare_only_representative_cli_probe_count" -eq 3' "$FRAMEWORK_DIR/tests/p0-p4/skill-eval-contracts.sh"; then pass; else fail "grader suites duplicate case mappings, omit direct structured probes, or overcount bounded full CLI calls"; fi +if [[ -f "$helper" ]] && grep -Fq 'case_roots' "$helper" && grep -Fq 'forbidden_paths' "$helper" && grep -Fq 'count_structured_json_assertion_failures' "$FRAMEWORK_DIR/tests/p0-p4/skill-eval-contracts.sh" && grep -Fq 'count_structured_json_assertion_failures' "$FRAMEWORK_DIR/tests/p0-p4/progressive-discovery-contracts.sh" && grep -Fq 'prepare_only_direct_structured_probe_count" -eq 409' "$FRAMEWORK_DIR/tests/p0-p4/progressive-discovery-contracts.sh" && grep -Fq 'prepare_only_representative_cli_probe_count" -eq 3' "$FRAMEWORK_DIR/tests/p0-p4/progressive-discovery-contracts.sh" && grep -Fq 'prepare_only_plan_mode_mutation_count" -eq 14' "$FRAMEWORK_DIR/tests/p0-p4/progressive-discovery-contracts.sh" && grep -Fq 'prepare_only_mutation_invocation_count" -eq 17' "$FRAMEWORK_DIR/tests/p0-p4/progressive-discovery-contracts.sh" && grep -Fq 'prepare_only_direct_structured_probe_count" -eq 409' "$FRAMEWORK_DIR/tests/p0-p4/skill-eval-contracts.sh" && grep -Fq 'prepare_only_representative_cli_probe_count" -eq 3' "$FRAMEWORK_DIR/tests/p0-p4/skill-eval-contracts.sh"; then pass; else fail "grader suites duplicate case mappings, omit direct structured probes, or overcount bounded full CLI calls"; fi test_start "actual graders mutate every prepare-only active root, forbid plan documents, and journal branches detailed refs" if ruby -ryaml -e ' diff --git a/tests/p0-p4/installer-contracts.sh b/tests/p0-p4/installer-contracts.sh index 4f6500a7..35b643d7 100644 --- a/tests/p0-p4/installer-contracts.sh +++ b/tests/p0-p4/installer-contracts.sh @@ -84,7 +84,7 @@ else fail "first install failed; see /tmp/p0p4-install-1.err" fi -test_start "Codex single-skill install keeps routing metadata in the installed skill" +test_start "Codex selected workflow install keeps routing metadata and its canonical review producer" INSTALL_HOME_SKILL_TABLE="$(mktemp -d)" p0p4_register_cleanup "$INSTALL_HOME_SKILL_TABLE" if HOME="$INSTALL_HOME_SKILL_TABLE" bash "$FRAMEWORK_DIR/install.sh" --agent codex --skill assistant-workflow --no-hooks >/tmp/p0p4-install-single-skill-table.out 2>/tmp/p0p4-install-single-skill-table.err; then @@ -92,10 +92,10 @@ if HOME="$INSTALL_HOME_SKILL_TABLE" bash "$FRAMEWORK_DIR/install.sh" --agent cod installed_skills_dir="$INSTALL_HOME_SKILL_TABLE/.codex/skills" assistant_skill_rows="$(count_occurrences "^| assistant-" "$agents_file")" - if [[ ! -d "$installed_skills_dir/assistant-workflow" ]]; then - fail "expected assistant-workflow to be installed" - elif [[ -d "$installed_skills_dir/assistant-review" || -d "$installed_skills_dir/assistant-docs" ]]; then - fail "expected single-skill install to avoid installing assistant-review and assistant-docs" + if [[ ! -d "$installed_skills_dir/assistant-workflow" || ! -d "$installed_skills_dir/assistant-review" ]]; then + fail "expected assistant-workflow and its canonical assistant-review producer to be installed" + elif [[ -d "$installed_skills_dir/assistant-docs" ]]; then + fail "expected selected workflow install to avoid unrelated assistant-docs" elif [[ "$assistant_skill_rows" != "0" ]]; then fail "expected lean Codex AGENTS.md to avoid duplicating installed skill routing tables; found $assistant_skill_rows rows" elif ! grep -Fq "Codex uses installed skills through native skill routing." "$agents_file"; then @@ -107,6 +107,177 @@ else fail "single-skill Codex install failed; see /tmp/p0p4-install-single-skill-table.err" fi +test_start "selected bundled requirements close transitively without duplicate cycle copies" +INSTALL_HOME_REQUIRES_CLOSURE="$(mktemp -d)" +REQUIRES_CLOSURE_FRAMEWORK_ROOT="$(mktemp -d)" +p0p4_register_cleanup "$INSTALL_HOME_REQUIRES_CLOSURE" "$REQUIRES_CLOSURE_FRAMEWORK_ROOT" +cp "$FRAMEWORK_DIR/install.sh" "$REQUIRES_CLOSURE_FRAMEWORK_ROOT/install.sh" +for closure_skill in assistant-closure-a assistant-closure-b assistant-closure-c; do + mkdir -p "$REQUIRES_CLOSURE_FRAMEWORK_ROOT/skills/$closure_skill" +done +cat >"$REQUIRES_CLOSURE_FRAMEWORK_ROOT/skills/assistant-closure-a/SKILL.md" <<'EOF' +--- +name: assistant-closure-a +description: Closure root fixture. +requires: + - assistant-closure-b + - assistant-closure-c +--- +EOF +cat >"$REQUIRES_CLOSURE_FRAMEWORK_ROOT/skills/assistant-closure-b/SKILL.md" <<'EOF' +--- +name: assistant-closure-b +description: Closure transitive fixture. +requires: + - assistant-closure-c +--- +EOF +cat >"$REQUIRES_CLOSURE_FRAMEWORK_ROOT/skills/assistant-closure-c/SKILL.md" <<'EOF' +--- +name: assistant-closure-c +description: Closure cycle fixture. +requires: + - assistant-closure-a +--- +EOF +if HOME="$INSTALL_HOME_REQUIRES_CLOSURE" bash "$REQUIRES_CLOSURE_FRAMEWORK_ROOT/install.sh" --agent codex --skill assistant-closure-a --dry-run >/tmp/p0p4-install-requires-closure-dry.out 2>/tmp/p0p4-install-requires-closure-dry.err \ + && grep -Fq -- "-> $INSTALL_HOME_REQUIRES_CLOSURE/.codex/skills/assistant-closure-a/" /tmp/p0p4-install-requires-closure-dry.out \ + && grep -Fq -- "-> $INSTALL_HOME_REQUIRES_CLOSURE/.codex/skills/assistant-closure-b/" /tmp/p0p4-install-requires-closure-dry.out \ + && grep -Fq -- "-> $INSTALL_HOME_REQUIRES_CLOSURE/.codex/skills/assistant-closure-c/" /tmp/p0p4-install-requires-closure-dry.out \ + && [[ ! -e "$INSTALL_HOME_REQUIRES_CLOSURE/.codex" && ! -e "$INSTALL_HOME_REQUIRES_CLOSURE/.agents" ]] \ + && HOME="$INSTALL_HOME_REQUIRES_CLOSURE" bash "$REQUIRES_CLOSURE_FRAMEWORK_ROOT/install.sh" --agent codex --skill assistant-closure-a >/tmp/p0p4-install-requires-closure.out 2>/tmp/p0p4-install-requires-closure.err; then + installed_closure_skills="$(find "$INSTALL_HOME_REQUIRES_CLOSURE/.codex/skills" -mindepth 1 -maxdepth 1 -type d -name 'assistant-closure-*' -exec basename {} \; | sort | tr '\n' ' ')" + if [[ "$installed_closure_skills" == "assistant-closure-a assistant-closure-b assistant-closure-c " ]] \ + && [[ "$(count_occurrences 'assistant-closure-a ->' /tmp/p0p4-install-requires-closure.out)" == "1" ]] \ + && [[ "$(count_occurrences 'assistant-closure-b ->' /tmp/p0p4-install-requires-closure.out)" == "1" ]] \ + && [[ "$(count_occurrences 'assistant-closure-c ->' /tmp/p0p4-install-requires-closure.out)" == "1" ]]; then + pass + else + fail "selected bundled requirement closure did not install each cycle member exactly once" + fi +else + fail "selected bundled requirement closure or dry-run failed" +fi + +test_start "selected custom root supports an empty assistant inventory and advisory external requires" +INSTALL_HOME_EMPTY_INVENTORY="$(mktemp -d)" +EMPTY_INVENTORY_FRAMEWORK_ROOT="$(mktemp -d)" +EMPTY_INVENTORY_SKILL_NAME="custom-root-$(basename "$EMPTY_INVENTORY_FRAMEWORK_ROOT")" +EMPTY_INVENTORY_SKILL="$EMPTY_INVENTORY_FRAMEWORK_ROOT/skills/$EMPTY_INVENTORY_SKILL_NAME" +p0p4_register_cleanup "$INSTALL_HOME_EMPTY_INVENTORY" "$EMPTY_INVENTORY_FRAMEWORK_ROOT" +mkdir -p "$EMPTY_INVENTORY_SKILL" +cp "$FRAMEWORK_DIR/install.sh" "$EMPTY_INVENTORY_FRAMEWORK_ROOT/install.sh" +cat >"$EMPTY_INVENTORY_SKILL/SKILL.md" </tmp/p0p4-install-empty-inventory.out 2>/tmp/p0p4-install-empty-inventory.err; then + if [[ -f "$INSTALL_HOME_EMPTY_INVENTORY/.codex/skills/$EMPTY_INVENTORY_SKILL_NAME/SKILL.md" ]] \ + && grep -Fq "NOTE: $EMPTY_INVENTORY_SKILL_NAME requires 'external-skill'" /tmp/p0p4-install-empty-inventory.out; then + pass + else + fail "selected custom root did not install or report its missing external requirement" + fi +else + fail "selected custom root with an empty assistant inventory failed; see /tmp/p0p4-install-empty-inventory.err" +fi + +test_start "selected skill names reject path traversal before dry-run or install writes" +INSTALL_HOME_PATHFUL_SKILL="$(mktemp -d)" +p0p4_register_cleanup "$INSTALL_HOME_PATHFUL_SKILL" +pathful_skill='../../V1/skills/assistant-review' +if HOME="$INSTALL_HOME_PATHFUL_SKILL" bash "$FRAMEWORK_DIR/install.sh" --agent codex --skill "$pathful_skill" --dry-run >/tmp/p0p4-install-pathful-skill-dry.out 2>/tmp/p0p4-install-pathful-skill-dry.err; then + fail "pathful selected skill was accepted during dry-run" +elif HOME="$INSTALL_HOME_PATHFUL_SKILL" bash "$FRAMEWORK_DIR/install.sh" --agent codex --skill "$pathful_skill" >/tmp/p0p4-install-pathful-skill.out 2>/tmp/p0p4-install-pathful-skill.err; then + fail "pathful selected skill was accepted during install" +elif [[ -e "$INSTALL_HOME_PATHFUL_SKILL/.codex" || -e "$INSTALL_HOME_PATHFUL_SKILL/.agents" || -e "$INSTALL_HOME_PATHFUL_SKILL/V1" ]]; then + fail "pathful selected skill rejection wrote outside the managed skills root" +else + pass +fi + +test_start "selected closures reject unsafe implicit dependency targets before any write" +INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET="$(mktemp -d)" +UNSAFE_DEPENDENCY_EXTERNAL="$(mktemp -d)" +p0p4_register_cleanup "$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET" "$UNSAFE_DEPENDENCY_EXTERNAL" +unsafe_dependency_workflow="$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET/.codex/skills/assistant-workflow" +unsafe_dependency_review="$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET/.codex/skills/assistant-review" +mkdir -p "$unsafe_dependency_workflow" "$UNSAFE_DEPENDENCY_EXTERNAL" +printf 'preserve selected root\n' >"$unsafe_dependency_workflow/SKILL.md" +printf 'preserve external sentinel\n' >"$UNSAFE_DEPENDENCY_EXTERNAL/sentinel.txt" +cp "$unsafe_dependency_workflow/SKILL.md" "$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET/workflow.before" +cp "$UNSAFE_DEPENDENCY_EXTERNAL/sentinel.txt" "$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET/sentinel.before" +ln -s "$UNSAFE_DEPENDENCY_EXTERNAL" "$unsafe_dependency_review" +if HOME="$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET" bash "$FRAMEWORK_DIR/install.sh" --agent codex --skill assistant-workflow >/tmp/p0p4-install-unsafe-dependency-target.out 2>/tmp/p0p4-install-unsafe-dependency-target.err; then + fail "selected closure accepted an implicit dependency target symlink" +elif ! cmp -s "$unsafe_dependency_workflow/SKILL.md" "$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET/workflow.before"; then + fail "implicit dependency target rejection mutated the selected root" +elif ! cmp -s "$UNSAFE_DEPENDENCY_EXTERNAL/sentinel.txt" "$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET/sentinel.before"; then + fail "implicit dependency target rejection mutated the external sentinel" +elif HOME="$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET" bash "$FRAMEWORK_DIR/install.sh" --agent codex --skill assistant-workflow --dry-run >/tmp/p0p4-install-unsafe-dependency-target-dry.out 2>/tmp/p0p4-install-unsafe-dependency-target-dry.err; then + fail "selected closure accepted an implicit dependency target symlink during dry-run" +elif ! cmp -s "$unsafe_dependency_workflow/SKILL.md" "$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET/workflow.before" \ + || ! cmp -s "$UNSAFE_DEPENDENCY_EXTERNAL/sentinel.txt" "$INSTALL_HOME_UNSAFE_DEPENDENCY_TARGET/sentinel.before"; then + fail "dry-run implicit dependency target rejection mutated a protected path" +else + pass +fi + +test_start "selected closures reject wrong-type managed targets before any write" +INSTALL_HOME_WRONG_TYPE_SKILL_TARGET="$(mktemp -d)" +p0p4_register_cleanup "$INSTALL_HOME_WRONG_TYPE_SKILL_TARGET" +wrong_type_dependency_target="$INSTALL_HOME_WRONG_TYPE_SKILL_TARGET/.codex/skills/assistant-review" +mkdir -p "$(dirname "$wrong_type_dependency_target")" +printf 'preserve wrong-type target\n' >"$wrong_type_dependency_target" +cp "$wrong_type_dependency_target" "$INSTALL_HOME_WRONG_TYPE_SKILL_TARGET/wrong-type.before" +if HOME="$INSTALL_HOME_WRONG_TYPE_SKILL_TARGET" bash "$FRAMEWORK_DIR/install.sh" --agent codex --skill assistant-workflow >/tmp/p0p4-install-wrong-type-skill-target.out 2>/tmp/p0p4-install-wrong-type-skill-target.err; then + fail "selected closure accepted a wrong-type managed target" +elif [[ -e "$INSTALL_HOME_WRONG_TYPE_SKILL_TARGET/.codex/skills/assistant-workflow" ]]; then + fail "wrong-type managed target rejection copied the selected root" +elif ! cmp -s "$wrong_type_dependency_target" "$INSTALL_HOME_WRONG_TYPE_SKILL_TARGET/wrong-type.before"; then + fail "wrong-type managed target rejection changed the target" +elif HOME="$INSTALL_HOME_WRONG_TYPE_SKILL_TARGET" bash "$FRAMEWORK_DIR/install.sh" --agent codex --skill assistant-workflow --dry-run >/tmp/p0p4-install-wrong-type-skill-target-dry.out 2>/tmp/p0p4-install-wrong-type-skill-target-dry.err; then + fail "selected closure accepted a wrong-type managed target during dry-run" +else + pass +fi + +test_start "selected custom roots reject source and metadata symlinks before reads or writes" +INSTALL_HOME_UNSAFE_SOURCE="$(mktemp -d)" +UNSAFE_SOURCE_FRAMEWORK_ROOT="$(mktemp -d)" +UNSAFE_SOURCE_EXTERNAL="$(mktemp -d)" +p0p4_register_cleanup "$INSTALL_HOME_UNSAFE_SOURCE" "$UNSAFE_SOURCE_FRAMEWORK_ROOT" "$UNSAFE_SOURCE_EXTERNAL" +cp "$FRAMEWORK_DIR/install.sh" "$UNSAFE_SOURCE_FRAMEWORK_ROOT/install.sh" +mkdir -p "$UNSAFE_SOURCE_FRAMEWORK_ROOT/skills" +for unsafe_source_kind in source metadata; do + unsafe_source_skill="assistant-unsafe-$unsafe_source_kind" + unsafe_source_home="$INSTALL_HOME_UNSAFE_SOURCE/$unsafe_source_kind" + mkdir -p "$unsafe_source_home" "$UNSAFE_SOURCE_EXTERNAL/$unsafe_source_kind" + printf '%s\n' '---' "name: $unsafe_source_skill" 'description: Unsafe source fixture.' '---' >"$UNSAFE_SOURCE_EXTERNAL/$unsafe_source_kind/SKILL.md" + if [[ "$unsafe_source_kind" == "source" ]]; then + ln -s "$UNSAFE_SOURCE_EXTERNAL/$unsafe_source_kind" "$UNSAFE_SOURCE_FRAMEWORK_ROOT/skills/$unsafe_source_skill" + else + mkdir -p "$UNSAFE_SOURCE_FRAMEWORK_ROOT/skills/$unsafe_source_skill" + ln -s "$UNSAFE_SOURCE_EXTERNAL/$unsafe_source_kind/SKILL.md" "$UNSAFE_SOURCE_FRAMEWORK_ROOT/skills/$unsafe_source_skill/SKILL.md" + fi + + if HOME="$unsafe_source_home" bash "$UNSAFE_SOURCE_FRAMEWORK_ROOT/install.sh" --agent codex --skill "$unsafe_source_skill" --dry-run >/tmp/p0p4-install-unsafe-${unsafe_source_kind}-dry.out 2>/tmp/p0p4-install-unsafe-${unsafe_source_kind}-dry.err \ + || HOME="$unsafe_source_home" bash "$UNSAFE_SOURCE_FRAMEWORK_ROOT/install.sh" --agent codex --skill "$unsafe_source_skill" >/tmp/p0p4-install-unsafe-${unsafe_source_kind}.out 2>/tmp/p0p4-install-unsafe-${unsafe_source_kind}.err \ + || [[ -e "$unsafe_source_home/.codex" || -e "$unsafe_source_home/.agents" ]]; then + unsafe_source_failure="$unsafe_source_kind symlink was accepted or wrote to the managed home" + break + fi +done +if [[ -z "${unsafe_source_failure:-}" ]]; then + pass +else + fail "$unsafe_source_failure" +fi + test_start "requires fixtures stay outside the canonical skills inventory" requires_fixture_prefix='assistant-requires-contract-fixture-' canonical_fixture_creation='mktemp -d "$FRAMEWORK_DIR/skills/'"$requires_fixture_prefix" @@ -591,4 +762,68 @@ else fail "Codex --no-hooks install failed; see /tmp/p0p4-install-codex-no-hooks.err" fi +test_start "selective debugging and review installs ship an executable common change-impact checker" +CHANGE_IMPACT_INSTALL_ROOT="$(mktemp -d)" +p0p4_register_cleanup "$CHANGE_IMPACT_INSTALL_ROOT" +change_impact_install_failure="" +for change_impact_skill in assistant-debugging assistant-review; do + change_impact_home="$CHANGE_IMPACT_INSTALL_ROOT/$change_impact_skill home [isolated]" + change_impact_case="$change_impact_home/change impact fixture" + change_impact_skill_file="$change_impact_home/.codex/skills/$change_impact_skill/SKILL.md" + change_impact_tool="$(dirname "$change_impact_skill_file")/../../tools/change-impact/validate-change-impact.cjs" + change_impact_protocol="$(dirname "$change_impact_skill_file")/../../tools/change-impact/protocol.v1.json" + change_impact_example="$(dirname "$change_impact_skill_file")/../../tools/change-impact/example.completion.v1.json" + change_impact_reference="$change_impact_home/.codex/skills/$change_impact_skill/references/change-impact.md" + mkdir -p "$change_impact_case" + if ! HOME="$change_impact_home" bash "$FRAMEWORK_DIR/install.sh" --agent codex --skill "$change_impact_skill" --no-hooks >/tmp/p0p4-change-impact-${change_impact_skill}.out 2>/tmp/p0p4-change-impact-${change_impact_skill}.err; then + change_impact_install_failure="$change_impact_skill selective install failed" + break + elif [[ ! -f "$change_impact_skill_file" || ! -f "$change_impact_tool" || ! -f "$change_impact_protocol" || ! -f "$change_impact_example" ]]; then + change_impact_install_failure="$change_impact_skill selective install omitted common validator resources at the path resolved from its loaded SKILL.md" + break + elif [[ -d "$change_impact_home/.codex/skills/assistant-workflow" || ! -f "$change_impact_reference" ]] \ + || ! grep -Fq '../../tools/change-impact/validate-change-impact.cjs' "$change_impact_reference"; then + change_impact_install_failure="$change_impact_skill did not resolve the installed common checker without workflow" + break + elif [[ "$change_impact_skill" == "assistant-debugging" && ! -f "$change_impact_home/.codex/skills/assistant-review/SKILL.md" ]]; then + change_impact_install_failure="assistant-debugging selective installation omitted its canonical assistant-review producer" + break + elif [[ "$change_impact_skill" == "assistant-review" ]] && { [[ -d "$change_impact_home/.codex/skills/assistant-debugging" ]] || [[ -d "$change_impact_home/.codex/skills/assistant-workflow" ]]; }; then + change_impact_install_failure="assistant-review selective installation copied an unrelated consumer" + break + elif ! node -e 'const fs=require("node:fs"); const source=JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const target=process.argv[2]; for (const [name, value] of Object.entries(source)) fs.writeFileSync(`${target}/${name}.json`, JSON.stringify(value));' "$FRAMEWORK_DIR/tools/change-impact/example.completion.v1.json" "$change_impact_case"; then + change_impact_install_failure="$change_impact_skill could not materialize the valid checker fixture" + break + elif ! node "$change_impact_tool" --phase completion --capture "$change_impact_case/capture.json" --expected "$change_impact_case/expected.json" --assessment "$change_impact_case/assessment.json" --review "$change_impact_case/review.json" >/tmp/p0p4-change-impact-${change_impact_skill}-valid.out; then + change_impact_install_failure="$change_impact_skill installed checker rejected its valid protocol example" + break + elif ! node -e 'const fs=require("node:fs"); const p=process.argv[1]; const value=JSON.parse(fs.readFileSync(p,"utf8")); const edge=value.edges.pop(); value.requirements=value.requirements.filter((requirement) => requirement.edge_id !== edge.id); fs.writeFileSync(p,JSON.stringify(value));' "$change_impact_case/capture.json"; then + change_impact_install_failure="$change_impact_skill could not materialize the consumer-omission fixture" + break + elif node "$change_impact_tool" --phase completion --capture "$change_impact_case/capture.json" --expected "$change_impact_case/expected.json" --assessment "$change_impact_case/assessment.json" --review "$change_impact_case/review.json" >/tmp/p0p4-change-impact-${change_impact_skill}-consumer-omission.out; then + change_impact_install_failure="$change_impact_skill installed checker accepted a capture missing a known consumer" + break + elif ! grep -Fq 'CAPTURE_EXPECTED_EDGES_MISMATCH' /tmp/p0p4-change-impact-${change_impact_skill}-consumer-omission.out; then + change_impact_install_failure="$change_impact_skill consumer omission did not fail against independent expected truth" + break + elif ! node -e 'const fs=require("node:fs"); const source=JSON.parse(fs.readFileSync(process.argv[1], "utf8")); fs.writeFileSync(process.argv[2],JSON.stringify(source.capture));' "$FRAMEWORK_DIR/tools/change-impact/example.completion.v1.json" "$change_impact_case/capture.json"; then + change_impact_install_failure="$change_impact_skill could not restore the root-omission fixture" + break + elif ! node -e 'const fs=require("node:fs"); const p=process.argv[1]; const value=JSON.parse(fs.readFileSync(p,"utf8")); value.roots=[]; fs.writeFileSync(p,JSON.stringify(value));' "$change_impact_case/capture.json"; then + change_impact_install_failure="$change_impact_skill could not materialize the omission fixture" + break + elif node "$change_impact_tool" --phase completion --capture "$change_impact_case/capture.json" --expected "$change_impact_case/expected.json" --assessment "$change_impact_case/assessment.json" --review "$change_impact_case/review.json" >/tmp/p0p4-change-impact-${change_impact_skill}-omission.out; then + change_impact_install_failure="$change_impact_skill installed checker accepted an empty shared discovery-root capture" + break + elif ! grep -Fq 'CAPTURE_DISCOVERY_ROOTS_MISSING' /tmp/p0p4-change-impact-${change_impact_skill}-omission.out; then + change_impact_install_failure="$change_impact_skill omission did not fail for the missing discovery-root reason" + break + fi +done +if [[ -z "$change_impact_install_failure" ]]; then + pass +else + fail "$change_impact_install_failure" +fi + p0p4_finish_suite "${BASH_SOURCE[0]}" diff --git a/tests/p0-p4/lib/feature-preparation-case-oracle.sh b/tests/p0-p4/lib/feature-preparation-case-oracle.sh index 0934626d..6fa0d693 100644 --- a/tests/p0-p4/lib/feature-preparation-case-oracle.sh +++ b/tests/p0-p4/lib/feature-preparation-case-oracle.sh @@ -14,6 +14,7 @@ readonly FEATURE_PREP_EXPECTED_CASE_RECORDS=( 'viewing-route-preserves-active-behavior|light|none' 'feature-preparation-counterclassifies-unknown-conflict-and-gap|light|none' 'medium-prepare-only-terminal-route|medium|none' + 'prepare-only-shared-impact-retains-discovery-evidence|medium|none' 'large-prepare-only-terminal-route|large|none' 'medium-prepare-only-readiness-plan|medium|inline' 'medium-prepare-only-not-applicable-readiness-plan|medium|inline' diff --git a/tests/p0-p4/lib/feature-preparation-response-fixtures.sh b/tests/p0-p4/lib/feature-preparation-response-fixtures.sh index e7cc5153..b7a3a005 100644 --- a/tests/p0-p4/lib/feature-preparation-response-fixtures.sh +++ b/tests/p0-p4/lib/feature-preparation-response-fixtures.sh @@ -32,6 +32,7 @@ readonly FEATURE_PREP_CASE_MANIFEST=( 'viewing-route-preserves-active-behavior|light|none' 'feature-preparation-counterclassifies-unknown-conflict-and-gap|light|none' 'medium-prepare-only-terminal-route|medium|none' + 'prepare-only-shared-impact-retains-discovery-evidence|medium|none' 'large-prepare-only-terminal-route|large|none' 'medium-prepare-only-readiness-plan|medium|inline' 'medium-prepare-only-not-applicable-readiness-plan|medium|inline' @@ -408,6 +409,68 @@ build_medium_prepare_only_terminal_response() { build_medium_prepare_only_response "$response_path" "$summary" none } +build_medium_prepare_only_shared_impact_response() { + local response_path="$1" + local summary="$2" + local temporary_response="${response_path}.tmp" + + build_medium_prepare_only_response "$response_path" "$summary" none + jq ' + .change_impact_applicability = { + impact_scope: "shared", + applicability_reason: "The navigation helper is shared by the captured consumers.", + expanded_artifact_carried: true + } + | .change_impact_evidence = { + artifact_identity: "capture-prepare-only-current", + phase: "discovery", + impact_scope: "shared", + status: "valid", + validator_result_ref: "change-impact#discovery", + capture_ref: "change-impact#capture", + expected_context_ref: "change-impact#expected" + } + ' "$response_path" >"$temporary_response" + mv "$temporary_response" "$response_path" +} + +build_local_debugging_compact_causal_response() { + local response_path="$1" + local summary="$2" + + jq -n --arg summary "$summary" ' + { + summary: $summary, + status: "root_cause_found", + symptom_summary: "The local draft-navigation route loses its draft when the guard runs; the draft should remain available.", + reproduction: { + target: "local draft-navigation guard", + command_or_steps: "Inspect the supplied route trace for draft preservation through the guard.", + observed_result: "The supplied trace reaches the guard only on this route and shows the local draft is not preserved.", + reproduced: "yes" + }, + hypotheses: [ + {rank: 1, cause: "The route guard omits the local draft check.", supporting_evidence: "The supplied trace reaches this guard before the draft is lost.", disconfirming_evidence: "A trace showing draft preservation through the same guard would refute this cause.", diagnostic_check: "Inspect the supplied route trace at the guard boundary.", result: "confirmed"}, + {rank: 2, cause: "A different route clears the draft before the guard.", supporting_evidence: "A route transition could clear draft state.", disconfirming_evidence: "The supplied trace confines the loss to the inspected guard path.", diagnostic_check: "Compare the supplied trace before and after the guard.", result: "refuted"}, + {rank: 3, cause: "Draft storage fails independently of navigation.", supporting_evidence: "Draft storage is a possible local dependency.", disconfirming_evidence: "The supplied trace identifies the guard as the first loss point.", diagnostic_check: "Inspect the supplied trace for storage writes before the guard.", result: "refuted"} + ], + root_cause: "The route guard omits the local draft check.", + confidence: "medium", + verification: [ + {command_or_check: "Inspect the supplied causal trace", result: "passed", evidence: "The trace proves only this route reaches the guard and identifies the guard as the draft-loss point."}, + {command_or_check: "Run the local draft-navigation regression after a repair", result: "skipped", evidence: "No repair was performed during this diagnosis-only result."} + ], + residual_risks: ["No repair or draft-navigation regression has run; execute the focused regression before claiming the bug is fixed."], + change_impact_applicability: { + impact_scope: "local", + applicability_reason: "Only the inspected route reaches this guard.", + causal_evidence_ref: "tests/local-route-draft", + expanded_artifact_carried: false + } + } + ' >"$response_path" +} + build_medium_prepare_only_readiness_plan_response() { local response_path="$1" local summary="$2" @@ -1397,15 +1460,15 @@ build_workflow_review_lifecycle_eval_response() { case "$case_id" in standard-pack-review-result-retains-checklist) jq -n --arg summary "$summary" --argjson identity "$identity" ' - {summary:$summary,current_assistant_review_contract:{schema_version:"7.1"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.1",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",architecture_decision_pack_review_ref:"journal#pack-review",architecture_decision_pack_review_contract:"assistant-review/contracts/output.yaml#architecture_decision_pack_review",validation_status:"validated"}}' >"$response_path" + {summary:$summary,current_assistant_review_contract:{schema_version:"7.2"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.2",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",architecture_decision_pack_review_ref:"journal#pack-review",architecture_decision_pack_review_contract:"assistant-review/contracts/output.yaml#architecture_decision_pack_review",validation_status:"validated"}}' >"$response_path" ;; light-pack-review-result-retains-current-snapshot) jq -n --arg summary "$summary" --argjson identity "$identity" ' - {summary:$summary,current_assistant_review_contract:{schema_version:"7.1"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},fresh_review_result:{result:"PASS",reviewed_scope:"changed workflow contract surface",findings:[],evidence:"focused validation passed",canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.1",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,architecture_decision_pack_review_ref:"journal#pack-review",architecture_decision_pack_review_contract:"assistant-review/contracts/output.yaml#architecture_decision_pack_review",validation_status:"validated"}}' >"$response_path" + {summary:$summary,current_assistant_review_contract:{schema_version:"7.2"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},fresh_review_result:{result:"PASS",reviewed_scope:"changed workflow contract surface",findings:[],evidence:"focused validation passed",canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.2",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,architecture_decision_pack_review_ref:"journal#pack-review",architecture_decision_pack_review_contract:"assistant-review/contracts/output.yaml#architecture_decision_pack_review",validation_status:"validated"}}' >"$response_path" ;; incomplete-review-blocks-clean-final-handoff) jq -n --arg summary "$summary" --argjson identity "$identity" ' - {summary:$summary,current_assistant_review_contract:{schema_version:"7.1"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",result:"HAS_REMAINING_ITEMS",coverage_complete:false,final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.1",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",validation_status:"validated"},final_handoff:{changed_behavior_and_areas:["Review completion routing"],architecture_decisions_and_rationale:["assistant-review remains canonical"],rejected_alternatives_and_tradeoffs:["Do not duplicate the review schema"],requirement_evidence:["Canonical review terminal evidence"],automated_verification:["Focused contract suite"],manual_test_scenarios:["N/A: contract-only change"],compatibility_and_regression_surfaces:["final_review_snapshot_id compatibility alias"],known_limitations_and_untested_areas:["No runtime application surface"],rollback_or_recovery:"Revert the workflow contract projection.",review_completion:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",review_producer_schema_version:"7.1",result:"HAS_REMAINING_ITEMS",coverage_complete:false,final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,completion_disposition:"remaining_items",remaining_or_blocker_summary:"Review has remaining items or incomplete coverage; resolve the remaining review items."},review_claim:"Review has remaining items or incomplete coverage; workflow completion is blocked."}}' >"$response_path" + {summary:$summary,current_assistant_review_contract:{schema_version:"7.2"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",result:"HAS_REMAINING_ITEMS",coverage_complete:false,final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.2",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",validation_status:"validated"},final_handoff:{changed_behavior_and_areas:["Review completion routing"],architecture_decisions_and_rationale:["assistant-review remains canonical"],rejected_alternatives_and_tradeoffs:["Do not duplicate the review schema"],requirement_evidence:["Canonical review terminal evidence"],automated_verification:["Focused contract suite"],manual_test_scenarios:["N/A: contract-only change"],compatibility_and_regression_surfaces:["final_review_snapshot_id compatibility alias"],known_limitations_and_untested_areas:["No runtime application surface"],rollback_or_recovery:"Revert the workflow contract projection.",review_completion:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",review_producer_schema_version:"7.2",result:"HAS_REMAINING_ITEMS",coverage_complete:false,final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,completion_disposition:"remaining_items",remaining_or_blocker_summary:"Review has remaining items or incomplete coverage; resolve the remaining review items."},review_claim:"Review has remaining items or incomplete coverage; workflow completion is blocked."}}' >"$response_path" ;; blocked-qa-blocks-clean-final-handoff|rejected-qa-blocks-clean-final-handoff) local qa_verdict qa_result disposition blocker review_claim @@ -1419,7 +1482,7 @@ build_workflow_review_lifecycle_eval_response() { review_claim="QA has remaining acceptance items; workflow completion is blocked until they are resolved." fi jq -n --arg summary "$summary" --argjson identity "$identity" --arg verdict "$qa_verdict" --arg result "$qa_result" --arg disposition "$disposition" --arg blocker "$blocker" --arg review_claim "$review_claim" ' - {summary:$summary,current_assistant_review_contract:{schema_version:"7.1"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},canonical_qa_result:{canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:$verdict,result:$result},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.1",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",validation_status:"validated"},qa_evaluation_result:{canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",producer_schema_version:"7.1",delegation_path_ref:"journal#qa-delegation",delegation_contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",validation_status:"validated"},final_handoff:{changed_behavior_and_areas:["Review completion routing"],architecture_decisions_and_rationale:["assistant-review remains canonical"],rejected_alternatives_and_tradeoffs:["Do not duplicate the review schema"],requirement_evidence:["Canonical review and QA terminal evidence"],automated_verification:["Focused contract suite"],manual_test_scenarios:["N/A: contract-only change"],compatibility_and_regression_surfaces:["final_review_snapshot_id compatibility alias"],known_limitations_and_untested_areas:["No runtime application surface"],rollback_or_recovery:"Revert the workflow contract projection.",review_completion:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",review_producer_schema_version:"7.1",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,qa_evaluation_result_ref:"journal#qa-result",qa_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",qa_producer_schema_version:"7.1",qa_final_verdict:$verdict,qa_result:$result,completion_disposition:$disposition,remaining_or_blocker_summary:$blocker},review_claim:$review_claim}}' >"$response_path" + {summary:$summary,current_assistant_review_contract:{schema_version:"7.2"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},canonical_qa_result:{canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:$verdict,result:$result},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.2",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",validation_status:"validated"},qa_evaluation_result:{canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",producer_schema_version:"7.2",delegation_path_ref:"journal#qa-delegation",delegation_contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",validation_status:"validated"},final_handoff:{changed_behavior_and_areas:["Review completion routing"],architecture_decisions_and_rationale:["assistant-review remains canonical"],rejected_alternatives_and_tradeoffs:["Do not duplicate the review schema"],requirement_evidence:["Canonical review and QA terminal evidence"],automated_verification:["Focused contract suite"],manual_test_scenarios:["N/A: contract-only change"],compatibility_and_regression_surfaces:["final_review_snapshot_id compatibility alias"],known_limitations_and_untested_areas:["No runtime application surface"],rollback_or_recovery:"Revert the workflow contract projection.",review_completion:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",review_producer_schema_version:"7.2",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,qa_evaluation_result_ref:"journal#qa-result",qa_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",qa_producer_schema_version:"7.2",qa_final_verdict:$verdict,qa_result:$result,completion_disposition:$disposition,remaining_or_blocker_summary:$blocker},review_claim:$review_claim}}' >"$response_path" ;; fulfilled-preparation-qa-obligation-allows-completion|fulfilled-not-applicable-preparation-qa-obligation-allows-concern-completion) local qa_verdict qa_result source_binding obligation_binding preparation_scope @@ -1435,15 +1498,15 @@ build_workflow_review_lifecycle_eval_response() { obligation_binding='{"source_feature_preparation_evidence_ref":"prep/viewing-route"}' fi jq -n --arg summary "$summary" --argjson identity "$identity" --arg verdict "$qa_verdict" --arg result "$qa_result" --arg scope "$preparation_scope" --argjson source "$source_binding" --argjson obligation "$obligation_binding" ' - {summary:$summary,feature_preparation_scope:$scope,current_assistant_review_contract:{schema_version:"7.1"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.1",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",validation_status:"validated"},approved_feature_preparation_qa_acceptance_obligation:({requested_scope:"Run the requested acceptance QA.",execution_prerequisite:"Implementation and tests are complete.",feature_preparation_scope:$scope}+$obligation),canonical_qa_result:({canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:$verdict,result:$result,approved_feature_preparation_qa_acceptance_obligation_result_ref:"journal#qa-result/obligation",approved_feature_preparation_qa_acceptance_obligation_result:({requested_scope_status:"fulfilled",requested_scope_evidence:"Acceptance criteria were evaluated.",execution_prerequisite_status:"met",execution_prerequisite_evidence:"Implementation and tests passed.",requested_scope:"Run the requested acceptance QA.",execution_prerequisite:"Implementation and tests are complete.",feature_preparation_scope:$scope}+$source)}),qa_evaluation_result:{canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",producer_schema_version:"7.1",approved_feature_preparation_qa_acceptance_obligation_result_ref:"journal#qa-result/obligation",delegation_path_ref:"journal#qa-delegation",delegation_contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",validation_status:"validated"},final_handoff:{changed_behavior_and_areas:["Deferred QA acceptance routing"],architecture_decisions_and_rationale:["assistant-review remains canonical"],rejected_alternatives_and_tradeoffs:["Do not duplicate the QA schema"],requirement_evidence:["Canonical obligation result"],automated_verification:["Focused contract suite"],manual_test_scenarios:["N/A: contract-only change"],compatibility_and_regression_surfaces:["Both preparation source branches"],known_limitations_and_untested_areas:["No runtime application surface"],rollback_or_recovery:"Revert the workflow projection.",review_completion:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",review_producer_schema_version:"7.1",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,qa_evaluation_result_ref:"journal#qa-result",qa_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",qa_producer_schema_version:"7.1",qa_final_verdict:$verdict,qa_result:$result,approved_feature_preparation_qa_acceptance_obligation_result_ref:"journal#qa-result/obligation",qa_obligation_requested_scope_status:"fulfilled",qa_obligation_execution_prerequisite_status:"met",qa_obligation_source_binding_verified:true,completion_disposition:"complete"},review_claim:"No material findings within the reviewed scope and available evidence"},workflow_complete:"--- WORKFLOW COMPLETE ---"}' >"$response_path" + {summary:$summary,feature_preparation_scope:$scope,current_assistant_review_contract:{schema_version:"7.2"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.2",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",validation_status:"validated"},approved_feature_preparation_qa_acceptance_obligation:({requested_scope:"Run the requested acceptance QA.",execution_prerequisite:"Implementation and tests are complete.",feature_preparation_scope:$scope}+$obligation),canonical_qa_result:({canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:$verdict,result:$result,approved_feature_preparation_qa_acceptance_obligation_result_ref:"journal#qa-result/obligation",approved_feature_preparation_qa_acceptance_obligation_result:({requested_scope_status:"fulfilled",requested_scope_evidence:"Acceptance criteria were evaluated.",execution_prerequisite_status:"met",execution_prerequisite_evidence:"Implementation and tests passed.",requested_scope:"Run the requested acceptance QA.",execution_prerequisite:"Implementation and tests are complete.",feature_preparation_scope:$scope}+$source)}),qa_evaluation_result:{canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",producer_schema_version:"7.2",approved_feature_preparation_qa_acceptance_obligation_result_ref:"journal#qa-result/obligation",delegation_path_ref:"journal#qa-delegation",delegation_contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",validation_status:"validated"},final_handoff:{changed_behavior_and_areas:["Deferred QA acceptance routing"],architecture_decisions_and_rationale:["assistant-review remains canonical"],rejected_alternatives_and_tradeoffs:["Do not duplicate the QA schema"],requirement_evidence:["Canonical obligation result"],automated_verification:["Focused contract suite"],manual_test_scenarios:["N/A: contract-only change"],compatibility_and_regression_surfaces:["Both preparation source branches"],known_limitations_and_untested_areas:["No runtime application surface"],rollback_or_recovery:"Revert the workflow projection.",review_completion:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",review_producer_schema_version:"7.2",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,qa_evaluation_result_ref:"journal#qa-result",qa_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",qa_producer_schema_version:"7.2",qa_final_verdict:$verdict,qa_result:$result,approved_feature_preparation_qa_acceptance_obligation_result_ref:"journal#qa-result/obligation",qa_obligation_requested_scope_status:"fulfilled",qa_obligation_execution_prerequisite_status:"met",qa_obligation_source_binding_verified:true,completion_disposition:"complete"},review_claim:"No material findings within the reviewed scope and available evidence"},workflow_complete:"--- WORKFLOW COMPLETE ---"}' >"$response_path" ;; qa-reject-source-fix-requires-rebuild-review-before-resume) jq -n --arg summary "$summary" --argjson identity "$identity" ' - {summary:$summary,current_assistant_review_contract:{schema_version:"7.1"},prior_canonical_qa_result:{canonical_result_ref:"journal#qa-rejected",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:"rejected",result:"HAS_REMAINING_ITEMS"},fresh_canonical_final_summary:{canonical_result_ref:"journal#review-post-fix",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",coverage_complete:true,final_snapshot_identity:$identity},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},post_fix_build_validation:{ref:"validation#post-fix",status:"passed",source_digest:"post-fix-digest",evidence:"Post-fix Build validation passed against the rehashed source."},post_rejection_digest_evidence:{ref:"digest#post-fix",comparison:"changed",pre_fix_source_digest:"pre-fix-digest",post_fix_source_digest:"post-fix-digest",post_fix_snapshot_identity:$identity},current_canonical_qa_result:{canonical_result_ref:"journal#qa-resumed",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:"accepted",result:"CLEAN"},current_qa_delegation_path:{ref:"journal#qa-delegation",contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",artifact:{subagent_policy_state:"delegation_triggered",subagent_execution_mode:"delegated",subagent_trigger_scope:["post-rejection QA resume"],fresh_context_evidence:"Fresh QAEvaluator context after post-fix review."}},qa_evaluation_result:{canonical_result_ref:"journal#qa-resumed",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",producer_schema_version:"7.1",delegation_path_ref:"journal#qa-delegation",delegation_contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",validation_status:"validated",rejection_recovery:{rejected_qa_result_ref:"journal#qa-rejected",pre_fix_source_digest:"pre-fix-digest",post_fix_source_digest:"post-fix-digest",source_digest_comparison:"changed",build_validation_ref:"validation#post-fix",rehash_evidence_ref:"digest#post-fix",fresh_review_result_ref:"journal#review-post-fix",fresh_review_snapshot_identity:$identity,fresh_review_coverage_complete:true,qa_resume_authorized:true}}}' >"$response_path" + {summary:$summary,current_assistant_review_contract:{schema_version:"7.2"},prior_canonical_qa_result:{canonical_result_ref:"journal#qa-rejected",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:"rejected",result:"HAS_REMAINING_ITEMS"},fresh_canonical_final_summary:{canonical_result_ref:"journal#review-post-fix",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",coverage_complete:true,final_snapshot_identity:$identity},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},post_fix_build_validation:{ref:"validation#post-fix",status:"passed",source_digest:"post-fix-digest",evidence:"Post-fix Build validation passed against the rehashed source."},post_rejection_digest_evidence:{ref:"digest#post-fix",comparison:"changed",pre_fix_source_digest:"pre-fix-digest",post_fix_source_digest:"post-fix-digest",post_fix_snapshot_identity:$identity},current_canonical_qa_result:{canonical_result_ref:"journal#qa-resumed",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:"accepted",result:"CLEAN"},current_qa_delegation_path:{ref:"journal#qa-delegation",contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",artifact:{subagent_policy_state:"delegation_triggered",subagent_execution_mode:"delegated",subagent_trigger_scope:["post-rejection QA resume"],fresh_context_evidence:"Fresh QAEvaluator context after post-fix review."}},qa_evaluation_result:{canonical_result_ref:"journal#qa-resumed",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",producer_schema_version:"7.2",delegation_path_ref:"journal#qa-delegation",delegation_contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",validation_status:"validated",rejection_recovery:{rejected_qa_result_ref:"journal#qa-rejected",pre_fix_source_digest:"pre-fix-digest",post_fix_source_digest:"post-fix-digest",source_digest_comparison:"changed",build_validation_ref:"validation#post-fix",rehash_evidence_ref:"digest#post-fix",fresh_review_result_ref:"journal#review-post-fix",fresh_review_snapshot_identity:$identity,fresh_review_coverage_complete:true,qa_resume_authorized:true}}}' >"$response_path" ;; qa-reject-unchanged-source-allows-resume-with-digest-equality) jq -n --arg summary "$summary" ' - {summary:$summary,current_assistant_review_contract:{schema_version:"7.1"},prior_canonical_qa_result:{canonical_result_ref:"journal#qa-rejected",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:"rejected",result:"HAS_REMAINING_ITEMS"},post_rejection_digest_evidence:{ref:"digest#equal-source",comparison:"equal",pre_fix_source_digest:"unchanged-source-digest",post_fix_source_digest:"unchanged-source-digest"},current_canonical_qa_result:{canonical_result_ref:"journal#qa-resumed",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:"accepted",result:"CLEAN"},current_qa_delegation_path:{ref:"journal#qa-delegation",contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",artifact:{subagent_policy_state:"delegation_triggered",subagent_execution_mode:"delegated",subagent_trigger_scope:["unchanged-source QA resume"],fresh_context_evidence:"Fresh QAEvaluator context after digest-equality validation."}},qa_evaluation_result:{canonical_result_ref:"journal#qa-resumed",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",producer_schema_version:"7.1",delegation_path_ref:"journal#qa-delegation",delegation_contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",validation_status:"validated",rejection_recovery:{rejected_qa_result_ref:"journal#qa-rejected",pre_fix_source_digest:"unchanged-source-digest",post_fix_source_digest:"unchanged-source-digest",source_digest_comparison:"equal",digest_equality_evidence_ref:"digest#equal-source",rehash_evidence_ref:"digest#equal-source",qa_resume_authorized:true}}}' >"$response_path" + {summary:$summary,current_assistant_review_contract:{schema_version:"7.2"},prior_canonical_qa_result:{canonical_result_ref:"journal#qa-rejected",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:"rejected",result:"HAS_REMAINING_ITEMS"},post_rejection_digest_evidence:{ref:"digest#equal-source",comparison:"equal",pre_fix_source_digest:"unchanged-source-digest",post_fix_source_digest:"unchanged-source-digest"},current_canonical_qa_result:{canonical_result_ref:"journal#qa-resumed",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:"accepted",result:"CLEAN"},current_qa_delegation_path:{ref:"journal#qa-delegation",contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",artifact:{subagent_policy_state:"delegation_triggered",subagent_execution_mode:"delegated",subagent_trigger_scope:["unchanged-source QA resume"],fresh_context_evidence:"Fresh QAEvaluator context after digest-equality validation."}},qa_evaluation_result:{canonical_result_ref:"journal#qa-resumed",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",producer_schema_version:"7.2",delegation_path_ref:"journal#qa-delegation",delegation_contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",validation_status:"validated",rejection_recovery:{rejected_qa_result_ref:"journal#qa-rejected",pre_fix_source_digest:"unchanged-source-digest",post_fix_source_digest:"unchanged-source-digest",source_digest_comparison:"equal",digest_equality_evidence_ref:"digest#equal-source",rehash_evidence_ref:"digest#equal-source",qa_resume_authorized:true}}}' >"$response_path" ;; small-strict-blocked-qa-requires-terminal-projection|small-required-rejected-qa-requires-terminal-projection) local qa_verdict qa_result disposition blocker @@ -1453,7 +1516,7 @@ build_workflow_review_lifecycle_eval_response() { qa_verdict="rejected"; qa_result="HAS_REMAINING_ITEMS"; disposition="remaining_items"; blocker="QA has remaining acceptance items; resolve and rerun QA." fi jq -n --arg summary "$summary" --argjson identity "$identity" --arg verdict "$qa_verdict" --arg result "$qa_result" --arg disposition "$disposition" --arg blocker "$blocker" ' - {summary:$summary,triage_result:{size:"small",controller_intensity:"strict",qa_evaluation_mode:"required"},current_assistant_review_contract:{schema_version:"7.1"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},canonical_qa_result:{canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:$verdict,result:$result},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.1",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",validation_status:"validated"},qa_evaluation_result:{canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",producer_schema_version:"7.1",delegation_path_ref:"journal#qa-delegation",delegation_contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",validation_status:"validated"},final_handoff:{changed_behavior_and_areas:["Small elevated completion routing"],architecture_decisions_and_rationale:["Use typed terminal projection"],rejected_alternatives_and_tradeoffs:["Do not fabricate medium slices"],requirement_evidence:["Canonical review and QA state"],automated_verification:["Focused contract suite"],manual_test_scenarios:["N/A: contract-only change"],compatibility_and_regression_surfaces:["Small strict and required-QA work"],known_limitations_and_untested_areas:["No runtime application surface"],rollback_or_recovery:"Revert the workflow projection.",review_completion:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",review_producer_schema_version:"7.1",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,qa_evaluation_result_ref:"journal#qa-result",qa_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",qa_producer_schema_version:"7.1",qa_final_verdict:$verdict,qa_result:$result,completion_disposition:$disposition,remaining_or_blocker_summary:$blocker},review_claim:($blocker + " Workflow completion is blocked.")}}' >"$response_path" + {summary:$summary,triage_result:{size:"small",controller_intensity:"strict",qa_evaluation_mode:"required"},current_assistant_review_contract:{schema_version:"7.2"},canonical_final_summary:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity},canonical_qa_result:{canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",final_verdict:$verdict,result:$result},current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.2",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",validation_status:"validated"},qa_evaluation_result:{canonical_result_ref:"journal#qa-result",canonical_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",producer_schema_version:"7.2",delegation_path_ref:"journal#qa-delegation",delegation_contract:"assistant-review/contracts/output.yaml#qa_evaluation_delegation_path",validation_status:"validated"},final_handoff:{changed_behavior_and_areas:["Small elevated completion routing"],architecture_decisions_and_rationale:["Use typed terminal projection"],rejected_alternatives_and_tradeoffs:["Do not fabricate medium slices"],requirement_evidence:["Canonical review and QA state"],automated_verification:["Focused contract suite"],manual_test_scenarios:["N/A: contract-only change"],compatibility_and_regression_surfaces:["Small strict and required-QA work"],known_limitations_and_untested_areas:["No runtime application surface"],rollback_or_recovery:"Revert the workflow projection.",review_completion:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",review_producer_schema_version:"7.2",result:"CLEAN",coverage_complete:true,evidence_bounded_claim:"No material findings within the reviewed scope and available evidence",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,qa_evaluation_result_ref:"journal#qa-result",qa_contract:"assistant-review/contracts/output.yaml#qa_evaluation_result",qa_producer_schema_version:"7.2",qa_final_verdict:$verdict,qa_result:$result,completion_disposition:$disposition,remaining_or_blocker_summary:$blocker},review_claim:($blocker + " Workflow completion is blocked.")}}' >"$response_path" ;; post-fix-review-closure-allows-issues-fixed-completion|post-fix-review-regression-remains-open) local closure_status closure_result completion_disposition review_claim remaining_summary aggregated_findings aggregation_ledger closure_coverage_disposition closure_finding_ids @@ -1479,11 +1542,11 @@ build_workflow_review_lifecycle_eval_response() { closure_finding_ids='["review_pass:pass-closure:finding-workflow-regressed"]' fi jq -n --arg summary "$summary" --argjson identity "$identity" --arg closure_status "$closure_status" --arg result "$closure_result" --arg disposition "$completion_disposition" --arg claim "$review_claim" --arg remaining_summary "$remaining_summary" --arg coverage_disposition "$closure_coverage_disposition" --argjson closure_finding_ids "$closure_finding_ids" --argjson findings "$aggregated_findings" --argjson aggregation "$aggregation_ledger" ' - {summary:$summary,current_assistant_review_contract:{schema_version:"7.1"},canonical_final_summary:({canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,coverage_complete:true,final_batch_plan:{batch_id:"batch-current",review_snapshot_id:"review-current",scope_size:"small",topology:{discovery_pass_count:2,canonical_discovery_perspectives:{trivial_small:["contract_and_test_oracle","runtime_lifecycle_and_failure_paths"],medium:["contract_and_test_oracle","runtime_lifecycle_and_failure_paths","integration_compatibility_and_consumers"],large:["contract_and_test_oracle","runtime_lifecycle_and_failure_paths","integration_compatibility_and_consumers","architecture_maintainability_and_reuse"]},security_specialist_triggered:false,closure_verification_required:true,max_required_responses:3,max_repair_attempts_per_pass:1},expected_passes:[{review_pass_id:"pass-consumer",perspective:"contract_and_test_oracle",assigned_scope:["workflow-terminal-evidence"],coverage_obligations:["terminal evidence binding"],prior_finding_visibility:"none"},{review_pass_id:"pass-failure-paths",perspective:"runtime_lifecycle_and_failure_paths",assigned_scope:["workflow-terminal-evidence"],coverage_obligations:["fail-closed terminal evidence"],prior_finding_visibility:"none"},{review_pass_id:"pass-closure",perspective:"closure_verification",assigned_scope:["workflow-fixed-history"],coverage_obligations:["verify immutable fixed closure"],prior_finding_visibility:"closure_ledger"}],required_coverage_tuples:[{review_pass_id:"pass-consumer",scope_item_id:"workflow-terminal-evidence",applicable_concern:"canonical producer consumption",review_perspective:"contract_and_test_oracle",coverage_obligation:"terminal evidence binding"},{review_pass_id:"pass-failure-paths",scope_item_id:"workflow-terminal-evidence",applicable_concern:"canonical producer failure handling",review_perspective:"runtime_lifecycle_and_failure_paths",coverage_obligation:"fail-closed terminal evidence"},{review_pass_id:"pass-closure",scope_item_id:"workflow-fixed-history",applicable_concern:"immutable fixed finding closure",review_perspective:"closure_verification",coverage_obligation:"verify immutable fixed closure"}]},coverage_ledger:[{batch_id:"batch-current",review_snapshot_id:"review-current",review_pass_id:"pass-consumer",perspective:"contract_and_test_oracle",coverage_obligation:"terminal evidence binding",assigned_scope:["workflow-terminal-evidence"],scope_item_id:"workflow-terminal-evidence",applicable_concern:"canonical producer consumption",terminal_state:"completed",coverage_status:"complete",coverage_disposition:"inspected_no_risk",evidence:"Focused lifecycle validation passed."},{batch_id:"batch-current",review_snapshot_id:"review-current",review_pass_id:"pass-failure-paths",perspective:"runtime_lifecycle_and_failure_paths",coverage_obligation:"fail-closed terminal evidence",assigned_scope:["workflow-terminal-evidence"],scope_item_id:"workflow-terminal-evidence",applicable_concern:"canonical producer failure handling",terminal_state:"completed",coverage_status:"complete",coverage_disposition:"inspected_no_risk",evidence:"Focused lifecycle mutation validation passed."},{batch_id:"batch-current",review_snapshot_id:"review-current",review_pass_id:"pass-closure",perspective:"closure_verification",coverage_obligation:"verify immutable fixed closure",assigned_scope:["workflow-fixed-history"],scope_item_id:"workflow-fixed-history",applicable_concern:"immutable fixed finding closure",terminal_state:"completed",coverage_status:"complete",coverage_disposition:$coverage_disposition,evidence:"Closure verification completed."} + (if $coverage_disposition == "finding" then {finding_ids:$closure_finding_ids} else {} end)],batch_summaries:[{started_batch_ordinal:1,batch_id:"batch-current",review_snapshot_id:"review-current",snapshot_identity:$identity,batch_status:"complete",expected_response_count:3,terminal_response_count:3,aggregate_rubric_recomputed:true}],aggregation_ledger:$aggregation,aggregated_findings:$findings,result:$result,fixed_items:[{aggregate_finding_id:"aggregate-fixed-workflow",severity:"must-fix",file:"skills/assistant-workflow/contracts/output.yaml",description:"Original workflow closure finding was repaired.",fixed_in_round:2}],closure_results:[{aggregate_finding_id:"aggregate-fixed-workflow",status:$closure_status,evidence:(if $closure_status == "verified_closed" then "Closure pass verified the repaired workflow finding." else "Closure pass found the original workflow finding regressed." end)}],nits:[]} + (if $result == "ISSUES_FIXED" then {evidence_bounded_claim:"No material findings within the reviewed scope and available evidence"} else {remaining_items:[{severity:"must-fix",file:"skills/assistant-workflow/contracts/output.yaml",description:"Original workflow closure finding regressed after the fix.",reason_unresolved:"Closure verification returned regressed."}]} end)),current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.1",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",validation_status:"validated"},final_handoff:({changed_behavior_and_areas:["Workflow closure consumer projection"],architecture_decisions_and_rationale:["assistant-review fixed-history contracts remain canonical"],rejected_alternatives_and_tradeoffs:["Do not rewrite immutable closure identifiers"],requirement_evidence:["Canonical closure result and aggregation ledger"],automated_verification:["Focused workflow lifecycle eval"],manual_test_scenarios:["N/A: contract-only change"],compatibility_and_regression_surfaces:["assistant-review v7.1 fixed-history output"],known_limitations_and_untested_areas:["No runtime application surface"],rollback_or_recovery:"Revert the workflow consumer projection.",review_completion:({canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",review_producer_schema_version:"7.1",result:$result,coverage_complete:true,final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,completion_disposition:$disposition} + (if $result == "ISSUES_FIXED" then {evidence_bounded_claim:"No material findings within the reviewed scope and available evidence"} else {remaining_or_blocker_summary:$remaining_summary} end)),review_claim:$claim})}' >"$response_path" + {summary:$summary,current_assistant_review_contract:{schema_version:"7.2"},canonical_final_summary:({canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,coverage_complete:true,final_batch_plan:{batch_id:"batch-current",review_snapshot_id:"review-current",scope_size:"small",topology:{discovery_pass_count:2,canonical_discovery_perspectives:{trivial_small:["contract_and_test_oracle","runtime_lifecycle_and_failure_paths"],medium:["contract_and_test_oracle","runtime_lifecycle_and_failure_paths","integration_compatibility_and_consumers"],large:["contract_and_test_oracle","runtime_lifecycle_and_failure_paths","integration_compatibility_and_consumers","architecture_maintainability_and_reuse"]},security_specialist_triggered:false,closure_verification_required:true,max_required_responses:3,max_repair_attempts_per_pass:1},expected_passes:[{review_pass_id:"pass-consumer",perspective:"contract_and_test_oracle",assigned_scope:["workflow-terminal-evidence"],coverage_obligations:["terminal evidence binding"],prior_finding_visibility:"none"},{review_pass_id:"pass-failure-paths",perspective:"runtime_lifecycle_and_failure_paths",assigned_scope:["workflow-terminal-evidence"],coverage_obligations:["fail-closed terminal evidence"],prior_finding_visibility:"none"},{review_pass_id:"pass-closure",perspective:"closure_verification",assigned_scope:["workflow-fixed-history"],coverage_obligations:["verify immutable fixed closure"],prior_finding_visibility:"closure_ledger"}],required_coverage_tuples:[{review_pass_id:"pass-consumer",scope_item_id:"workflow-terminal-evidence",applicable_concern:"canonical producer consumption",review_perspective:"contract_and_test_oracle",coverage_obligation:"terminal evidence binding"},{review_pass_id:"pass-failure-paths",scope_item_id:"workflow-terminal-evidence",applicable_concern:"canonical producer failure handling",review_perspective:"runtime_lifecycle_and_failure_paths",coverage_obligation:"fail-closed terminal evidence"},{review_pass_id:"pass-closure",scope_item_id:"workflow-fixed-history",applicable_concern:"immutable fixed finding closure",review_perspective:"closure_verification",coverage_obligation:"verify immutable fixed closure"}]},coverage_ledger:[{batch_id:"batch-current",review_snapshot_id:"review-current",review_pass_id:"pass-consumer",perspective:"contract_and_test_oracle",coverage_obligation:"terminal evidence binding",assigned_scope:["workflow-terminal-evidence"],scope_item_id:"workflow-terminal-evidence",applicable_concern:"canonical producer consumption",terminal_state:"completed",coverage_status:"complete",coverage_disposition:"inspected_no_risk",evidence:"Focused lifecycle validation passed."},{batch_id:"batch-current",review_snapshot_id:"review-current",review_pass_id:"pass-failure-paths",perspective:"runtime_lifecycle_and_failure_paths",coverage_obligation:"fail-closed terminal evidence",assigned_scope:["workflow-terminal-evidence"],scope_item_id:"workflow-terminal-evidence",applicable_concern:"canonical producer failure handling",terminal_state:"completed",coverage_status:"complete",coverage_disposition:"inspected_no_risk",evidence:"Focused lifecycle mutation validation passed."},{batch_id:"batch-current",review_snapshot_id:"review-current",review_pass_id:"pass-closure",perspective:"closure_verification",coverage_obligation:"verify immutable fixed closure",assigned_scope:["workflow-fixed-history"],scope_item_id:"workflow-fixed-history",applicable_concern:"immutable fixed finding closure",terminal_state:"completed",coverage_status:"complete",coverage_disposition:$coverage_disposition,evidence:"Closure verification completed."} + (if $coverage_disposition == "finding" then {finding_ids:$closure_finding_ids} else {} end)],batch_summaries:[{started_batch_ordinal:1,batch_id:"batch-current",review_snapshot_id:"review-current",snapshot_identity:$identity,batch_status:"complete",expected_response_count:3,terminal_response_count:3,aggregate_rubric_recomputed:true}],aggregation_ledger:$aggregation,aggregated_findings:$findings,result:$result,fixed_items:[{aggregate_finding_id:"aggregate-fixed-workflow",severity:"must-fix",file:"skills/assistant-workflow/contracts/output.yaml",description:"Original workflow closure finding was repaired.",fixed_in_round:2}],closure_results:[{aggregate_finding_id:"aggregate-fixed-workflow",status:$closure_status,evidence:(if $closure_status == "verified_closed" then "Closure pass verified the repaired workflow finding." else "Closure pass found the original workflow finding regressed." end)}],nits:[]} + (if $result == "ISSUES_FIXED" then {evidence_bounded_claim:"No material findings within the reviewed scope and available evidence"} else {remaining_items:[{severity:"must-fix",file:"skills/assistant-workflow/contracts/output.yaml",description:"Original workflow closure finding regressed after the fix.",reason_unresolved:"Closure verification returned regressed."}]} end)),current_final_batch:{review_snapshot_id:"review-current",final_snapshot_identity:$identity},review_result:{canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",producer_schema_version:"7.2",final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,delegation_path_ref:"journal#review-delegation",delegation_contract:"assistant-review/contracts/output.yaml#review_delegation_path",validation_status:"validated"},final_handoff:({changed_behavior_and_areas:["Workflow closure consumer projection"],architecture_decisions_and_rationale:["assistant-review fixed-history contracts remain canonical"],rejected_alternatives_and_tradeoffs:["Do not rewrite immutable closure identifiers"],requirement_evidence:["Canonical closure result and aggregation ledger"],automated_verification:["Focused workflow lifecycle eval"],manual_test_scenarios:["N/A: contract-only change"],compatibility_and_regression_surfaces:["assistant-review v7.2 fixed-history output"],known_limitations_and_untested_areas:["No runtime application surface"],rollback_or_recovery:"Revert the workflow consumer projection.",review_completion:({canonical_result_ref:"journal#final-summary",canonical_contract:"assistant-review/contracts/output.yaml#final_summary",review_producer_schema_version:"7.2",result:$result,coverage_complete:true,final_review_snapshot_id:"review-current",final_snapshot_identity_ref:"journal#final-summary/final-snapshot-identity",final_snapshot_identity:$identity,completion_disposition:$disposition} + (if $result == "ISSUES_FIXED" then {evidence_bounded_claim:"No material findings within the reviewed scope and available evidence"} else {remaining_or_blocker_summary:$remaining_summary} end)),review_claim:$claim})}' >"$response_path" ;; stale-assistant-review-version-invalidates-persisted-results) jq -n --arg summary "$summary" ' - {summary:$summary,task_state_reconciliation:{classification:"stale",assistant_review_packet_compatibility:{persisted_producer_schema_versions:["7.0"],current_producer_schema_version:"7.1",status:"invalidated",invalidated_refs:["review_result.canonical_result_ref","review_result.delegation_path_ref","review_result.final_snapshot_identity_ref","review_result.architecture_decision_pack_review_ref","qa_evaluation_result.canonical_result_ref","qa_evaluation_result.delegation_path_ref","qa_evaluation_result.approved_feature_preparation_qa_acceptance_obligation_result_ref"],rebuild_route:"rerun_review_and_qa"},exact_next_action:"Rerun the current assistant-review review and QA lanes, then rebuild workflow wrappers."}}' >"$response_path" + {summary:$summary,task_state_reconciliation:{classification:"stale",assistant_review_packet_compatibility:{persisted_producer_schema_versions:["7.1"],current_producer_schema_version:"7.2",status:"invalidated",invalidated_refs:["review_result.canonical_result_ref","review_result.delegation_path_ref","review_result.final_snapshot_identity_ref","review_result.architecture_decision_pack_review_ref","qa_evaluation_result.canonical_result_ref","qa_evaluation_result.delegation_path_ref","qa_evaluation_result.approved_feature_preparation_qa_acceptance_obligation_result_ref"],rebuild_route:"rerun_review_and_qa"},exact_next_action:"Rerun the current assistant-review review and QA lanes, then rebuild workflow wrappers."}}' >"$response_path" ;; *) return 1 diff --git a/tests/p0-p4/progressive-discovery-contracts.sh b/tests/p0-p4/progressive-discovery-contracts.sh index 6b26a8ba..5c7a2ce8 100644 --- a/tests/p0-p4/progressive-discovery-contracts.sh +++ b/tests/p0-p4/progressive-discovery-contracts.sh @@ -376,6 +376,9 @@ write_workflow_eval_responses() { medium-prepare-only-terminal-route) build_medium_prepare_only_terminal_response "$response_path" "$required_summary" ;; + prepare-only-shared-impact-retains-discovery-evidence) + build_medium_prepare_only_shared_impact_response "$response_path" "$required_summary" + ;; medium-prepare-only-readiness-plan) build_medium_prepare_only_readiness_plan_response "$response_path" "$required_summary" ;; @@ -3555,15 +3558,15 @@ fi test_start "workflow keeps full-corpus baselines and case-targeted mutation grading proportional" full_corpus_eval_call_sites="$(awk 'index($0, "--responses") && !/full_corpus_eval_call_sites=/ { count++ } END { print count + 0 }' "${BASH_SOURCE[0]}")" if [[ "$workflow_eval_invocation_count" -eq 26 \ - && "$prepare_only_direct_structured_probe_count" -eq 375 \ - && "$prepare_only_plan_mode_mutation_count" -eq 12 \ + && "$prepare_only_direct_structured_probe_count" -eq 409 \ + && "$prepare_only_plan_mode_mutation_count" -eq 14 \ && "$prepare_only_representative_cli_probe_count" -eq 3 \ - && "$prepare_only_mutation_invocation_count" -eq 15 \ + && "$prepare_only_mutation_invocation_count" -eq 17 \ && "$actual_grader_invocation_count" -eq $((workflow_eval_invocation_count + prepare_only_mutation_invocation_count + non_workflow_skill_eval_count)) \ && "$full_corpus_eval_call_sites" -eq 2 ]]; then pass else - fail "expected 26 bounded workflow evals, 375 independent direct production structured-grader probes, 12 plan-mode case CLI mutations, 3 representative branch CLI probes, and one full-corpus plus one case-targeted runner call site; found $workflow_eval_invocation_count workflow evals, $prepare_only_direct_structured_probe_count direct probes, $prepare_only_plan_mode_mutation_count plan-mode, $prepare_only_representative_cli_probe_count representative, $prepare_only_mutation_invocation_count mutation CLI calls, $non_workflow_skill_eval_count non-workflow, and $actual_grader_invocation_count total CLI invocations across $full_corpus_eval_call_sites call sites" + fail "expected 26 bounded workflow evals, 409 independent direct production structured-grader probes, 14 plan-mode case CLI mutations, 3 representative branch CLI probes, and one full-corpus plus one case-targeted runner call site; found $workflow_eval_invocation_count workflow evals, $prepare_only_direct_structured_probe_count direct probes, $prepare_only_plan_mode_mutation_count plan-mode, $prepare_only_representative_cli_probe_count representative, $prepare_only_mutation_invocation_count mutation CLI calls, $non_workflow_skill_eval_count non-workflow, and $actual_grader_invocation_count total CLI invocations across $full_corpus_eval_call_sites call sites" fi p0p4_finish_suite "${BASH_SOURCE[0]}" diff --git a/tests/p0-p4/skill-eval-contracts.sh b/tests/p0-p4/skill-eval-contracts.sh index 63ee7b41..3665fe8b 100644 --- a/tests/p0-p4/skill-eval-contracts.sh +++ b/tests/p0-p4/skill-eval-contracts.sh @@ -420,6 +420,9 @@ p0p4_write_skill_eval_responses() { medium-prepare-only-terminal-route) build_medium_prepare_only_terminal_response "$response_path" "$required_summary" ;; + prepare-only-shared-impact-retains-discovery-evidence) + build_medium_prepare_only_shared_impact_response "$response_path" "$required_summary" + ;; medium-prepare-only-readiness-plan) build_medium_prepare_only_readiness_plan_response "$response_path" "$required_summary" ;; @@ -498,6 +501,18 @@ p0p4_write_skill_eval_responses() { esac continue fi + if [[ "$skill_name" == "assistant-debugging" ]] \ + && jq -e --arg id "$id" '.cases[] | select(.id == $id) | (.machine_expectations.structured_json_assertions? // []) | length > 0' "$fixture_file" >/dev/null; then + case "$id" in + local-debugging-retains-compact-causal-impact) + build_local_debugging_compact_causal_response "$response_path" "$required_summary" + ;; + *) + fail "unhandled structured assistant-debugging eval case: $id" + ;; + esac + continue + fi if [[ "$skill_name" == "assistant-review" ]] \ && jq -e --arg id "$id" '.cases[] | select(.id == $id) | (.machine_expectations.structured_json_assertions? // []) | length > 0' "$fixture_file" >/dev/null; then case "$id" in @@ -641,6 +656,25 @@ p0p4_write_skill_eval_responses() { mv "${response_path}.trivial" "$response_path" p0p4_add_assistant_review_audit_report "$response_path" "${response_path}.audit" ;; + compact-local-review-retains-causal-applicability) + p0p4_write_assistant_review_batch_response "$response_path" "$required_summary" "CLEAN" true complete false + jq '.change_impact_applicability = { + impact_scope: "local", + applicability_reason: "The supplied route trace confines the change to this navigation guard.", + causal_evidence_ref: "tests/local-navigation-guard", + expanded_artifact_carried: false + }' "$response_path" >"${response_path}.compact-local" + mv "${response_path}.compact-local" "$response_path" + ;; + compact-cosmetic-review-retains-applicability) + p0p4_write_assistant_review_batch_response "$response_path" "$required_summary" "CLEAN" true complete false + jq '.change_impact_applicability = { + impact_scope: "not_applicable", + applicability_reason: "The navigation label correction is cosmetic.", + expanded_artifact_carried: false + }' "$response_path" >"${response_path}.compact-cosmetic" + mv "${response_path}.compact-cosmetic" "$response_path" + ;; qa-obligation-echo-fulfills-exact-binding) p0p4_write_assistant_review_qa_response "$response_path" "$required_summary" accepted CLEAN fulfilled met false ;; @@ -1605,6 +1639,32 @@ else fail "skill eval runner --responses did not pass generated all-required response set: $(grep -E '^(FAIL|Summary:)' "$passing_response_output" | paste -sd ' | ' -)" fi +test_start "assistant-review compact applicability keeps canonical envelopes and rejects omissions" +compact_review_output="$(mktemp "${TMPDIR:-/tmp}/skill-eval-compact-review.XXXXXX")" +p0p4_register_cleanup "$compact_review_output" +compact_review_failures=() +while IFS='|' read -r compact_review_case compact_review_mutation; do + compact_review_response="$passing_response_dir/assistant-review/$compact_review_case.txt" + cp "$compact_review_response" "${compact_review_response}.original" + jq "$compact_review_mutation" "$compact_review_response" >"$compact_review_output" + mv "$compact_review_output" "$compact_review_response" + if "$skill_eval_runner" --responses "$passing_response_dir" --skill assistant-review --case "$compact_review_case" >"$passing_response_output" 2>&1; then + compact_review_failures+=("$compact_review_case:$compact_review_mutation") + fi + mv "${compact_review_response}.original" "$compact_review_response" +done <<'EOF_COMPACT_REVIEW_MUTATIONS' +compact-local-review-retains-causal-applicability|del(.change_impact_applicability) +compact-local-review-retains-causal-applicability|del(.change_impact_applicability.causal_evidence_ref) +compact-local-review-retains-causal-applicability|del(.final_summary) +compact-cosmetic-review-retains-applicability|del(.change_impact_applicability) +compact-cosmetic-review-retains-applicability|del(.change_impact_applicability.applicability_reason) +EOF_COMPACT_REVIEW_MUTATIONS +if [[ "${#compact_review_failures[@]}" -eq 0 ]]; then + pass +else + fail "compact review applicability mutations unexpectedly passed: ${compact_review_failures[*]}" +fi + test_start "assistant-review runtime grader rejects malformed canonical envelopes and claims" assistant_review_runtime_mutation_output="$(mktemp "${TMPDIR:-/tmp}/skill-eval-review-runtime.XXXXXX")" p0p4_register_cleanup "$assistant_review_runtime_mutation_output" @@ -2584,7 +2644,7 @@ run_prepare_only_representative_path_probe \ run_prepare_only_representative_path_probe \ medium-prepare-only-not-applicable-readiness-plan '["feature_preparation_result", "readiness_plan", "evidence_ref"]' if [[ ${#workflow_core_failures[@]} -eq 0 \ - && "$prepare_only_direct_structured_probe_count" -eq 375 \ + && "$prepare_only_direct_structured_probe_count" -eq 409 \ && "$prepare_only_representative_cli_probe_count" -eq 3 ]]; then pass else diff --git a/tests/p0-p4/skill-instruction-quality-contracts.sh b/tests/p0-p4/skill-instruction-quality-contracts.sh index e8535a8c..cf55d5fb 100755 --- a/tests/p0-p4/skill-instruction-quality-contracts.sh +++ b/tests/p0-p4/skill-instruction-quality-contracts.sh @@ -609,8 +609,8 @@ for reviewer_agent in \ done done for review_term in \ - "assistant-review contracts are v7" \ - "Persisted v6 batch packets are invalidated and rebuilt" \ + "assistant-review contracts are v7.2" \ + "Persisted 6.0/7.0/7.1 batch packets are invalidated and rebuilt" \ "risk_selected_specialist" \ "assistant-security checklist/perspective"; do if ! grep -Fq "$review_term" "$review_skill"; then diff --git a/tests/p0-p4/worker-status-contracts.sh b/tests/p0-p4/worker-status-contracts.sh index 1724e522..cacdf4e8 100644 --- a/tests/p0-p4/worker-status-contracts.sh +++ b/tests/p0-p4/worker-status-contracts.sh @@ -90,7 +90,7 @@ missing_typed_artifact_terms=() for term in \ "artifact_reference_protocol:" \ "required_fields: [artifact_id, artifact_type, producer, consumer, location_ref, schema_or_contract, validation_status, summary]" \ - "artifact_types: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, test_result, review_result, qa_evaluation_result]" \ + "artifact_types: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, change_impact_evidence, test_result, review_result, qa_evaluation_result]" \ "location_ref is the typed location/ref pointer" \ "Producer responsibility: create or update the artifact" \ "Consumer responsibility: validate schema_or_contract and validation_status before relying on location_ref" \ @@ -114,7 +114,7 @@ for term in \ "schema_or_contract" \ "validation_status" \ "ledger covers Done Contract, Harness Recipe, Harness Run State, Trace Ledger, Replay Packet, Pivot/Restart Decision, changed files, verification evidence, and plan deviation refs when applicable." \ - "enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, test_result, review_result, qa_evaluation_result]" \ + "enum_values: [done_contract, harness_recipe, harness_run_state, trace_ledger, replay_packet, pivot_restart_decision, changed_files, verification_evidence, plan_deviation, task_packet, context_map, architecture_decision_pack, change_impact_evidence, test_result, review_result, qa_evaluation_result]" \ "- name: pivot_restart_decision"; do if ! grep -Fq -- "$term" "$output_contract"; then missing_typed_artifact_terms+=("output.yaml: $term") diff --git a/tests/p0-p4/workflow-architecture-completion-contracts.sh b/tests/p0-p4/workflow-architecture-completion-contracts.sh index fe63fee6..afad2558 100644 --- a/tests/p0-p4/workflow-architecture-completion-contracts.sh +++ b/tests/p0-p4/workflow-architecture-completion-contracts.sh @@ -58,19 +58,28 @@ fresh_review_field_has_property() { ' "$file" } -fresh_review_pack_refs_are_declared() { +fresh_review_common_refs_are_declared() { local file="$1" local field - for field in canonical_result_ref final_snapshot_identity_ref delegation_path_ref architecture_decision_pack_review_ref; do + local common_condition='architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried' + for field in canonical_result_ref canonical_contract producer_schema_version final_review_snapshot_id final_snapshot_identity_ref delegation_path_ref delegation_contract; do fresh_review_field_has_property "$file" "$field" 'type: string' \ - && fresh_review_field_has_property "$file" "$field" 'required: conditional' \ + || return 1 + done + fresh_review_field_has_property "$file" final_snapshot_identity 'type: object' \ + && fresh_review_field_has_property "$file" validation_status 'type: enum' \ + || return 1 + for field in canonical_result_ref canonical_contract producer_schema_version final_review_snapshot_id final_snapshot_identity_ref final_snapshot_identity delegation_path_ref delegation_contract validation_status; do + fresh_review_field_has_property "$file" "$field" 'required: conditional' \ + && fresh_review_field_has_property "$file" "$field" "condition: \"$common_condition\"" \ + || return 1 + done + for field in architecture_decision_pack_review_ref architecture_decision_pack_review_contract; do + fresh_review_field_has_property "$file" "$field" 'required: conditional' \ && fresh_review_field_has_property "$file" "$field" 'condition: "architecture_design_mode in [lightweight, required, review_intensive]"' \ || return 1 done - fresh_review_field_has_property "$file" delegation_contract 'type: string' \ - && fresh_review_field_has_property "$file" delegation_contract 'required: conditional' \ - && fresh_review_field_has_property "$file" delegation_contract 'condition: "architecture_design_mode in [lightweight, required, review_intensive]"' \ - && fresh_review_field_has_property "$file" delegation_contract 'validation: "Must equal assistant-review/contracts/output.yaml#review_delegation_path"' + fresh_review_field_has_property "$file" delegation_contract 'validation: "Must equal assistant-review/contracts/output.yaml#review_delegation_path"' } without_fresh_review_pack_refs() { @@ -309,7 +318,7 @@ if ! ruby -ryaml -e ' verification = pack_fields.fetch("verification") verification_fields = verification.fetch("object_fields").to_h { |field| [field["name"], field] } maps = contracts.map { |contract| (contract["fields"] || []).find { |field| field["name"] == "requirement_acceptance_map" } || (contract["artifacts"] || []).find { |artifact| artifact["name"] == "requirement_acceptance_map" } }.compact - valid = contracts.all? { |contract| contract.fetch("schema_version") == "11.0" } && + valid = contracts.all? { |contract| contract.fetch("schema_version") == output.fetch("schema_version") } && pack_fields.fetch("selected_alternative_id")["required"] == "conditional" && pack_fields.fetch("selected_alternative_id")["condition"].include?("alternatives") && alternative_fields.fetch("alternative_id")["required"] == true && @@ -672,7 +681,8 @@ elif ! grep -Fq 'semantic_type_inspection' <<<"$migration_note" \ || ! grep -Fq 'contributor_evidence' <<<"$migration_note"; then fail "v9 migration note does not preserve CodeMapper semantic inspection and collaborative contributor evidence migrations" elif ! ruby -ryaml -e ' - ARGV.each { |path| exit 1 unless YAML.load_file(path).fetch("schema_version") == "11.0" } + expected = YAML.load_file(ARGV.first).fetch("schema_version") + ARGV.each { |path| exit 1 unless YAML.load_file(path).fetch("schema_version") == expected } ' "$workflow_dir/contracts/input.yaml" "$workflow_dir/contracts/output.yaml" "$workflow_dir/contracts/phase-gates.yaml" "$workflow_dir/contracts/handoffs.yaml" "$workflow_dir/contracts/index.yaml"; then fail "v11 migration does not bump every assistant-workflow canonical contract header" elif ! grep -Fq 'verification_command' <<<"$migration_note"; then @@ -752,15 +762,16 @@ if ruby -ryaml -e ' end producer_basis = final_identity.fetch("object_fields").find { |field| field["name"] == "basis" } + common_condition = "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" valid = final_identity["required"] == true && producer_shape == expected_shape && producer_basis.fetch("enum_values") == %w[git_revision diff_digest content_digest task_or_pr_revision] && review_ref["required"] == true && exact_binding.call(review_ref) && review_identity["required"] == true && exact_identity.call(review_identity) && fresh_ref["required"] == "conditional" && - fresh_ref["condition"] == "architecture_design_mode in [lightweight, required, review_intensive]" && + fresh_ref["condition"] == common_condition && exact_binding.call(fresh_ref) && fresh_identity["required"] == "conditional" && - fresh_identity["condition"] == "architecture_design_mode in [lightweight, required, review_intensive]" && + fresh_identity["condition"] == common_condition && exact_identity.call(fresh_identity) exit valid ? 0 : 1 ' "$assistant_review_output" "$output_contract" \ @@ -834,54 +845,57 @@ elif ! grep -Fq 'assistant-review/contracts/output.yaml#final_summary' <<<"$fres fail "light Pack fresh_review_result does not retain validated canonical assistant-review output refs" elif ! grep -Fq 'architecture_decision_pack_review' <<<"$(phase_block REVIEW)" \ || ! grep -Fq 'assistant-review/contracts/output.yaml#final_summary' "$review_router" \ - || ! p0p4_contains_text "$review_router" 'review_delegation_path'; then + || ! p0p4_contains_text "$review_router" 'review_delegation_path' \ + || ! p0p4_contains_text "$review_router" 'expanded impact' \ + || ! p0p4_contains_text "$review_router" 'resolve its own policy' \ + || ! p0p4_contains_text "$review_router" 'do not copy light Build policy'; then fail "light Pack review routing does not preserve the canonical delegation-path requirement" else pass fi -test_start "light Pack fresh_review_result declares every conditional canonical reference" +test_start "light Pack and expanded-impact fresh_review_result declarations keep common and Pack-only refs distinct" fresh_review_ref_missing=() -for field in canonical_result_ref final_snapshot_identity_ref delegation_path_ref architecture_decision_pack_review_ref; do +common_condition='condition: "architecture_design_mode in [lightweight, required, review_intensive] or impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried"' +for field in canonical_result_ref canonical_contract producer_schema_version final_review_snapshot_id final_snapshot_identity_ref final_snapshot_identity delegation_path_ref delegation_contract validation_status; do for property in \ - 'type: string' \ 'required: conditional' \ - 'condition: "architecture_design_mode in [lightweight, required, review_intensive]"'; do + "$common_condition"; do if ! fresh_review_field_has_property "$output_contract" "$field" "$property"; then fresh_review_ref_missing+=("$field $property") fi done done -for property in \ - 'type: string' \ - 'required: conditional' \ - 'condition: "architecture_design_mode in [lightweight, required, review_intensive]"' \ - 'validation: "Must equal assistant-review/contracts/output.yaml#review_delegation_path"'; do - if ! fresh_review_field_has_property "$output_contract" delegation_contract "$property"; then - fresh_review_ref_missing+=("delegation_contract $property") - fi +for field in architecture_decision_pack_review_ref architecture_decision_pack_review_contract; do + for property in \ + 'required: conditional' \ + 'condition: "architecture_design_mode in [lightweight, required, review_intensive]"'; do + if ! fresh_review_field_has_property "$output_contract" "$field" "$property"; then + fresh_review_ref_missing+=("$field $property") + fi + done done if [[ ${#fresh_review_ref_missing[@]} -eq 0 ]]; then pass else - fail "light Pack fresh_review_result reference declarations are incomplete: ${fresh_review_ref_missing[*]}" + fail "light Pack or expanded-impact fresh_review_result declarations are incomplete: ${fresh_review_ref_missing[*]}" fi -test_start "light Pack fresh_review_result rejects independent canonical-reference omissions" +test_start "light Pack and expanded-impact fresh_review_result rejects common and Pack-only reference omissions" fresh_review_mutation_dir="$(mktemp -d "${TMPDIR:-/tmp}/workflow-light-pack-ref.XXXXXX")" p0p4_register_cleanup "$fresh_review_mutation_dir" fresh_review_mutation_failures=() -for omitted in canonical_result_ref final_snapshot_identity_ref delegation_path_ref delegation_contract architecture_decision_pack_review_ref canonical_result_ref,final_snapshot_identity_ref,delegation_path_ref,delegation_contract,architecture_decision_pack_review_ref; do +for omitted in canonical_result_ref canonical_contract producer_schema_version final_review_snapshot_id final_snapshot_identity_ref final_snapshot_identity delegation_path_ref delegation_contract validation_status architecture_decision_pack_review_ref architecture_decision_pack_review_contract canonical_result_ref,final_snapshot_identity_ref,delegation_path_ref,delegation_contract,architecture_decision_pack_review_ref; do mutated_output="$fresh_review_mutation_dir/${omitted//,/-}.yaml" without_fresh_review_pack_refs "$output_contract" "$mutated_output" "$omitted" - if fresh_review_pack_refs_are_declared "$mutated_output"; then + if fresh_review_common_refs_are_declared "$mutated_output"; then fresh_review_mutation_failures+=("$omitted omission accepted") fi done if [[ ${#fresh_review_mutation_failures[@]} -eq 0 ]]; then pass else - fail "light Pack fresh_review_result reference declaration guard false-passes: ${fresh_review_mutation_failures[*]}" + fail "light Pack or expanded-impact fresh_review_result reference declaration guard false-passes: ${fresh_review_mutation_failures[*]}" fi test_start "assistant-review and every Reviewer prompt produce workflow-consumable reviewed_scope" @@ -1597,7 +1611,8 @@ if ! jq -e ' standard_pack_review_failures+=("standard Pack review retention eval lacks structured canonical reference assertions") fi if ! ruby -ryaml -e ' - ARGV.each { |path| exit 1 unless YAML.load_file(path).fetch("schema_version") == "11.0" } + expected = YAML.load_file(ARGV.first).fetch("schema_version") + ARGV.each { |path| exit 1 unless YAML.load_file(path).fetch("schema_version") == expected } ' "$workflow_dir/contracts/input.yaml" "$workflow_dir/contracts/output.yaml" "$workflow_dir/contracts/phase-gates.yaml" "$workflow_dir/contracts/handoffs.yaml" "$workflow_dir/contracts/index.yaml"; then standard_pack_review_failures+=("workflow v11 does not cover every canonical contract header") fi @@ -2014,7 +2029,7 @@ for case_id in \ fi done done -if [[ "$canonical_review_schema_version" == "7.1" && ${#terminal_shape_failures[@]} -eq 0 ]]; then +if [[ "$canonical_review_schema_version" == "$(ruby -ryaml -e 'puts YAML.load_file(ARGV.fetch(0)).fetch("schema_version")' "$assistant_review_output")" && ${#terminal_shape_failures[@]} -eq 0 ]]; then pass else fail "terminal review consumer endpoint gaps: canonical=$canonical_review_schema_version ${terminal_shape_failures[*]}" diff --git a/tests/p0-p4/workflow-basics-contracts.sh b/tests/p0-p4/workflow-basics-contracts.sh index 961fb448..c62bf687 100644 --- a/tests/p0-p4/workflow-basics-contracts.sh +++ b/tests/p0-p4/workflow-basics-contracts.sh @@ -49,7 +49,7 @@ fi test_start "review delegation dispatches from applicable instruction triggers" review_trigger_failures=() for file_and_term in \ - "$FRAMEWORK_DIR/skills/assistant-review/SKILL.md::assistant-review contracts are v7" \ + "$FRAMEWORK_DIR/skills/assistant-review/SKILL.md::assistant-review contracts are v7.2" \ "$FRAMEWORK_DIR/skills/assistant-workflow/SKILL.md::assistant-review owns Reviewer/QAEvaluator handoffs" \ "$FRAMEWORK_DIR/skills/assistant-review/SKILL.md::load \`contracts/input.yaml\` review-entry fields selected by \`review-entry-fields\` in \`contracts/index.yaml\`" \ "$FRAMEWORK_DIR/skills/assistant-review/SKILL.md::consumers can use the producer packet" \ diff --git a/tests/test-p0-p4-contracts.sh b/tests/test-p0-p4-contracts.sh index 3e3f2d72..55b3270d 100755 --- a/tests/test-p0-p4-contracts.sh +++ b/tests/test-p0-p4-contracts.sh @@ -19,6 +19,7 @@ p0p4_suite_is_excluded() { source "$P0P4_SUITE_DIR/repo-guard-contracts.sh" source "$P0P4_SUITE_DIR/harness-controller-contracts.sh" source "$P0P4_SUITE_DIR/installer-contracts.sh" +source "$P0P4_SUITE_DIR/change-impact-contracts.sh" source "$P0P4_SUITE_DIR/windows-installer-contracts.sh" source "$P0P4_SUITE_DIR/hook-retirement-contracts.sh" source "$P0P4_SUITE_DIR/sol-native-overhead-contracts.sh" diff --git a/tests/windows/installer-contracts.ps1 b/tests/windows/installer-contracts.ps1 index 16d84b7b..87a77eef 100644 --- a/tests/windows/installer-contracts.ps1 +++ b/tests/windows/installer-contracts.ps1 @@ -1353,6 +1353,52 @@ requires: } } + Invoke-Contract 'selected bundled requirements close transitively without duplicate cycle copies' { + $fixtureFrameworkRoot = Join-Path $script:SuiteRoot ('closure-framework-' + [Guid]::NewGuid().ToString('N')) + $fixtureInstaller = Join-Path $fixtureFrameworkRoot 'install.ps1' + try { + [System.IO.Directory]::CreateDirectory((Join-Path $fixtureFrameworkRoot 'skills')) | Out-Null + [System.IO.File]::Copy($script:InstallerPath, $fixtureInstaller) + $fixtureSkills = @{ + 'assistant-closure-a' = @('Closure root fixture.', @('assistant-closure-b', 'assistant-closure-c')) + 'assistant-closure-b' = @('Closure transitive fixture.', @('assistant-closure-c')) + 'assistant-closure-c' = @('Closure cycle fixture.', @('assistant-closure-a')) + } + foreach ($entry in $fixtureSkills.GetEnumerator()) { + $fixtureSkillRoot = Join-Path (Join-Path $fixtureFrameworkRoot 'skills') $entry.Key + [System.IO.Directory]::CreateDirectory($fixtureSkillRoot) | Out-Null + $requires = ($entry.Value[1] | ForEach-Object { ' - ' + $_ }) -join [Environment]::NewLine + [System.IO.File]::WriteAllText((Join-Path $fixtureSkillRoot 'SKILL.md'), "---`nname: $($entry.Key)`ndescription: $($entry.Value[0])`nrequires:`n$requires`n---`n", (New-Object System.Text.UTF8Encoding($false))) + } + Use-IsolatedEnvironment 'selected bundled closure' { + param($root, $isolatedUserProfile) + $warmup = Invoke-PowerShellFile -LiteralPath $fixtureInstaller -Arguments @('-Help') + Assert-Equal 0 $warmup.ExitCode 'Bundled closure PowerShell warm-up failed' + $beforeDryRun = Get-TreeFingerprint -LiteralPath $root + $dryRun = Invoke-PowerShellFile -LiteralPath $fixtureInstaller -Arguments @('-Agent', 'codex', '-Skill', 'ASSISTANT-CLOSURE-A', '-DryRun') + Assert-Equal 0 $dryRun.ExitCode "Bundled closure dry-run failed: $($dryRun.Output)" + Assert-Contains $dryRun.Output (Join-Path $isolatedUserProfile '.agents\skills\assistant-closure-a') 'Bundled closure dry-run omitted the canonical-cased root skill' + Assert-Contains $dryRun.Output 'assistant-closure-b' 'Bundled closure dry-run omitted transitive dependency' + Assert-Contains $dryRun.Output 'assistant-closure-c' 'Bundled closure dry-run omitted cycle dependency' + Assert-Equal $beforeDryRun (Get-TreeFingerprint -LiteralPath $root) 'Bundled closure dry-run mutated the isolated filesystem' + Assert-Equal 0 @(Get-InstalledSkillNames -SkillsRoot (Join-Path $isolatedUserProfile '.agents\skills')).Count 'Bundled closure dry-run wrote managed skills' + + $result = Invoke-PowerShellFile -LiteralPath $fixtureInstaller -Arguments @('-Agent', 'codex', '-Skill', 'ASSISTANT-CLOSURE-A') + Assert-Equal 0 $result.ExitCode "Bundled closure install failed: $($result.Output)" + Assert-Equal @('assistant-closure-a', 'assistant-closure-b', 'assistant-closure-c') (Get-InstalledSkillNames -SkillsRoot (Join-Path $isolatedUserProfile '.agents\skills')) 'Bundled closure did not install each cycle member exactly once' + foreach ($skill in @('assistant-closure-a', 'assistant-closure-b', 'assistant-closure-c')) { + $copy = $skill + ' -> ' + (Join-Path $isolatedUserProfile ('.agents\skills\' + $skill)) + Assert-Equal 1 (Get-LiteralCount -Text $result.Output -Needle $copy) "Bundled closure copied $skill more than once" + } + } + } + finally { + if (Test-Path -LiteralPath $fixtureFrameworkRoot) { + Remove-Item -LiteralPath $fixtureFrameworkRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } + } + Invoke-Contract 'Codex installation preserves existing legacy Memory Graph state without a CLI' { Use-IsolatedEnvironment 'codex legacy state preservation' { param($root, $isolatedUserProfile) @@ -1378,7 +1424,7 @@ requires: } } - Invoke-Contract 'full inventory and single-skill selection remain bounded' { + Invoke-Contract 'full inventory and selected bundled requirements remain bounded' { Use-IsolatedEnvironment 'inventory selection' { param($root, $isolatedUserProfile) $full = Invoke-Installer -Arguments @('-Agent', 'gemini') @@ -1399,7 +1445,7 @@ requires: [Environment]::SetEnvironmentVariable('HOME', $singleHome, 'Process') $single = Invoke-Installer -Arguments @('-Agent', 'claude', '-Skill', 'assistant-workflow') Assert-Equal 0 $single.ExitCode "Single-skill install failed: $($single.Output)" - Assert-Equal @('assistant-workflow') (Get-InstalledSkillNames -SkillsRoot (Join-Path $singleHome '.claude\skills')) 'Single-skill install copied additional skills' + Assert-Equal @('assistant-review', 'assistant-workflow') (Get-InstalledSkillNames -SkillsRoot (Join-Path $singleHome '.claude\skills')) 'Selected workflow install omitted or copied outside its canonical review producer' } } @@ -2135,6 +2181,66 @@ requires: } } + Invoke-Contract 'selective debugging and review installs ship an executable common change-impact checker' { + Use-IsolatedEnvironment 'selective change impact checker' { + param($root, $isolatedUserProfile) + $node = Get-Command node -ErrorAction SilentlyContinue + Assert-True ($null -ne $node) 'Node runtime is unavailable: installed change-impact validation remains an explicit blocker' + $fixtureRoot = Join-Path $root 'Change Impact Fixture [isolated]' + foreach ($skill in @('assistant-debugging', 'assistant-review')) { + $skillProfile = Join-Path $root ('Change Impact Profile ' + $skill) + $codexHome = Join-Path $root ('Codex Change Impact Home ' + $skill) + [void][System.IO.Directory]::CreateDirectory($skillProfile) + [Environment]::SetEnvironmentVariable('USERPROFILE', $skillProfile, 'Process') + [Environment]::SetEnvironmentVariable('HOME', $skillProfile, 'Process') + [Environment]::SetEnvironmentVariable('CODEX_HOME', $codexHome, 'Process') + $result = Invoke-Installer -Arguments @('-Agent', 'codex', '-Skill', $skill) + Assert-Equal 0 $result.ExitCode "Selective $skill installation failed: $($result.Output)" + $reference = Join-Path $skillProfile (".agents\skills\$skill\references\change-impact.md") + $installedSkill = Join-Path $skillProfile (".agents\skills\$skill\SKILL.md") + $tool = [System.IO.Path]::GetFullPath((Join-Path (Split-Path -Parent $installedSkill) '..\..\tools\change-impact\validate-change-impact.cjs')) + $protocol = [System.IO.Path]::GetFullPath((Join-Path (Split-Path -Parent $installedSkill) '..\..\tools\change-impact\protocol.v1.json')) + $example = [System.IO.Path]::GetFullPath((Join-Path (Split-Path -Parent $installedSkill) '..\..\tools\change-impact\example.completion.v1.json')) + Assert-True ([System.IO.File]::Exists($installedSkill)) "$skill selective installation omitted its installed SKILL.md" + if ($skill -eq 'assistant-debugging') { + Assert-True ([System.IO.File]::Exists((Join-Path $skillProfile '.agents\skills\assistant-review\SKILL.md'))) 'assistant-debugging selective installation omitted its canonical assistant-review producer' + } + else { + Assert-False ([System.IO.Directory]::Exists((Join-Path $skillProfile '.agents\skills\assistant-debugging'))) 'assistant-review selective installation copied an unrelated consumer' + } + Assert-True ([System.IO.File]::Exists($tool)) "$skill selective installation omitted the common checker at the path resolved from its loaded SKILL.md" + Assert-True ([System.IO.File]::Exists($protocol) -and [System.IO.File]::Exists($example)) "$skill selective installation omitted common protocol resources" + Assert-False ([System.IO.Directory]::Exists((Join-Path $skillProfile '.agents\skills\assistant-workflow'))) "$skill selective installation unexpectedly installed workflow" + Assert-True ([System.IO.File]::Exists($reference)) "$skill selective installation omitted its installed change-impact reference" + Assert-Contains ([System.IO.File]::ReadAllText($reference)) '../../tools/change-impact/validate-change-impact.cjs' "$skill reference does not resolve the installed common checker" + + $caseRoot = Join-Path $fixtureRoot $skill + [void][System.IO.Directory]::CreateDirectory($caseRoot) + $documents = Read-JsonFile -LiteralPath $example + foreach ($property in $documents.PSObject.Properties) { Write-JsonFile -LiteralPath (Join-Path $caseRoot ($property.Name + '.json')) -Value $property.Value } + $validOutput = @(& $node.Source $tool '--phase' 'completion' '--capture' (Join-Path $caseRoot 'capture.json') '--expected' (Join-Path $caseRoot 'expected.json') '--assessment' (Join-Path $caseRoot 'assessment.json') '--review' (Join-Path $caseRoot 'review.json') 2>&1) -join [Environment]::NewLine + Assert-Equal 0 $LASTEXITCODE "$skill installed checker rejected valid protocol documents: $validOutput" + Assert-Contains $validOutput '"complete":true' "$skill installed checker did not report complete valid output" + + $capture = Read-JsonFile -LiteralPath (Join-Path $caseRoot 'capture.json') + $omittedEdge = @($capture.edges)[-1] + $capture.edges = @($capture.edges | Where-Object { $_.id -ne $omittedEdge.id }) + $capture.requirements = @($capture.requirements | Where-Object { $_.edge_id -ne $omittedEdge.id }) + Write-JsonFile -LiteralPath (Join-Path $caseRoot 'capture.json') -Value $capture + $consumerOmissionOutput = @(& $node.Source $tool '--phase' 'completion' '--capture' (Join-Path $caseRoot 'capture.json') '--expected' (Join-Path $caseRoot 'expected.json') '--assessment' (Join-Path $caseRoot 'assessment.json') '--review' (Join-Path $caseRoot 'review.json') 2>&1) -join [Environment]::NewLine + Assert-True ($LASTEXITCODE -ne 0) "$skill installed checker accepted a capture missing a known consumer" + Assert-Contains $consumerOmissionOutput 'CAPTURE_EXPECTED_EDGES_MISMATCH' "$skill consumer omission did not fail against independent expected truth" + + $capture = (Read-JsonFile -LiteralPath $example).capture + $capture.roots = @() + Write-JsonFile -LiteralPath (Join-Path $caseRoot 'capture.json') -Value $capture + $omissionOutput = @(& $node.Source $tool '--phase' 'completion' '--capture' (Join-Path $caseRoot 'capture.json') '--expected' (Join-Path $caseRoot 'expected.json') '--assessment' (Join-Path $caseRoot 'assessment.json') '--review' (Join-Path $caseRoot 'review.json') 2>&1) -join [Environment]::NewLine + Assert-True ($LASTEXITCODE -ne 0) "$skill installed checker accepted an empty shared discovery-root capture" + Assert-Contains $omissionOutput 'CAPTURE_DISCOVERY_ROOTS_MISSING' "$skill omission did not report the missing discovery-root reason" + } + } + } + } finally { if (Test-Path -LiteralPath $script:SuiteRoot) { diff --git a/tools/change-impact/README.md b/tools/change-impact/README.md new file mode 100644 index 00000000..b291a4e9 --- /dev/null +++ b/tools/change-impact/README.md @@ -0,0 +1,116 @@ +# Change-impact validator + +`validate-change-impact.cjs` is a Node 22-or-newer standard-library-only CLI. It checks +that an assessment closes against a separately supplied capture authority. It +does not execute commands in its JSON files, access the network, mutate the +repository, or claim to discover every runtime dependency. + +Use one explicit file for each input. The expected-context file is the current +independent authority: its `capture_id`, snapshot IDs, and every required edge +must exactly match the capture. A response cannot make itself current merely by +choosing matching strings in its assessment. The caller must obtain this +authority independently; the tool validates consistency, not that discovery was +complete. + +When recapturing, calculate `universe_id` from the current bounded discovery +universe (roots plus relevant source, registration, configuration, event and +state files), including files newly added since the prior capture. Give the +fresh inventory and resulting ID to the independent expected-context authority +before completion. A reused old universe ID is stale once that authority has a +new ID; the checker then rejects the capture and assessment. + +```sh +node tools/change-impact/validate-change-impact.cjs --phase discovery \ + --capture capture.json --expected expected-context.json +node tools/change-impact/validate-change-impact.cjs --phase pre_build \ + --capture capture.json --expected expected-context.json --assessment assessment.json +node tools/change-impact/validate-change-impact.cjs --phase completion \ + --capture capture.json --expected expected-context.json --assessment assessment.json \ + --review review.json +node tools/change-impact/validate-change-impact.cjs --phase pre_build \ + --capture capture.json --expected expected-context.json --assessment assessment.json \ + --receipt prior-result.json +``` + +The command writes one JSON result with stable reason codes and exits zero only +when the requested phase is valid. Completion also requires actual verification +run against the current expected `base_id`/`candidate_id`/`universe_id` snapshot, +plus one exact review binding per captured requirement obligation. Missing Node 22 or a +missing installed checker is a blocker for a triggered deterministic gate; do +not replace it with a manual pass. + +A valid result is `change-impact-validation-result/v1` and includes a +`change-impact-validation-receipt/v1`. The receipt carries only phase-relevant +canonical JSON SHA-256 input digests: capture and expected for discovery, plus +assessment for pre-build, and plus review for completion. To reuse a prior +result, pass that complete prior result with `--receipt`; fresh runs omit +`--receipt`. Fresh validation omits the prior result. The validator reruns +current validation first and then requires an exact receipt match. A missing, +legacy, malformed, or mismatched receipt is rejected. + +The object API snapshots phase-relevant plain JSON by own data descriptors +before validation and hashing. It rejects accessors, proxies, symbols, +non-enumerable or extra array properties, holes, non-JSON values, and cycles. + +Canonical identity recursively sorts object keys by JavaScript UTF-16 code +units, retains array order, encodes JSON as UTF-8, and hashes with SHA-256. +Whitespace and object-key order do not affect reuse; array order does. A change +to `validation_contract` or a receipt version invalidates reuse. Receipts are +identity checksums, not signatures or proof of exhaustive dependency discovery. + +Discovery can record material unknown boundaries. Pre-build and completion +reject them until their impact is resolved. A waiver records residual risk only +when its `rationale_ref` explains the waiver and its `authorization_ref` cites +existing policy or authorization; it does not create authorization and cannot +pass completion. + +## Document model + +All documents are JSON and have a fixed `schema_version`. See +`protocol.v1.json`, `example.completion.v1.json`, and the tests for complete +valid examples. + +* Capture: `change-impact-capture/v1` records roots, the union of base and + candidate edges, `base_id`/`candidate_id`/`universe_id`, and material unknown + boundaries. Pre-build and completion behavior assessments require at least one + root, edge, and contract/state-transition requirement, including local scope. + Discovery without an assessment and compact cosmetic controls may have none. + A shared assessment or material unknown boundary also requires at least one + recorded discovery root. An edge has a stable ID, consumer ID, contract ID, dependency kind + (`call`, `wrapper`, `config`, `registration`, `event`, `state`, or `public`), + presence (`base`, `candidate`, or `both`), and source identity. Its required + contract/state-transition records are distinct from edges: one consumer edge + can require navigation success, dirty-cancel, and stale-completion coverage. +* Expected context: `change-impact-expected-context/v1` independently records + the required impact scope, current capture ID, snapshot, required edges, + required contract/state transitions, and current verification source + identities. It catches an unsupported local claim, a known omitted consumer + or state transition, or an added/removed edge even when the assessment is + internally consistent. +* Assessment: `change-impact-assessment/v1` creates exactly one canonical + obligation per captured contract/state-transition requirement. `preserve` and `change` require planned verification; + a behavior pre-build and completion must retain at least one such bound verification; completion also requires its current execution record. `unaffected` needs a rationale and cannot carry verification; `waived` and + `blocked` remain unverified residual risk and cannot pass completion. + `change` and `waived` also require an opaque `authorization_ref` to existing + authority; the checker does not interpret it or create new authorization. + Verification plans bind contract, state transition, oracle, manual/command + steps reference, and source identity. Each actual verification also records + its exact executed snapshot, which must match the independent current expected + snapshot at completion; old records without that snapshot must be rerun. + Equivalent obligations may reuse a plan only through one named group with two + or more same-contract members and a justification. A plan cannot serve two + equivalence groups or both an equivalence group and a direct obligation. +* Review: `change-impact-review/v1` is a projection of the existing + assistant-review scope manifest and coverage ledger. It carries their source + IDs and review snapshot identity, then maps each assessment obligation and + edge to the original scope-item and coverage-concern IDs. The independently + supplied expected context records the required source and mappings, so a new + assessment-authored review list or wrong/stale review source fails. + +`review_context` may be `null` for discovery and pre-build, because review +artifacts do not yet exist. Completion requires its current source identities +and exact mappings. This is not a workflow-only dependency: standalone +debugging and review can supply the same explicit documents. + +Input documents are limited to 256 KiB, depth 16, 200 array items and 60 object +keys. Diagnostics contain reason codes only, never arbitrary input values. diff --git a/tools/change-impact/example.completion.v1.json b/tools/change-impact/example.completion.v1.json new file mode 100644 index 00000000..6ea3d382 --- /dev/null +++ b/tools/change-impact/example.completion.v1.json @@ -0,0 +1,45 @@ +{ + "capture": { + "schema_version": "change-impact-capture/v1", + "capture_id": "capture-example-current", + "snapshot": { "base_id": "base-example", "candidate_id": "candidate-example", "universe_id": "universe-example-current" }, + "roots": [{ "id": "root-1", "method": "bounded repository and configuration inventory", "source_id": "inventory-current" }], + "edges": [{ "id": "edge-editor", "consumer_id": "editor", "contract_id": "navigation", "dependency_kind": "call", "presence": "both", "source_id": "editor-current" }], + "requirements": [{ "id": "req-dirty-cancel", "edge_id": "edge-editor", "contract_id": "navigation", "state_transition": "dirty-cancel", "source_id": "editor-current" }], + "unknown_boundaries": [] + }, + "expected": { + "schema_version": "change-impact-expected-context/v1", + "authority_id": "independent-context-example", + "capture_id": "capture-example-current", + "snapshot": { "base_id": "base-example", "candidate_id": "candidate-example", "universe_id": "universe-example-current" }, + "required_impact_scope": "shared", + "required_edges": [{ "id": "edge-editor", "consumer_id": "editor", "contract_id": "navigation", "dependency_kind": "call", "presence": "both", "source_id": "editor-current" }], + "required_requirements": [{ "id": "req-dirty-cancel", "edge_id": "edge-editor", "contract_id": "navigation", "state_transition": "dirty-cancel", "source_id": "editor-current", "verification_source_id": "navigation-test-current" }], + "review_context": { + "scope_manifest_id": "assistant-review-scope-manifest-example", + "coverage_ledger_id": "assistant-review-coverage-ledger-example", + "review_snapshot_id": "assistant-review-snapshot-example", + "snapshot_id": "review-source-example-current", + "required_bindings": [{ "requirement_id": "req-dirty-cancel", "scope_item_id": "scope-navigation", "coverage_concern_id": "concern-dirty-cancel" }] + } + }, + "assessment": { + "schema_version": "change-impact-assessment/v1", + "capture_id": "capture-example-current", + "snapshot": { "base_id": "base-example", "candidate_id": "candidate-example", "universe_id": "universe-example-current" }, + "impact_scope": "shared", + "assessment_kind": "behavior", + "obligations": [{ "id": "obligation-dirty-cancel", "requirement_id": "req-dirty-cancel", "disposition": "preserve", "rationale_ref": "decision-preserve-dirty-cancel", "authorization_ref": null, "verification_id": "verify-dirty-cancel", "equivalence_group_id": null }], + "verification_plans": [{ "id": "verify-dirty-cancel", "contract_id": "navigation", "state_transition": "dirty-cancel", "oracle": "cancel keeps the current editor route", "steps_ref": "tests/navigation-dirty-cancel", "source_identity": "navigation-test-current" }], + "equivalence_groups": [], + "actual_verifications": [{ "verification_id": "verify-dirty-cancel", "outcome": "passed", "executed_snapshot": { "base_id": "base-example", "candidate_id": "candidate-example", "universe_id": "universe-example-current" }, "executed_source_identity": "navigation-test-current", "evidence_ref": "test-result-navigation-current" }] + }, + "review": { + "schema_version": "change-impact-review/v1", + "capture_id": "capture-example-current", + "snapshot": { "base_id": "base-example", "candidate_id": "candidate-example", "universe_id": "universe-example-current" }, + "review_source": { "scope_manifest_id": "assistant-review-scope-manifest-example", "coverage_ledger_id": "assistant-review-coverage-ledger-example", "review_snapshot_id": "assistant-review-snapshot-example", "snapshot_id": "review-source-example-current" }, + "bindings": [{ "obligation_id": "obligation-dirty-cancel", "requirement_id": "req-dirty-cancel", "scope_item_id": "scope-navigation", "coverage_concern_id": "concern-dirty-cancel" }] + } +} diff --git a/tools/change-impact/protocol.v1.json b/tools/change-impact/protocol.v1.json new file mode 100644 index 00000000..f6708931 --- /dev/null +++ b/tools/change-impact/protocol.v1.json @@ -0,0 +1,15 @@ +{ + "schema_version": "change-impact-protocol/v1", + "purpose": "Validate closure against separately supplied capture authority; it is not a universal dependency analyzer.", + "runtime": "Node.js 22 or newer, standard library only", + "phases": { + "discovery": ["capture", "expected context"], + "pre_build": ["capture", "expected context", "assessment with planned verification"], + "completion": ["capture", "expected context", "assessment with actual verification", "review bindings"] + }, + "review_context": "null is allowed during discovery and pre-build; completion requires the current assistant-review scope-manifest, coverage-ledger, snapshot, and requirement mappings.", + "dependency_kinds": ["call", "wrapper", "config", "registration", "event", "state", "public"], + "dispositions": ["preserve", "change", "unaffected", "waived", "blocked"], + "identity_rules": ["Capture records base/candidate union edges and required contract/state transitions. Pre-build and completion behavior assessments require nonempty discovery roots, edges, and requirements; pre-build requires at least one consumed planned verification. Discovery without an assessment and cosmetic controls may remain empty.", "Expected context independently supplies current snapshot, required scope, edges, transitions, verification source identities, and review source mappings.", "Each actual verification records the exact executed base/candidate/universe snapshot, which completion matches to the current expected snapshot alongside the verification source identity. A behavior completion retains at least one bound preserve/change verification; a verification plan serves one direct obligation or one named equivalence group, never more than one equivalence group.", "Successful validation results use change-impact-validation-result/v1 and carry a change-impact-validation-receipt/v1. Reuse reruns validation and compares the prior result through --receipt against canonical parsed JSON SHA-256 digests of the phase-relevant inputs.", "Completion requires actual verification and exact review projections; discovery completeness remains a caller-reviewed limit."], + "limits": { "max_document_bytes": 262144, "max_depth": 16, "max_array_items": 200, "max_object_keys": 60 } +} diff --git a/tools/change-impact/validate-change-impact.cjs b/tools/change-impact/validate-change-impact.cjs new file mode 100644 index 00000000..985a0d61 --- /dev/null +++ b/tools/change-impact/validate-change-impact.cjs @@ -0,0 +1,467 @@ +#!/usr/bin/env node +"use strict"; + +// This checker validates closure against caller-supplied capture authority. It +// deliberately does not inspect a repository, run evidence commands, or infer +// whether the supplied authority contains every possible runtime consumer. + +const fs = require("node:fs"); +const crypto = require("node:crypto"); +const { types } = require("node:util"); + +const MAX_BYTES = 256 * 1024; +const MAX_DEPTH = 16; +const MAX_ARRAY = 200; +const MAX_OBJECT_KEYS = 60; +const INVALID_SNAPSHOT = Symbol("invalid snapshot"); +const PHASES = new Set(["discovery", "pre_build", "completion"]); +const DEPENDENCY_KINDS = new Set(["call", "wrapper", "config", "registration", "event", "state", "public"]); +const PRESENCES = new Set(["base", "candidate", "both"]); +const DISPOSITIONS = new Set(["preserve", "change", "unaffected", "waived", "blocked"]); +const SCOPES = new Set(["not_applicable", "local", "shared", "unresolved"]); +const SAFE_ERROR_CODES = new Set(["INPUT_UNREADABLE", "INPUT_TOO_LARGE", "INPUT_MALFORMED", "INPUT_UNSAFE_SHAPE", "CLI_USAGE_INVALID", "RUNTIME_NODE22_OR_NEWER_REQUIRED"]); + +function issue(code) { + return { code }; +} + +function nodeRuntimeSupported() { + return Number(process.versions.node.split(".")[0]) >= 22; +} + +function runtimeFailure() { + return { schema_version: "change-impact-validation-result/v1", valid: false, complete: false, phase: null, reasons: [issue("RUNTIME_NODE22_OR_NEWER_REQUIRED")], receipt: null }; +} + +function canonicalJson(value) { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`; +} + +function digest(value) { + return `sha256:${crypto.createHash("sha256").update(canonicalJson(value), "utf8").digest("hex")}`; +} + +function snapshotJson(value, state, depth = 0, ancestors = new WeakSet()) { + if (depth > MAX_DEPTH) return INVALID_SNAPSHOT; + if (typeof value === "string" && value.length > MAX_BYTES) return INVALID_SNAPSHOT; + if (value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value))) { + state.bytes += Buffer.byteLength(JSON.stringify(value), "utf8"); + return state.bytes <= MAX_BYTES ? value : INVALID_SNAPSHOT; + } + if (typeof value !== "object" || types.isProxy(value) || ancestors.has(value)) return INVALID_SNAPSHOT; + ancestors.add(value); + try { + if (Array.isArray(value)) { + const keys = Reflect.ownKeys(value); + const length = value.length; + if (length > MAX_ARRAY || keys.length !== length + 1 || !keys.includes("length")) return INVALID_SNAPSHOT; + const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length"); + if (!lengthDescriptor || !Object.hasOwn(lengthDescriptor, "value") || lengthDescriptor.value !== length) return INVALID_SNAPSHOT; + const snapshot = []; + state.bytes += 2; + for (let index = 0; index < length; index += 1) { + const key = String(index); + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) return INVALID_SNAPSHOT; + if (index > 0) state.bytes += 1; + const child = snapshotJson(descriptor.value, state, depth + 1, ancestors); + if (child === INVALID_SNAPSHOT || state.bytes > MAX_BYTES) return INVALID_SNAPSHOT; + snapshot.push(child); + } + return snapshot; + } + if (Object.getPrototypeOf(value) !== Object.prototype) return INVALID_SNAPSHOT; + const keys = Reflect.ownKeys(value); + if (keys.length > MAX_OBJECT_KEYS || keys.some((key) => typeof key !== "string")) return INVALID_SNAPSHOT; + const snapshot = Object.create(null); + state.bytes += 2; + for (const [index, key] of keys.sort().entries()) { + if (key.length > MAX_BYTES) return INVALID_SNAPSHOT; + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) return INVALID_SNAPSHOT; + if (index > 0) state.bytes += 1; + state.bytes += Buffer.byteLength(JSON.stringify(key), "utf8") + 1; + const child = snapshotJson(descriptor.value, state, depth + 1, ancestors); + if (child === INVALID_SNAPSHOT || state.bytes > MAX_BYTES) return INVALID_SNAPSHOT; + Object.defineProperty(snapshot, key, { value: child, enumerable: true, writable: true, configurable: true }); + } + return snapshot; + } finally { + ancestors.delete(value); + } +} + +function snapshotPhaseInputs(phase, capture, expected, assessment, review) { + const snapshot = (value) => snapshotJson(value, { bytes: 0 }); + const captured = snapshot(capture); + const expectedSnapshot = snapshot(expected); + if (captured === INVALID_SNAPSHOT || expectedSnapshot === INVALID_SNAPSHOT) return null; + const assessmentSnapshot = phase === "discovery" ? undefined : snapshot(assessment); + const reviewSnapshot = phase === "completion" ? snapshot(review) : undefined; + return assessmentSnapshot === INVALID_SNAPSHOT || reviewSnapshot === INVALID_SNAPSHOT ? null : { capture: captured, expected: expectedSnapshot, assessment: assessmentSnapshot, review: reviewSnapshot }; +} + +function receiptFor(phase, capture, expected, assessment, review) { + return { + schema_version: "change-impact-validation-receipt/v1", + validation_contract: "change-impact-validator/v1", + canonicalization: "change-impact-canonical-json/v1", + digest_algorithm: "sha256", + phase, + input_digests: { + capture: digest(capture), + expected: digest(expected), + assessment: phase === "discovery" ? null : digest(assessment), + review: phase === "completion" ? digest(review) : null, + }, + }; +} + +function validReceipt(receipt) { + return exactKeys(receipt, ["schema_version", "validation_contract", "canonicalization", "digest_algorithm", "phase", "input_digests"]) && + receipt.schema_version === "change-impact-validation-receipt/v1" && receipt.validation_contract === "change-impact-validator/v1" && + receipt.canonicalization === "change-impact-canonical-json/v1" && receipt.digest_algorithm === "sha256" && PHASES.has(receipt.phase) && + exactKeys(receipt.input_digests, ["capture", "expected", "assessment", "review"]) && + [receipt.input_digests.capture, receipt.input_digests.expected].every((value) => typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value)) && + (receipt.phase === "discovery" ? receipt.input_digests.assessment === null && receipt.input_digests.review === null : + typeof receipt.input_digests.assessment === "string" && /^sha256:[0-9a-f]{64}$/.test(receipt.input_digests.assessment) && + (receipt.phase === "completion" ? typeof receipt.input_digests.review === "string" && /^sha256:[0-9a-f]{64}$/.test(receipt.input_digests.review) : receipt.input_digests.review === null)); +} + +function validPriorResult(result) { + return isObject(result) && exactKeys(result, ["schema_version", "valid", "complete", "phase", "reasons", "receipt"]) && + result.schema_version === "change-impact-validation-result/v1" && result.valid === true && typeof result.complete === "boolean" && + PHASES.has(result.phase) && result.complete === (result.phase === "completion") && Array.isArray(result.reasons) && result.reasons.length === 0 && + validReceipt(result.receipt) && result.phase === result.receipt.phase; +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function bounded(value, depth = 0) { + if (depth > MAX_DEPTH) return false; + if (Array.isArray(value)) return value.length <= MAX_ARRAY && value.every((item) => bounded(item, depth + 1)); + if (isObject(value)) return Object.keys(value).length <= MAX_OBJECT_KEYS && Object.values(value).every((item) => bounded(item, depth + 1)); + return value === null || ["string", "number", "boolean"].includes(typeof value); +} + +function nonBlank(value) { + return typeof value === "string" && value.trim().length > 0 && value.length <= 512; +} + +function exactKeys(value, keys) { + return isObject(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key)); +} + +function uniqueIds(items, field = "id") { + return Array.isArray(items) && items.every((item) => isObject(item) && nonBlank(item[field])) && new Set(items.map((item) => item[field])).size === items.length; +} + +function sameSnapshot(left, right) { + return isObject(left) && isObject(right) && + left.base_id === right.base_id && left.candidate_id === right.candidate_id && left.universe_id === right.universe_id; +} + +function sameEdge(left, right) { + return left.id === right.id && left.consumer_id === right.consumer_id && left.contract_id === right.contract_id && + left.dependency_kind === right.dependency_kind && left.presence === right.presence && left.source_id === right.source_id; +} + +function sameRequirement(left, right) { + return left.id === right.id && left.edge_id === right.edge_id && left.contract_id === right.contract_id && + left.state_transition === right.state_transition && left.source_id === right.source_id; +} + +function validSnapshot(value) { + return exactKeys(value, ["base_id", "candidate_id", "universe_id"]) && + nonBlank(value.base_id) && nonBlank(value.candidate_id) && nonBlank(value.universe_id); +} + +function indexBy(items, field = "id") { + return new Map(items.map((item) => [item[field], item])); +} + +function readDocument(path) { + let descriptor; + try { + descriptor = fs.openSync(path, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0)); + } catch { + throw new Error("INPUT_UNREADABLE"); + } + try { + if (!fs.fstatSync(descriptor).isFile()) throw new Error("INPUT_TOO_LARGE"); + const chunks = []; + let totalBytes = 0; + while (totalBytes <= MAX_BYTES) { + const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, MAX_BYTES + 1 - totalBytes)); + const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, totalBytes); + if (bytesRead === 0) break; + chunks.push(buffer.subarray(0, bytesRead)); + totalBytes += bytesRead; + } + if (totalBytes > MAX_BYTES) throw new Error("INPUT_TOO_LARGE"); + const value = JSON.parse(Buffer.concat(chunks, totalBytes).toString("utf8")); + if (!bounded(value)) throw new Error("INPUT_UNSAFE_SHAPE"); + return value; + } catch (error) { + if (error.message === "INPUT_TOO_LARGE" || error.message === "INPUT_UNSAFE_SHAPE") throw error; + throw new Error("INPUT_MALFORMED"); + } finally { + fs.closeSync(descriptor); + } +} + +function validateCapture(capture, expected, phase, assessment, reasons) { + if (!exactKeys(capture, ["schema_version", "capture_id", "snapshot", "roots", "edges", "requirements", "unknown_boundaries"]) || + capture.schema_version !== "change-impact-capture/v1" || !nonBlank(capture.capture_id) || !validSnapshot(capture.snapshot) || + !Array.isArray(capture.roots) || !Array.isArray(capture.edges) || !Array.isArray(capture.requirements) || !Array.isArray(capture.unknown_boundaries)) { + reasons.push(issue("CAPTURE_SCHEMA_INVALID")); return; + } + const rootsValid = uniqueIds(capture.roots) && capture.roots.every((root) => exactKeys(root, ["id", "method", "source_id"]) && nonBlank(root.method) && nonBlank(root.source_id)); + const edgesValid = uniqueIds(capture.edges) && capture.edges.every((edge) => + exactKeys(edge, ["id", "consumer_id", "contract_id", "dependency_kind", "presence", "source_id"]) && + nonBlank(edge.consumer_id) && nonBlank(edge.contract_id) && DEPENDENCY_KINDS.has(edge.dependency_kind) && PRESENCES.has(edge.presence) && nonBlank(edge.source_id)); + if (!rootsValid) reasons.push(issue("CAPTURE_ROOTS_INVALID")); + if (!edgesValid) { reasons.push(issue("CAPTURE_EDGES_INVALID")); return; } + const edges = indexBy(capture.edges); + const requirementsValid = uniqueIds(capture.requirements) && capture.requirements.every((requirement) => + exactKeys(requirement, ["id", "edge_id", "contract_id", "state_transition", "source_id"]) && nonBlank(requirement.edge_id) && + nonBlank(requirement.contract_id) && nonBlank(requirement.state_transition) && nonBlank(requirement.source_id) && + edges.has(requirement.edge_id) && edges.get(requirement.edge_id).contract_id === requirement.contract_id); + const boundariesValid = uniqueIds(capture.unknown_boundaries) && capture.unknown_boundaries.every((boundary) => + exactKeys(boundary, ["id", "material", "source_id"]) && typeof boundary.material === "boolean" && nonBlank(boundary.source_id)); + if (!requirementsValid) reasons.push(issue("CAPTURE_REQUIREMENTS_INVALID")); + if (!boundariesValid) reasons.push(issue("CAPTURE_BOUNDARIES_INVALID")); + if (reasons.length) return; + const behaviorAssessment = phase !== "discovery" && assessment?.assessment_kind === "behavior"; + if (behaviorAssessment && expected.required_impact_scope !== "shared") { + if (capture.roots.length === 0) reasons.push(issue("CAPTURE_BEHAVIOR_ROOTS_EMPTY")); + if (capture.edges.length === 0) reasons.push(issue("CAPTURE_BEHAVIOR_CONSUMERS_EMPTY")); + } + if (capture.roots.length === 0 && (expected.required_impact_scope === "shared" || + capture.unknown_boundaries.some((boundary) => boundary.material))) reasons.push(issue("CAPTURE_DISCOVERY_ROOTS_MISSING")); + if (expected.required_impact_scope === "shared" && capture.edges.length === 0) reasons.push(issue("CAPTURE_SHARED_CONSUMERS_EMPTY")); + if (expected.required_impact_scope === "shared" && capture.requirements.length === 0) reasons.push(issue("CAPTURE_SHARED_REQUIREMENTS_EMPTY")); + if (capture.edges.some((edge) => !capture.requirements.some((requirement) => requirement.edge_id === edge.id))) reasons.push(issue("CAPTURE_EDGE_REQUIREMENT_MISSING")); + if (capture.capture_id !== expected.capture_id || !sameSnapshot(capture.snapshot, expected.snapshot)) reasons.push(issue("CAPTURE_CONTEXT_MISMATCH")); + const captured = indexBy(capture.edges); + const required = indexBy(expected.required_edges); + if (captured.size !== required.size || [...required.entries()].some(([id, edge]) => !captured.has(id) || !sameEdge(captured.get(id), edge))) reasons.push(issue("CAPTURE_EXPECTED_EDGES_MISMATCH")); + const capturedRequirements = indexBy(capture.requirements); + const requiredRequirements = indexBy(expected.required_requirements); + if (capturedRequirements.size !== requiredRequirements.size || [...requiredRequirements.entries()].some(([id, requirement]) => + !capturedRequirements.has(id) || !sameRequirement(capturedRequirements.get(id), requirement))) reasons.push(issue("CAPTURE_EXPECTED_REQUIREMENTS_MISMATCH")); +} + +function validateExpected(expected, phase, reasons) { + if (!exactKeys(expected, ["schema_version", "authority_id", "capture_id", "snapshot", "required_impact_scope", "required_edges", "required_requirements", "review_context"]) || + expected.schema_version !== "change-impact-expected-context/v1" || !nonBlank(expected.authority_id) || !nonBlank(expected.capture_id) || + !validSnapshot(expected.snapshot) || !SCOPES.has(expected.required_impact_scope) || !Array.isArray(expected.required_edges) || !Array.isArray(expected.required_requirements)) { + reasons.push(issue("EXPECTED_CONTEXT_SCHEMA_INVALID")); return; + } + const edgesValid = uniqueIds(expected.required_edges) && expected.required_edges.every((edge) => + exactKeys(edge, ["id", "consumer_id", "contract_id", "dependency_kind", "presence", "source_id"]) && + nonBlank(edge.consumer_id) && nonBlank(edge.contract_id) && DEPENDENCY_KINDS.has(edge.dependency_kind) && PRESENCES.has(edge.presence) && nonBlank(edge.source_id)); + if (!edgesValid) { reasons.push(issue("EXPECTED_CONTEXT_EDGES_INVALID")); return; } + const edges = indexBy(expected.required_edges); + const requirementsValid = uniqueIds(expected.required_requirements) && expected.required_requirements.every((requirement) => + exactKeys(requirement, ["id", "edge_id", "contract_id", "state_transition", "source_id", "verification_source_id"]) && + nonBlank(requirement.edge_id) && nonBlank(requirement.contract_id) && nonBlank(requirement.state_transition) && nonBlank(requirement.source_id) && + nonBlank(requirement.verification_source_id) && edges.has(requirement.edge_id) && edges.get(requirement.edge_id).contract_id === requirement.contract_id); + if (!requirementsValid) { reasons.push(issue("EXPECTED_CONTEXT_REQUIREMENTS_INVALID")); return; } + const context = expected.review_context; + if (phase !== "completion" && context === null) return; + if (!exactKeys(context, ["scope_manifest_id", "coverage_ledger_id", "review_snapshot_id", "snapshot_id", "required_bindings"]) || + !nonBlank(context.scope_manifest_id) || !nonBlank(context.coverage_ledger_id) || !nonBlank(context.review_snapshot_id) || !nonBlank(context.snapshot_id) || + !Array.isArray(context.required_bindings) || !context.required_bindings.every((binding) => + exactKeys(binding, ["requirement_id", "scope_item_id", "coverage_concern_id"]) && nonBlank(binding.requirement_id) && nonBlank(binding.scope_item_id) && nonBlank(binding.coverage_concern_id)) || + !uniqueStrings(context.required_bindings.map((binding) => binding.requirement_id))) reasons.push(issue("EXPECTED_REVIEW_CONTEXT_INVALID")); + if (reasons.length === 0 && (context.required_bindings.length !== expected.required_requirements.length || + context.required_bindings.some((binding) => !expected.required_requirements.some((requirement) => requirement.id === binding.requirement_id)))) reasons.push(issue("EXPECTED_REVIEW_CONTEXT_INCOMPLETE")); +} + +function validateAssessment(assessment, capture, expected, phase, reasons) { + if (!exactKeys(assessment, ["schema_version", "capture_id", "snapshot", "impact_scope", "assessment_kind", "obligations", "verification_plans", "equivalence_groups", "actual_verifications"]) || + assessment.schema_version !== "change-impact-assessment/v1" || !nonBlank(assessment.capture_id) || !validSnapshot(assessment.snapshot) || + !SCOPES.has(assessment.impact_scope) || !["behavior", "cosmetic"].includes(assessment.assessment_kind) || + !Array.isArray(assessment.obligations) || !Array.isArray(assessment.verification_plans) || + !Array.isArray(assessment.equivalence_groups) || !Array.isArray(assessment.actual_verifications)) { + reasons.push(issue("ASSESSMENT_SCHEMA_INVALID")); return; + } + if (assessment.capture_id !== expected.capture_id || !sameSnapshot(assessment.snapshot, expected.snapshot)) reasons.push(issue("ASSESSMENT_CONTEXT_STALE")); + if (assessment.impact_scope !== expected.required_impact_scope) reasons.push(issue("IMPACT_SCOPE_CONTEXT_MISMATCH")); + if (assessment.assessment_kind === "cosmetic" && assessment.impact_scope !== "not_applicable") reasons.push(issue("COSMETIC_SCOPE_INVALID")); + if (assessment.assessment_kind === "behavior" && assessment.impact_scope === "not_applicable") reasons.push(issue("BEHAVIOR_SCOPE_INVALID")); + + const requirements = indexBy(capture.requirements); + const expectedRequirements = indexBy(expected.required_requirements); + if (!uniqueIds(assessment.obligations) || !assessment.obligations.every((obligation) => + exactKeys(obligation, ["id", "requirement_id", "disposition", "rationale_ref", "authorization_ref", "verification_id", "equivalence_group_id"]) && + nonBlank(obligation.requirement_id) && DISPOSITIONS.has(obligation.disposition) && nonBlank(obligation.rationale_ref) && + (obligation.authorization_ref === null || nonBlank(obligation.authorization_ref)) && + (obligation.verification_id === null || nonBlank(obligation.verification_id)) && + (obligation.equivalence_group_id === null || nonBlank(obligation.equivalence_group_id)))) { + reasons.push(issue("OBLIGATIONS_SCHEMA_INVALID")); return; + } + const obligationsByRequirement = new Map(); + for (const obligation of assessment.obligations) { + if (!requirements.has(obligation.requirement_id)) reasons.push(issue("OBLIGATION_REQUIREMENT_DANGLING")); + if (obligationsByRequirement.has(obligation.requirement_id)) reasons.push(issue("OBLIGATION_REQUIREMENT_DUPLICATE")); + obligationsByRequirement.set(obligation.requirement_id, obligation); + const needsVerification = ["preserve", "change"].includes(obligation.disposition); + if (["change", "waived"].includes(obligation.disposition) && !nonBlank(obligation.authorization_ref)) reasons.push(issue("AUTHORIZATION_REFERENCE_MISSING")); + if (!["change", "waived"].includes(obligation.disposition) && obligation.authorization_ref !== null) reasons.push(issue("AUTHORIZATION_REFERENCE_UNEXPECTED")); + if (needsVerification && !obligation.verification_id && !obligation.equivalence_group_id) reasons.push(issue("OBLIGATION_VERIFICATION_MISSING")); + if (!needsVerification && (obligation.verification_id || obligation.equivalence_group_id)) reasons.push(issue("UNVERIFIED_DISPOSITION_MARKED_VERIFIED")); + } + if (obligationsByRequirement.size !== requirements.size || [...requirements.keys()].some((id) => !obligationsByRequirement.has(id))) reasons.push(issue("CAPTURE_REQUIREMENT_OBLIGATION_MISSING")); + + const plansValid = uniqueIds(assessment.verification_plans) && assessment.verification_plans.every((plan) => + exactKeys(plan, ["id", "contract_id", "state_transition", "oracle", "steps_ref", "source_identity"]) && + nonBlank(plan.contract_id) && nonBlank(plan.state_transition) && nonBlank(plan.oracle) && nonBlank(plan.steps_ref) && nonBlank(plan.source_identity)); + if (!plansValid) { reasons.push(issue("VERIFICATION_PLANS_INVALID")); return; } + const plans = indexBy(assessment.verification_plans); + for (const obligation of assessment.obligations) { + if (obligation.verification_id && !plans.has(obligation.verification_id)) reasons.push(issue("VERIFICATION_PLAN_DANGLING")); + if (obligation.verification_id && requirements.has(obligation.requirement_id) && plans.has(obligation.verification_id) && + (plans.get(obligation.verification_id).contract_id !== requirements.get(obligation.requirement_id).contract_id || + plans.get(obligation.verification_id).state_transition !== requirements.get(obligation.requirement_id).state_transition || + plans.get(obligation.verification_id).source_identity !== expectedRequirements.get(obligation.requirement_id)?.verification_source_id)) reasons.push(issue("VERIFICATION_REQUIREMENT_MISMATCH")); + } + + const groupsValid = uniqueIds(assessment.equivalence_groups) && assessment.equivalence_groups.every((group) => + exactKeys(group, ["id", "member_obligation_ids", "justification_ref", "verification_id"]) && + Array.isArray(group.member_obligation_ids) && group.member_obligation_ids.length >= 2 && uniqueStrings(group.member_obligation_ids) && + nonBlank(group.justification_ref) && nonBlank(group.verification_id)); + if (!groupsValid) { reasons.push(issue("EQUIVALENCE_GROUPS_INVALID")); return; } + const groups = indexBy(assessment.equivalence_groups); + for (const group of assessment.equivalence_groups) { + const members = group.member_obligation_ids.map((id) => assessment.obligations.find((obligation) => obligation.id === id)); + if (members.some((member) => !member) || !plans.has(group.verification_id)) { reasons.push(issue("EQUIVALENCE_GROUP_DANGLING")); continue; } + const contracts = new Set(members.map((member) => requirements.get(member.requirement_id)?.contract_id)); + const transitions = new Set(members.map((member) => requirements.get(member.requirement_id)?.state_transition)); + const verificationSources = new Set(members.map((member) => expectedRequirements.get(member.requirement_id)?.verification_source_id)); + if (contracts.size !== 1 || transitions.size !== 1 || verificationSources.size !== 1 || plans.get(group.verification_id).contract_id !== [...contracts][0] || + plans.get(group.verification_id).state_transition !== [...transitions][0] || plans.get(group.verification_id).source_identity !== [...verificationSources][0]) reasons.push(issue("EQUIVALENCE_GROUP_UNJUSTIFIED")); + if (members.some((member) => member.equivalence_group_id !== group.id || member.verification_id !== null)) reasons.push(issue("EQUIVALENCE_GROUP_BINDING_INVALID")); + } + for (const obligation of assessment.obligations) if (obligation.equivalence_group_id && + (!groups.has(obligation.equivalence_group_id) || !groups.get(obligation.equivalence_group_id).member_obligation_ids.includes(obligation.id))) reasons.push(issue("EQUIVALENCE_GROUP_BINDING_INVALID")); + + const directPlanUse = new Map(); + for (const obligation of assessment.obligations) if (obligation.verification_id) directPlanUse.set(obligation.verification_id, (directPlanUse.get(obligation.verification_id) || 0) + 1); + if ([...directPlanUse.values()].some((count) => count > 1)) reasons.push(issue("DIRECT_VERIFICATION_REUSE_REQUIRES_EQUIVALENCE")); + const equivalencePlanUse = new Map(); + for (const group of assessment.equivalence_groups) equivalencePlanUse.set(group.verification_id, (equivalencePlanUse.get(group.verification_id) || 0) + 1); + if ([...equivalencePlanUse.values()].some((count) => count > 1)) reasons.push(issue("EQUIVALENCE_VERIFICATION_REUSE_REQUIRES_SEPARATE_PLANS")); + const consumedPlans = new Set([...directPlanUse.keys(), ...assessment.equivalence_groups.map((group) => group.verification_id)]); + if (assessment.verification_plans.some((plan) => !consumedPlans.has(plan.id))) reasons.push(issue("VERIFICATION_PLAN_ORPHANED")); + if (assessment.equivalence_groups.some((group) => directPlanUse.has(group.verification_id))) reasons.push(issue("VERIFICATION_REUSE_BINDING_INVALID")); + + const actualsValid = uniqueIds(assessment.actual_verifications, "verification_id") && assessment.actual_verifications.every((actual) => + exactKeys(actual, ["verification_id", "outcome", "executed_snapshot", "executed_source_identity", "evidence_ref"]) && + nonBlank(actual.verification_id) && actual.outcome === "passed" && validSnapshot(actual.executed_snapshot) && + nonBlank(actual.executed_source_identity) && nonBlank(actual.evidence_ref)); + if (!actualsValid) { reasons.push(issue("ACTUAL_VERIFICATIONS_INVALID")); return; } + if (phase === "pre_build" && assessment.actual_verifications.length > 0) reasons.push(issue("PREBUILD_EXECUTION_NOT_ALLOWED")); + if (phase === "pre_build" && assessment.assessment_kind === "behavior" && consumedPlans.size === 0) reasons.push(issue("BEHAVIOR_PREBUILD_VERIFICATION_MISSING")); + if (phase !== "discovery" && (capture.unknown_boundaries.some((boundary) => boundary.material) || assessment.impact_scope === "unresolved")) reasons.push(issue("MATERIAL_IMPACT_UNRESOLVED")); + if (phase === "pre_build" && assessment.obligations.some((obligation) => obligation.disposition === "blocked")) reasons.push(issue("BLOCKED_IMPACT_UNRESOLVED")); + if (phase === "completion") { + if (assessment.obligations.some((obligation) => ["waived", "blocked"].includes(obligation.disposition))) reasons.push(issue("UNVERIFIED_RESIDUAL_RISK")); + const actuals = indexBy(assessment.actual_verifications, "verification_id"); + const needed = new Map(assessment.obligations.flatMap((obligation) => obligation.verification_id ? [[obligation.verification_id, obligation.requirement_id]] : obligation.equivalence_group_id ? [[groups.get(obligation.equivalence_group_id)?.verification_id, obligation.requirement_id]] : [])); + if (assessment.assessment_kind === "behavior" && needed.size === 0) reasons.push(issue("BEHAVIOR_COMPLETION_VERIFICATION_MISSING")); + for (const [verificationId, requirementId] of needed) if (!actuals.has(verificationId) || + !sameSnapshot(actuals.get(verificationId).executed_snapshot, expected.snapshot) || + actuals.get(verificationId).executed_source_identity !== plans.get(verificationId)?.source_identity || + actuals.get(verificationId).executed_source_identity !== expectedRequirements.get(requirementId)?.verification_source_id) reasons.push(issue("EXECUTED_VERIFICATION_MISSING_OR_STALE")); + if (actuals.size !== consumedPlans.size || [...actuals.keys()].some((id) => !consumedPlans.has(id))) reasons.push(issue("ACTUAL_VERIFICATION_ORPHANED")); + } +} + +function uniqueStrings(values) { + return values.every(nonBlank) && new Set(values).size === values.length; +} + +function validateReview(review, assessment, expected, reasons) { + if (!exactKeys(review, ["schema_version", "capture_id", "snapshot", "review_source", "bindings"]) || review.schema_version !== "change-impact-review/v1" || + !nonBlank(review.capture_id) || !validSnapshot(review.snapshot) || !Array.isArray(review.bindings)) { reasons.push(issue("REVIEW_SCHEMA_INVALID")); return; } + if (review.capture_id !== expected.capture_id || !sameSnapshot(review.snapshot, expected.snapshot)) reasons.push(issue("REVIEW_CONTEXT_STALE")); + const source = review.review_source; + const expectedSource = expected.review_context; + if (!exactKeys(source, ["scope_manifest_id", "coverage_ledger_id", "review_snapshot_id", "snapshot_id"]) || + source.scope_manifest_id !== expectedSource.scope_manifest_id || source.coverage_ledger_id !== expectedSource.coverage_ledger_id || + source.review_snapshot_id !== expectedSource.review_snapshot_id || source.snapshot_id !== expectedSource.snapshot_id) reasons.push(issue("REVIEW_SOURCE_STALE_OR_WRONG")); + if (!review.bindings.every((binding) => exactKeys(binding, ["obligation_id", "requirement_id", "scope_item_id", "coverage_concern_id"]) && + nonBlank(binding.obligation_id) && nonBlank(binding.requirement_id) && nonBlank(binding.scope_item_id) && nonBlank(binding.coverage_concern_id)) || + !uniqueStrings(review.bindings.map((binding) => binding.obligation_id)) || !uniqueStrings(review.bindings.map((binding) => binding.requirement_id))) { reasons.push(issue("REVIEW_BINDINGS_INVALID")); return; } + const obligations = indexBy(assessment.obligations); + const expectedBindings = indexBy(expectedSource.required_bindings, "requirement_id"); + if (review.bindings.length !== obligations.size || review.bindings.length !== expectedBindings.size || review.bindings.some((binding) => + !obligations.has(binding.obligation_id) || obligations.get(binding.obligation_id).requirement_id !== binding.requirement_id || + !expectedBindings.has(binding.requirement_id) || expectedBindings.get(binding.requirement_id).scope_item_id !== binding.scope_item_id || + expectedBindings.get(binding.requirement_id).coverage_concern_id !== binding.coverage_concern_id)) reasons.push(issue("REVIEW_OBLIGATION_BINDING_INCOMPLETE")); +} + +function validate({ phase, capture, expected, assessment, review, priorResult }) { + if (!nodeRuntimeSupported()) return runtimeFailure(); + const reasons = []; + if (!PHASES.has(phase)) return { schema_version: "change-impact-validation-result/v1", valid: false, complete: false, phase: null, reasons: [issue("PHASE_INVALID")], receipt: null }; + const inputs = snapshotPhaseInputs(phase, capture, expected, assessment, review); + if (!inputs) return { schema_version: "change-impact-validation-result/v1", valid: false, complete: false, phase, reasons: [issue("INPUT_UNSAFE_SHAPE")], receipt: null }; + ({ capture, expected, assessment, review } = inputs); + validateExpected(expected, phase, reasons); + if (reasons.length === 0) validateCapture(capture, expected, phase, assessment, reasons); + if (phase !== "discovery" && reasons.length === 0) validateAssessment(assessment, capture, expected, phase, reasons); + if (phase === "completion" && reasons.length === 0) validateReview(review, assessment, expected, reasons); + if (reasons.length > 0) return { schema_version: "change-impact-validation-result/v1", valid: false, complete: false, phase, reasons, receipt: null }; + const receipt = receiptFor(phase, capture, expected, assessment, review); + if (priorResult !== undefined) { + const priorSnapshot = snapshotJson(priorResult, { bytes: 0 }); + if (priorSnapshot === INVALID_SNAPSHOT || !validPriorResult(priorSnapshot)) reasons.push(issue("RECEIPT_SCHEMA_INVALID")); + else if (canonicalJson(priorSnapshot.receipt) !== canonicalJson(receipt)) reasons.push(issue("RECEIPT_MISMATCH")); + } + return { schema_version: "change-impact-validation-result/v1", valid: reasons.length === 0, complete: phase === "completion" && reasons.length === 0, phase, reasons, receipt: reasons.length === 0 ? receipt : null }; +} + +function parseArgs(argv) { + const options = {}; + for (let index = 0; index < argv.length; index += 2) { + const key = argv[index]; const value = argv[index + 1]; + if (!key || !["--phase", "--capture", "--expected", "--assessment", "--review", "--receipt"].includes(key) || value === undefined || options[key]) throw new Error("CLI_USAGE_INVALID"); + options[key] = value; + } + if (!options["--phase"] || !options["--capture"] || !options["--expected"] || + (options["--phase"] !== "discovery" && !options["--assessment"]) || + (options["--phase"] === "completion" && !options["--review"])) throw new Error("CLI_USAGE_INVALID"); + return options; +} + +function main() { + try { + if (!nodeRuntimeSupported()) throw new Error("RUNTIME_NODE22_OR_NEWER_REQUIRED"); + const options = parseArgs(process.argv.slice(2)); + const result = validate({ + phase: options["--phase"], + capture: readDocument(options["--capture"]), + expected: readDocument(options["--expected"]), + assessment: options["--assessment"] ? readDocument(options["--assessment"]) : undefined, + review: options["--review"] ? readDocument(options["--review"]) : undefined, + priorResult: options["--receipt"] ? readDocument(options["--receipt"]) : undefined, + }); + process.stdout.write(`${JSON.stringify(result)}\n`); + process.exitCode = result.valid ? 0 : 1; + } catch (error) { + const code = SAFE_ERROR_CODES.has(error?.message) ? error.message : "VALIDATOR_INPUT_ERROR"; + process.stdout.write(`${JSON.stringify({ schema_version: "change-impact-validation-result/v1", valid: false, complete: false, phase: null, reasons: [issue(code)], receipt: null })}\n`); + process.exitCode = 2; + } +} + +if (require.main === module) main(); + +module.exports = { validate, readDocument, MAX_BYTES }; diff --git a/tools/evals/lib/skill-eval-fixtures.sh b/tools/evals/lib/skill-eval-fixtures.sh index 580126c3..13bb67a3 100644 --- a/tools/evals/lib/skill-eval-fixtures.sh +++ b/tools/evals/lib/skill-eval-fixtures.sh @@ -117,7 +117,7 @@ final_snapshot_identity_schema = { } inline_eval_only_roots = { "assistant-workflow" => [ - { "name" => "current_assistant_review_contract", "type" => "object", "required" => false, "object_fields" => [{ "name" => "schema_version", "type" => "enum", "required" => true, "enum_values" => ["7.1"] }] }, + { "name" => "current_assistant_review_contract", "type" => "object", "required" => false, "object_fields" => [{ "name" => "schema_version", "type" => "enum", "required" => true, "enum_values" => ["7.2"] }] }, { "name" => "current_final_batch", "type" => "object", "required" => false, "object_fields" => [{ "name" => "review_snapshot_id", "type" => "string", "required" => true }, final_snapshot_identity_schema] }, { "name" => "harness_entry_state", "type" => "object", "required" => false }, {