From 1333d9488537635bb65060fbe45d33d299485811 Mon Sep 17 00:00:00 2001 From: Laimis Date: Sat, 12 Sep 2026 23:01:14 +0300 Subject: [PATCH 01/14] Require verified impact coverage for shared changes --- README.md | 11 + .../variants/workflow-kernel-v1/manifest.json | 16 +- docs/skill-contract-design-guide.md | 4 + install.ps1 | 9 + skills/assistant-debugging/SKILL.md | 10 + .../contracts/handoffs.yaml | 20 +- .../assistant-debugging/contracts/index.yaml | 20 +- .../assistant-debugging/contracts/input.yaml | 31 +- .../assistant-debugging/contracts/output.yaml | 20 +- .../contracts/phase-gates.yaml | 15 +- skills/assistant-debugging/evals/cases.json | 15 + .../references/change-impact.md | 23 ++ skills/assistant-review/SKILL.md | 11 +- .../assistant-review/contracts/handoffs.yaml | 33 +- skills/assistant-review/contracts/index.yaml | 20 +- skills/assistant-review/contracts/input.yaml | 41 ++- skills/assistant-review/contracts/output.yaml | 25 +- .../contracts/phase-gates.yaml | 27 +- skills/assistant-review/evals/cases.json | 15 + .../references/change-impact.md | 22 ++ .../references/review-batch.md | 2 +- .../references/review-loop.md | 2 +- .../references/skill-contract-design-guide.md | 4 + skills/assistant-workflow/SKILL.md | 4 +- .../contracts/handoffs.yaml | 65 +++- .../assistant-workflow/contracts/index.yaml | 25 +- .../assistant-workflow/contracts/input.yaml | 31 +- .../assistant-workflow/contracts/output.yaml | 23 +- .../contracts/phase-gates.yaml | 13 +- skills/assistant-workflow/evals/cases.json | 257 +++++++------- .../references/change-impact.md | 28 ++ .../assistant-workflow/references/phases.md | 8 + .../references/phases/build.md | 8 + .../references/phases/decompose.md | 8 + .../references/phases/design.md | 8 + .../references/phases/discover.md | 8 + .../references/phases/document.md | 8 + .../references/phases/plan.md | 8 + .../phases/preparation-completion.md | 8 + .../references/phases/review.md | 8 + tests/change-impact/change-impact.test.cjs | 266 ++++++++++++++ .../independent-fixture-truth/capture.mjs | 26 ++ .../fixture-authority.json | 13 + .../fixture-src/base-registry.json | 1 + .../fixture-src/cache-retry.mjs | 11 + .../fixture-src/candidate-registry.json | 1 + .../fixture-src/unrelated-dirty-note.txt | 1 + .../fixture-src/validation.mjs | 21 ++ .../truth.expected.json | 42 +++ .../fixtures/multidomain.v1.json | 70 ++++ .../independent-fixture-adapter.test.cjs | 197 +++++++++++ .../assistant-review-reference-contracts.sh | 18 +- tests/p0-p4/change-impact-contracts.sh | 154 +++++++++ .../p0-p4/context-budget-report-contracts.sh | 2 +- tests/p0-p4/installer-contracts.sh | 58 ++++ .../feature-preparation-response-fixtures.sh | 20 +- tests/p0-p4/worker-status-contracts.sh | 4 +- ...kflow-architecture-completion-contracts.sh | 10 +- tests/test-p0-p4-contracts.sh | 1 + tests/windows/installer-contracts.ps1 | 50 +++ tools/change-impact/README.md | 86 +++++ .../change-impact/example.completion.v1.json | 45 +++ tools/change-impact/protocol.v1.json | 15 + .../change-impact/validate-change-impact.cjs | 324 ++++++++++++++++++ tools/evals/lib/skill-eval-fixtures.sh | 2 +- 65 files changed, 2149 insertions(+), 203 deletions(-) create mode 100644 skills/assistant-debugging/references/change-impact.md create mode 100644 skills/assistant-review/references/change-impact.md create mode 100644 skills/assistant-workflow/references/change-impact.md create mode 100644 tests/change-impact/change-impact.test.cjs create mode 100644 tests/change-impact/fixtures/independent-fixture-truth/capture.mjs create mode 100644 tests/change-impact/fixtures/independent-fixture-truth/fixture-authority.json create mode 100644 tests/change-impact/fixtures/independent-fixture-truth/fixture-src/base-registry.json create mode 100644 tests/change-impact/fixtures/independent-fixture-truth/fixture-src/cache-retry.mjs create mode 100644 tests/change-impact/fixtures/independent-fixture-truth/fixture-src/candidate-registry.json create mode 100644 tests/change-impact/fixtures/independent-fixture-truth/fixture-src/unrelated-dirty-note.txt create mode 100644 tests/change-impact/fixtures/independent-fixture-truth/fixture-src/validation.mjs create mode 100644 tests/change-impact/fixtures/independent-fixture-truth/truth.expected.json create mode 100644 tests/change-impact/fixtures/multidomain.v1.json create mode 100644 tests/change-impact/independent-fixture-adapter.test.cjs create mode 100644 tests/p0-p4/change-impact-contracts.sh create mode 100644 tools/change-impact/README.md create mode 100644 tools/change-impact/example.completion.v1.json create mode 100644 tools/change-impact/protocol.v1.json create mode 100644 tools/change-impact/validate-change-impact.cjs diff --git a/README.md b/README.md index ed7f9998..3169b46c 100644 --- a/README.md +++ b/README.md @@ -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..4e959c58 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": 2320, + "candidate_selected_initial_words": 486, + "selected_initial_word_delta": -1834, + "baseline_total_initial_words": 2909, + "candidate_total_initial_words": 1075, + "baseline_selected_entry_words": 3925, + "candidate_selected_entry_words": 2091, + "selected_entry_word_delta": -1834, "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..0e97b8f5 100644 --- a/docs/skill-contract-design-guide.md +++ b/docs/skill-contract-design-guide.md @@ -512,3 +512,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** — phase-aware evidence preserves + identity. Discovery may report gaps; completion needs verification and + review. Loaded-path tools missing runtime block. Fanout alone + does not escalate architecture, QA, or lane. diff --git a/install.ps1 b/install.ps1 index e6f76b3d..d3ad1c64 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2251,7 +2251,9 @@ 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 { @@ -2282,6 +2284,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 = @( @@ -2314,6 +2317,9 @@ function Invoke-AssistantFrameworkInstall { 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 +2347,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/skills/assistant-debugging/SKILL.md b/skills/assistant-debugging/SKILL.md index 1d4309bf..2191eb65 100644 --- a/skills/assistant-debugging/SKILL.md +++ b/skills/assistant-debugging/SKILL.md @@ -17,6 +17,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 +39,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, 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 +68,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..c47f399d 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,24 @@ 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: "artifact_identity, phase, impact_scope, and applicable capture/expected/assessment/checker refs are passed unchanged; discovery carries no assessment ref; a symptom-only consumer list is not impact evidence." + 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: 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: root_cause type: string required: true diff --git a/skills/assistant-debugging/contracts/index.yaml b/skills/assistant-debugging/contracts/index.yaml index 0e6885bd..c3496ad0 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_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_evidence] budget_words: 5000 fallback: diff --git a/skills/assistant-debugging/contracts/input.yaml b/skills/assistant-debugging/contracts/input.yaml index 5fd1a8da..dede4fc6 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,32 @@ 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 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. local requires causal_evidence_ref and ordinary regression verification/self-review; shared or unresolved requires expanded_artifact_carried=true." + object_fields: + - {name: impact_scope, type: enum, required: true, enum_values: [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 artifact_identity and capture/expected-context refs. Discovery may report gaps; an expanded repair may not claim pre-build or completion evidence without 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: 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..adc54c40 100644 --- a/skills/assistant-debugging/contracts/output.yaml +++ b/skills/assistant-debugging/contracts/output.yaml @@ -1,8 +1,26 @@ -schema_version: "1.0" +schema_version: "1.1" contract: output skill: assistant-debugging artifacts: + - 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: 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 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..2644aff1 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 @@ -64,6 +64,10 @@ gates: checkpoint_end: "--- PHASE: FIX COMPLETE ---" condition: "when can_modify_code is true and a fix or mitigation is attempted" exit_assertions: + - id: FX_CHANGE_IMPACT_PREBUILD + check: "Every behavior repair records compact applicability evidence. When the diagnosis/proposed repair has shared or unresolved material impact, or explicitly carries an expanded artifact, a current common-checker pre-build assessment covers the bounded base/candidate consumer union and planned obligations before source mutation. 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; load references/change-impact.md and repair the assessment or report its explicit blocker." - id: FX1 check: "Each changed file maps to the confirmed cause, mitigation, or regression test" on_fail: "Remove unrelated changes or document approved scope expansion" @@ -78,6 +82,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 +97,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..2af73361 100644 --- a/skills/assistant-debugging/evals/cases.json +++ b/skills/assistant-debugging/evals/cases.json @@ -115,6 +115,21 @@ "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"] + } } ] } diff --git a/skills/assistant-debugging/references/change-impact.md b/skills/assistant-debugging/references/change-impact.md new file mode 100644 index 00000000..a8057ec2 --- /dev/null +++ b/skills/assistant-debugging/references/change-impact.md @@ -0,0 +1,23 @@ +# Change-impact during debugging + +Load when the reported symptom, diagnosis, or proposed fix 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 a source fix, shared or +unresolved impact needs the common pre-build assessment, including bounded +base/candidate consumers and distinct material transitions. 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..d6764995 100644 --- a/skills/assistant-review/SKILL.md +++ b/skills/assistant-review/SKILL.md @@ -10,6 +10,7 @@ description: "Review code, fix actionable findings, and run one fresh re-review. 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. - `entry`: load `contracts/input.yaml` review-entry fields selected by `review-entry-fields` in `contracts/index.yaml`. +- `change_impact`: load its reference only when triggered; keep it out of reviewer bundles. - `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. @@ -19,7 +20,7 @@ Canonical input, output, phase-gate, and handoff schemas remain authoritative at 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. +Selectors resolve canonical fields. Entry loads no review guidance. 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. @@ -104,14 +105,16 @@ Review a Pack only when workflow metadata or review material says one applies. I ## 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. +Impact projection preserves canonical review closure; it never makes audit or +review-fix results clean by itself. + 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. 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`. diff --git a/skills/assistant-review/contracts/handoffs.yaml b/skills/assistant-review/contracts/handoffs.yaml index 83513dfe..d92577ba 100644 --- a/skills/assistant-review/contracts/handoffs.yaml +++ b/skills/assistant-review/contracts/handoffs.yaml @@ -4,11 +4,11 @@ # 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 +# v7.2 migration: persisted 6.0 and incompatible 7.0 packets are invalidated and +# rebuilt from a freshly rehashed 7.2 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::; # non-colliding records use that base ID; every colliding record appends @@ -32,7 +32,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 packets are invalidated and rebuilt under 7.2; 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." - "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 +44,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 +509,29 @@ 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; 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: gap_or_blocker_ref, type: string, required: conditional, condition: "status in [gaps_reported, blocked, invalid]"} + - name: coverage_obligations type: string[] required: true diff --git a/skills/assistant-review/contracts/index.yaml b/skills/assistant-review/contracts/index.yaml index 78d0bcda..8788c6b7 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 @@ -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_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_review_projection] budget_words: 10000 fallback: diff --git a/skills/assistant-review/contracts/input.yaml b/skills/assistant-review/contracts/input.yaml index e748b70f..70867da8 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,35 @@ 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 change 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. local requires causal_evidence_ref and ordinary review/verification; shared or unresolved requires expanded_artifact_carried=true." + object_fields: + - {name: impact_scope, type: enum, required: true, enum_values: [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 the supplied artifact_identity, capture/expected/assessment refs, and current review scope. Read-only audit may report a missing projection or material gap 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: 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 +590,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. 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." on_missing: fail object_fields: - name: source_schema_version @@ -579,7 +608,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 +629,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. 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 +646,7 @@ 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 diff --git a/skills/assistant-review/contracts/output.yaml b/skills/assistant-review/contracts/output.yaml index 66e66a13..7dcc471a 100644 --- a/skills/assistant-review/contracts/output.yaml +++ b/skills/assistant-review/contracts/output.yaml @@ -1,11 +1,32 @@ # 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_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. 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, 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: 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 +937,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 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..1eb00ff6 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,11 @@ 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 + check: "Every behavior review records compact applicability evidence. 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." @@ -223,6 +228,11 @@ gates: checkpoint_end: null exit_assertions: + - id: F_CHANGE_IMPACT_PREBUILD + check: "Before a review-fix edits a triggered shared or unresolved impact scope, a current common-checker pre-build assessment has valid planned obligations for the bounded 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; repair the pre-build assessment or return the review-fix as blocked/remaining." + - id: F1 check: "ALL must-fix items were addressed (not just some)" on_fail: "Fix remaining must-fix items before proceeding" @@ -235,6 +245,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 +346,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..ed5e2751 100644 --- a/skills/assistant-review/evals/cases.json +++ b/skills/assistant-review/evals/cases.json @@ -2027,6 +2027,21 @@ {"operator":"equals","path":["qa_evaluation_result","score_progression",0,"weighted_score"],"expected":1.8} ] } + }, + { + "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..33530542 --- /dev/null +++ b/skills/assistant-review/references/change-impact.md @@ -0,0 +1,22 @@ +# Change-impact review projection + +Load for a review that covers a behavior 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. 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..257d6ca1 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 under 7.2 from a fresh snapshot before `CLEAN`/`ISSUES_FIXED`. 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..8a08c69c 100644 --- a/skills/assistant-review/references/review-loop.md +++ b/skills/assistant-review/references/review-loop.md @@ -19,7 +19,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 under 7.2 before clean. 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. 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..0e97b8f5 100644 --- a/skills/assistant-skill-creator/references/skill-contract-design-guide.md +++ b/skills/assistant-skill-creator/references/skill-contract-design-guide.md @@ -512,3 +512,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** — phase-aware evidence preserves + identity. Discovery may report gaps; completion needs verification and + review. Loaded-path tools missing runtime 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..8049849b 100644 --- a/skills/assistant-workflow/SKILL.md +++ b/skills/assistant-workflow/SKILL.md @@ -24,6 +24,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 +51,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 changes or a local/shared/unresolved impact decision affects verification scope. Every behavior change records applicability; only shared, materially unresolved, or explicitly carried expanded work requires the common pre-Build/completion path. - `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 +62,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 diff --git a/skills/assistant-workflow/contracts/handoffs.yaml b/skills/assistant-workflow/contracts/handoffs.yaml index b4c6c2b6..c37e7778 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,22 @@ 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." + 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: requirement_acceptance_map type: string required: true @@ -1478,7 +1494,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 +1618,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: "Same compact v1 artifact reference envelope supplied to planning, Build, and Review." + validation: "artifact_identity, phase, capture_ref, expected_context_ref, validator_result_ref, impact_scope, and status exactly equal the current task packet; assessment_ref is present only for pre-build or completion. The CodeWriter may not relabel a pre-build plan as executed completion evidence." + object_fields: + - {name: artifact_identity, type: string, required: true} + - {name: phase, type: enum, required: true, enum_values: [discovery, pre_build, completion]} + - {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: 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 +1944,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 +2210,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 @@ -2239,6 +2275,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: "Expanded common impact artifact BuilderTester validates without relabelling discovery as later-phase evidence." + validation: "Preserves artifact_identity, phase, impact_scope, status, capture_ref, expected_context_ref, and validator_result_ref; assessment_ref is carried only for pre-build or completion." + 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: build_command type: string required: false @@ -2536,7 +2587,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 +2726,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..da5ac8c7 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_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_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..35a14e0f 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,35 @@ fields: default: [] on_missing: skip + - name: change_impact_applicability + type: object + required: conditional + condition: "a behavior or locality/shared/unresolved impact decision affects verification" + description: "Applicability decision." + validation: "local needs causal evidence; shared/unresolved needs expanded artifact." + object_fields: + - {name: impact_scope, type: enum, required: true, enum_values: [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: "Expanded assessment refs." + validation: "Includes identity, phase, capture, expected context, and an assessment only for pre-build or completion." + 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: 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..f0d020b2 100644 --- a/skills/assistant-workflow/contracts/output.yaml +++ b/skills/assistant-workflow/contracts/output.yaml @@ -3,7 +3,7 @@ # 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 @@ -49,6 +49,25 @@ completion_tiers: checkpoint_policy: "explicit phase checkpoints are required" artifacts: + - name: change_impact_evidence + type: object + required: conditional + condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" + description: "Cross-phase reference envelope for explicitly expanded common change-impact v1 documents; it does not duplicate their obligation ledger." + validation: "artifact_identity is retained unchanged from discovery through Build and Review. discovery may report gaps or unavailable checker without claiming planned/executed verification or review. pre_build requires a valid common-checker assessment with no material unknown boundary. completion additionally requires actual verification and a review projection from assistant-review that binds the original canonical scope_manifest, coverage_ledger, and current review snapshot; that projection never substitutes for assistant-review terminal coverage/result checks. Evidenced local work records compact applicability plus ordinary verification/self-review instead." + on_fail: "For an expanded gate, resolve the checker from the active skill installation and run its explicit discovery, pre-build, or completion phase. Missing Node 22 or checker is blocked/unverified; do not replace it with manual-pass prose." + 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: 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 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 @@ -1862,7 +1881,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..a20debbe 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 @@ -488,6 +488,11 @@ gates: check: "At least one relevant verification exists and passed: runnable tests for code changes, or documented validation_results for docs/no-op/discovery/non-runnable workflows" on_fail: "Add or run an appropriate verification and record a passing result before proceeding" + - id: B_CHANGE_IMPACT_PREBUILD + check: "Behavior changes record applicability. Shared/unresolved/expanded work passes common pre-build with capture/expected. 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, load references/change-impact.md, resolve the active installed checker, and repair the assessment or report the explicit blocker." + - id: B3A check: "Unknown-cause bugfixes completed assistant-debugging or recorded a concrete debugging blocker before implementation; no random patching occurred before reproduction/root-cause evidence" condition: "task_type == bugfix and debugging_mode in [root_cause_unknown, completed, blocked]" @@ -812,6 +817,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 evidence. Expanded artifacts preserve identity through discovery, pre-build, Build, and review; completion needs common 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..bb49fc58 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,40 @@ "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"] + } } ], "canonical_feature_preparation_result_expectations": { diff --git a/skills/assistant-workflow/references/change-impact.md b/skills/assistant-workflow/references/change-impact.md new file mode 100644 index 00000000..8b782efb --- /dev/null +++ b/skills/assistant-workflow/references/change-impact.md @@ -0,0 +1,28 @@ +# Change-impact gates + +Use this reference only when a behavior-bearing change, or a claim that work is +local/shared/unresolved, affects verification scope. Cosmetic work records its +concise applicability reason and does not manufacture 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 an impact scope, concise applicability reason, and +causal evidence for an evidenced local decision. 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 behavior +edits, that expanded path must cover relevant configuration, registration, +event, state, public, wrapper, added, and removed consumers and transitions. + +Completion refreshes the bounded universe and independent expected context, +then binds actual verification identity and assistant-review's projection of its +original `scope_manifest`, `coverage_ledger`, review snapshot, and concern +bindings. The projection adds no ledger and never replaces assistant-review's +own current terminal coverage and CLEAN/ISSUES_FIXED checks. Shared fanout alone +does not require an Architecture Decision Pack, strict controller, QA, or a +different delegation lane; use their existing evidence-based triggers. diff --git a/skills/assistant-workflow/references/phases.md b/skills/assistant-workflow/references/phases.md index 4bac8465..c323cc38 100644 --- a/skills/assistant-workflow/references/phases.md +++ b/skills/assistant-workflow/references/phases.md @@ -10,6 +10,14 @@ 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 change 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; 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 delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit diff --git a/skills/assistant-workflow/references/phases/build.md b/skills/assistant-workflow/references/phases/build.md index a9a69ad7..b1773ee8 100644 --- a/skills/assistant-workflow/references/phases/build.md +++ b/skills/assistant-workflow/references/phases/build.md @@ -12,6 +12,14 @@ 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 change 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; 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 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..38a7e71f 100644 --- a/skills/assistant-workflow/references/phases/decompose.md +++ b/skills/assistant-workflow/references/phases/decompose.md @@ -12,6 +12,14 @@ 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 change 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; 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 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..e6cac37f 100644 --- a/skills/assistant-workflow/references/phases/design.md +++ b/skills/assistant-workflow/references/phases/design.md @@ -12,6 +12,14 @@ 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 change 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; 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 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..85398994 100644 --- a/skills/assistant-workflow/references/phases/discover.md +++ b/skills/assistant-workflow/references/phases/discover.md @@ -12,6 +12,14 @@ 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 change 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; 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 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..9b753867 100644 --- a/skills/assistant-workflow/references/phases/document.md +++ b/skills/assistant-workflow/references/phases/document.md @@ -12,6 +12,14 @@ 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 change 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; 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 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..ab1704c5 100644 --- a/skills/assistant-workflow/references/phases/plan.md +++ b/skills/assistant-workflow/references/phases/plan.md @@ -12,6 +12,14 @@ 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 change 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; 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 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..b6821ec2 100644 --- a/skills/assistant-workflow/references/phases/preparation-completion.md +++ b/skills/assistant-workflow/references/phases/preparation-completion.md @@ -12,6 +12,14 @@ 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 change 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; 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 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..7caa2eae 100644 --- a/skills/assistant-workflow/references/phases/review.md +++ b/skills/assistant-workflow/references/phases/review.md @@ -12,6 +12,14 @@ 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 change 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; 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 delegation lane. + ## Progress Updates Phase markers are required only for `controller_intensity=strict`, explicit diff --git a/tests/change-impact/change-impact.test.cjs b/tests/change-impact/change-impact.test.cjs new file mode 100644 index 00000000..c7c101ff --- /dev/null +++ b/tests/change-impact/change-impact.test.cjs @@ -0,0 +1,266 @@ +"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 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_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) { + 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)); + 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")); + const child = spawnSync(process.execPath, args, { encoding: "utf8" }); + return { ...child, result: JSON.parse(child.stdout) }; +} + +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("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 = []; + assert.equal(validate({ phase: "discovery", capture: cosmetic.capture, expected: cosmetic.expected }).valid, 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("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, blocked, unresolved, and planned-as-actual completion states remain incomplete", () => { + 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")); + 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_source_identity: "anything", evidence_ref: "anything" }); + assert.ok(codes(validate({ phase: "completion", ...actual })).includes("ACTUAL_VERIFICATION_ORPHANED")); +}); + +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..d5484ed2 --- /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_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..b8c03290 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" && @@ -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") && @@ -1093,8 +1093,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 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" @@ -1203,7 +1203,7 @@ 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" } + 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" } @@ -1216,7 +1216,7 @@ if ruby -ryaml -e ' 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") && 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") && @@ -1321,7 +1321,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") && @@ -1526,7 +1526,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 @@ -1704,7 +1704,7 @@ 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" } && + 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") && 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 && diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh new file mode 100644 index 00000000..8b4788de --- /dev/null +++ b/tests/p0-p4/change-impact-contracts.sh @@ -0,0 +1,154 @@ +#!/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]}" + +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 "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 "local debugging output and discovery handoffs require only applicable change-impact fields" +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" => []} +abort "local output fabricated expanded refs" if local_output.keys.any? { |key| key.end_with?("_ref") || key == "change_impact_evidence" } + +%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") + 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]" +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" } +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 +carrier_fields = [] +%w[assistant-workflow assistant-debugging assistant-review].each do |skill| + source = load.call(File.join(framework, "skills", skill, "contracts/handoffs.yaml")) + collect = lambda do |value| + case value + when Hash + carrier_fields << value if value["name"] == "change_impact_evidence" + value.each_value { |child| collect.call(child) } + when Array + value.each { |child| collect.call(child) } + end + end + collect.call(source.fetch("handoffs")) +end +abort "missing phase-aware impact carriers" if carrier_fields.length < 5 +carrier_fields.each do |carrier| + 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") + abort "carrier lacks discovery phase" unless phase && phase["enum_values"] == ["discovery", "pre_build", "completion"] + abort "carrier requires assessment during discovery" unless assessment && assessment["condition"].include?("phase in [pre_build, completion]") + abort "carrier rejects a carried local impact artifact" unless scope && scope["enum_values"] == canonical_scopes +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, discovery, or Architect/CodeWriter/BuilderTester change-impact field applicability regressed" +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..8e1a4707 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: 5642, bytes: 45753} 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/installer-contracts.sh b/tests/p0-p4/installer-contracts.sh index 4f6500a7..26c7ad77 100644 --- a/tests/p0-p4/installer-contracts.sh +++ b/tests/p0-p4/installer-contracts.sh @@ -591,4 +591,62 @@ 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 ! 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-response-fixtures.sh b/tests/p0-p4/lib/feature-preparation-response-fixtures.sh index e7cc5153..42da71fa 100644 --- a/tests/p0-p4/lib/feature-preparation-response-fixtures.sh +++ b/tests/p0-p4/lib/feature-preparation-response-fixtures.sh @@ -1397,15 +1397,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 +1419,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 +1435,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 +1453,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 +1479,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/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..806535eb 100644 --- a/tests/p0-p4/workflow-architecture-completion-contracts.sh +++ b/tests/p0-p4/workflow-architecture-completion-contracts.sh @@ -309,7 +309,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 +672,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 @@ -1597,7 +1598,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 +2016,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/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..a14c0d24 100644 --- a/tests/windows/installer-contracts.ps1 +++ b/tests/windows/installer-contracts.ps1 @@ -2135,6 +2135,56 @@ 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' + $codexHome = Join-Path $root 'Codex Change Impact Home [isolated]' + $fixtureRoot = Join-Path $root 'Change Impact Fixture [isolated]' + [Environment]::SetEnvironmentVariable('CODEX_HOME', $codexHome, 'Process') + foreach ($skill in @('assistant-debugging', 'assistant-review')) { + $result = Invoke-Installer -Arguments @('-Agent', 'codex', '-Skill', $skill) + Assert-Equal 0 $result.ExitCode "Selective $skill installation failed: $($result.Output)" + $reference = Join-Path $isolatedUserProfile (".agents\skills\$skill\references\change-impact.md") + $installedSkill = Join-Path $isolatedUserProfile (".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" + 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 $isolatedUserProfile '.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..8f016135 --- /dev/null +++ b/tools/change-impact/README.md @@ -0,0 +1,86 @@ +# 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 +``` + +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 +and 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. + +Discovery can record material unknown boundaries. Pre-build and completion +reject them until their impact is resolved. A waiver records residual risk only +when an existing policy or authorization is cited by its `rationale_ref`; 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. A shared assessment or material unknown boundary requires at least + one recorded discovery root; compact cosmetic/local controls may have none. 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; + `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. Equivalent obligations may reuse a plan + only through a named group with two or more same-contract members and a + justification. +* 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..58a39df3 --- /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_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..f0170223 --- /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.", "Expected context independently supplies current snapshot, required scope, edges, transitions, verification source identities, and review source mappings.", "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..636634a8 --- /dev/null +++ b/tools/change-impact/validate-change-impact.cjs @@ -0,0 +1,324 @@ +#!/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 MAX_BYTES = 256 * 1024; +const MAX_DEPTH = 16; +const MAX_ARRAY = 200; +const MAX_OBJECT_KEYS = 60; +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 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, 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; + 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 (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 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_source_identity", "evidence_ref"]) && + nonBlank(actual.verification_id) && actual.outcome === "passed" && 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 !== "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]] : [])); + for (const [verificationId, requirementId] of needed) if (!actuals.has(verificationId) || 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 }) { + const reasons = []; + if (!PHASES.has(phase)) return { valid: false, complete: false, phase: null, reasons: [issue("PHASE_INVALID")] }; + validateExpected(expected, phase, reasons); + if (reasons.length === 0) validateCapture(capture, expected, reasons); + if (phase !== "discovery" && reasons.length === 0) validateAssessment(assessment, capture, expected, phase, reasons); + if (phase === "completion" && reasons.length === 0) validateReview(review, assessment, expected, reasons); + return { valid: reasons.length === 0, complete: phase === "completion" && reasons.length === 0, phase, reasons }; +} + +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"].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 (Number(process.versions.node.split(".")[0]) < 22) 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, + }); + 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({ valid: false, complete: false, phase: null, reasons: [issue(code)] })}\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 }, { From 261d54c695f95bb246003913dfd2f789e221424b Mon Sep 17 00:00:00 2001 From: Laimis Date: Sun, 13 Sep 2026 00:26:56 +0300 Subject: [PATCH 02/14] Fix review migration and shared-impact validation gaps --- .../assistant-review/contracts/handoffs.yaml | 17 +-- skills/assistant-review/contracts/index.yaml | 2 +- skills/assistant-review/contracts/input.yaml | 42 ++++++- skills/assistant-review/contracts/output.yaml | 2 +- .../contracts/phase-gates.yaml | 5 + .../references/review-batch.md | 2 +- .../references/review-loop.md | 5 +- tests/change-impact/change-impact.test.cjs | 61 +++++++--- .../independent-fixture-adapter.test.cjs | 2 +- .../assistant-review-reference-contracts.sh | 113 ++++++++++++++++-- tests/p0-p4/change-impact-contracts.sh | 54 +++++++++ .../p0-p4/context-budget-report-contracts.sh | 2 +- tools/change-impact/README.md | 2 +- tools/change-impact/protocol.v1.json | 2 +- .../change-impact/validate-change-impact.cjs | 8 +- 15 files changed, 271 insertions(+), 48 deletions(-) diff --git a/skills/assistant-review/contracts/handoffs.yaml b/skills/assistant-review/contracts/handoffs.yaml index d92577ba..fe83892e 100644 --- a/skills/assistant-review/contracts/handoffs.yaml +++ b/skills/assistant-review/contracts/handoffs.yaml @@ -7,10 +7,13 @@ schema_version: "7.2" contract: handoffs skill: assistant-review -# v7.2 migration: persisted 6.0 and incompatible 7.0 packets are invalidated and +# 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 normalizing UTF-8 NFC(description) as UTF-8 bytes, -# SHA-256 hashing it, and using legacy-v6::; +# 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.2; 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." @@ -589,17 +592,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 8788c6b7..8eaaa9f1 100644 --- a/skills/assistant-review/contracts/index.yaml +++ b/skills/assistant-review/contracts/index.yaml @@ -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: diff --git a/skills/assistant-review/contracts/input.yaml b/skills/assistant-review/contracts/input.yaml index 70867da8..e7e85c00 100644 --- a/skills/assistant-review/contracts/input.yaml +++ b/skills/assistant-review/contracts/input.yaml @@ -591,7 +591,7 @@ fields: required: conditional condition: "persisted review packet was created under v6" description: "v6 to producer 7.2 migration." - validation: "Invalidate v6 and rebuild from a freshly rehashed 7.2 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." + 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 @@ -629,7 +629,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.2. Rebuild required review/QA from a fresh 7.2 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 @@ -663,6 +663,44 @@ fields: 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 + 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" + # 1. Resolve review_scope, review_material_snapshot, change_kind, semantic_contract_review_required, behavioral_contract_review_required, agentic_loop_safety_review_required, and conditional architecture_decision_pack_review_required before entering the loop # 2. If review_material_snapshot.material is empty after resolution, inform user "nothing to review" and exit # 3. If qa_evaluation_mode=required: require acceptance_criteria, verification_evidence, qa_filter_policy, and Done Contract with debate_record when the task has one before QAEvaluator dispatch diff --git a/skills/assistant-review/contracts/output.yaml b/skills/assistant-review/contracts/output.yaml index 7dcc471a..8fd83c1c 100644 --- a/skills/assistant-review/contracts/output.yaml +++ b/skills/assistant-review/contracts/output.yaml @@ -937,7 +937,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.2 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 1eb00ff6..74c1ec05 100644 --- a/skills/assistant-review/contracts/phase-gates.yaml +++ b/skills/assistant-review/contracts/phase-gates.yaml @@ -67,6 +67,11 @@ gates: 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 # ───────────────────────────────────────────── diff --git a/skills/assistant-review/references/review-batch.md b/skills/assistant-review/references/review-batch.md index 257d6ca1..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.2 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 8a08c69c..844d9b38 100644 --- a/skills/assistant-review/references/review-loop.md +++ b/skills/assistant-review/references/review-loop.md @@ -4,10 +4,11 @@ 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?" @@ -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.2 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. diff --git a/tests/change-impact/change-impact.test.cjs b/tests/change-impact/change-impact.test.cjs index c7c101ff..9fdf96a6 100644 --- a/tests/change-impact/change-impact.test.cjs +++ b/tests/change-impact/change-impact.test.cjs @@ -127,14 +127,37 @@ test("shared and material captures record a discovery root, while compact cosmet assert.equal(validate({ phase: "discovery", capture: cosmetic.capture, expected: cosmetic.expected }).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("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); + 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.equal(validate({ phase: "pre_build", ...documents }).valid, true); assert.ok(codes(validate({ phase: "completion", ...documents })).includes("EXPECTED_REVIEW_CONTEXT_INVALID")); }); @@ -151,6 +174,16 @@ test("invalid phase diagnostics retain PHASE_INVALID without echoing untrusted p assert.equal(child.stdout.includes(sentinel), false); }); +test("canonical pre_build passes through the API and CLI with the same result", () => { + 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("known omitted consumer edge and omitted dirty-state transition fail against independent authority", () => { const documents = validDocuments(); documents.capture.edges.pop(); @@ -158,7 +191,7 @@ test("known omitted consumer edge and omitted dirty-state transition fail agains 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")); + 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", () => { @@ -168,12 +201,12 @@ test("base deletion, candidate addition, all supported dependency kinds, and equ 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")); + 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")); + 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"; @@ -184,13 +217,13 @@ test("unsupported local claim, stale universe, stale verification source, and wr test("waived, blocked, unresolved, and planned-as-actual completion states remain incomplete", () => { 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.equal(validate({ phase: "pre_build", ...waived }).valid, true); assert.ok(codes(validate({ phase: "completion", ...waived })).includes("UNVERIFIED_RESIDUAL_RISK")); 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: "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: "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")); @@ -205,11 +238,11 @@ test("capture requirements, reciprocal equivalence, and exact plan and actual co 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")); + 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")); + 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")); + 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_source_identity: "anything", evidence_ref: "anything" }); assert.ok(codes(validate({ phase: "completion", ...actual })).includes("ACTUAL_VERIFICATION_ORPHANED")); }); @@ -230,9 +263,9 @@ test("scalar and null array members never throw and return structural reasons", 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")); + 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")); + assert.ok(codes(validate({ phase: "pre_build", ...dangling })).includes("VERIFICATION_PLAN_DANGLING")); }); test("cosmetic local control remains proportionate", () => { @@ -250,7 +283,7 @@ test("CLI rejects malformed, oversized, nested, and command-looking input withou 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"); + 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)); diff --git a/tests/change-impact/independent-fixture-adapter.test.cjs b/tests/change-impact/independent-fixture-adapter.test.cjs index d5484ed2..2f8bc5d3 100644 --- a/tests/change-impact/independent-fixture-adapter.test.cjs +++ b/tests/change-impact/independent-fixture-adapter.test.cjs @@ -172,7 +172,7 @@ test("independent fixture authority adapts known consumers and transitions witho 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`); + assert.ok(codes(validate({ phase: "pre_build", ...missingTransition })).includes("CAPTURE_REQUIREMENT_OBLIGATION_MISSING"), `${value.id} transition omission`); } }); diff --git a/tests/p0-p4/assistant-review-reference-contracts.sh b/tests/p0-p4/assistant-review-reference-contracts.sh index b8c03290..b79ac7ba 100644 --- a/tests/p0-p4/assistant-review-reference-contracts.sh +++ b/tests/p0-p4/assistant-review-reference-contracts.sh @@ -1093,7 +1093,7 @@ 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.2 from a fresh snapshot' "$FRAMEWORK_DIR/skills/assistant-review/references/review-batch.md" \ +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 @@ -1176,9 +1176,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 +1203,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"}} + 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.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" @@ -1339,7 +1348,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") @@ -1653,13 +1662,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 +1705,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? @@ -1705,11 +1787,16 @@ if ruby -ryaml -rjson -e ' actual.uniq.length == actual.length && actual.sort == expected.sort end 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?("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 && @@ -1736,7 +1823,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 index 8b4788de..f9d6f4ec 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -42,6 +42,60 @@ 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 + +carrier_count = 0 +%w[assistant-workflow assistant-debugging assistant-review].each do |skill| + handoffs = YAML.load_file(File.join(framework, "skills", skill, "contracts/handoffs.yaml")) + walk = lambda do |value| + case value + when Hash + if value["name"] == "change_impact_evidence" + phase = field.call(value.fetch("object_fields"), "phase") + abort "#{skill} handoff phase enum drifted" unless phase.fetch("enum_values") == %w[discovery pre_build completion] + carrier_count += 1 + end + value.each_value { |child| walk.call(child) } + when Array + value.each { |child| walk.call(child) } + end + end + walk.call(handoffs.fetch("handoffs")) +end +abort "missing concrete phase-aware handoff carriers" unless carrier_count >= 5 + +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"])') +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 \ diff --git a/tests/p0-p4/context-budget-report-contracts.sh b/tests/p0-p4/context-budget-report-contracts.sh index 8e1a4707..94d1986a 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: 5642, bytes: 45753} + and .selected_load_set_context.declared_boundary_closure == {words: 5643, bytes: 45789} 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/tools/change-impact/README.md b/tools/change-impact/README.md index 8f016135..73cb7bec 100644 --- a/tools/change-impact/README.md +++ b/tools/change-impact/README.md @@ -22,7 +22,7 @@ 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 \ +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 \ diff --git a/tools/change-impact/protocol.v1.json b/tools/change-impact/protocol.v1.json index f0170223..60afedf8 100644 --- a/tools/change-impact/protocol.v1.json +++ b/tools/change-impact/protocol.v1.json @@ -4,7 +4,7 @@ "runtime": "Node.js 22 or newer, standard library only", "phases": { "discovery": ["capture", "expected context"], - "pre-build": ["capture", "expected context", "assessment with planned verification"], + "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.", diff --git a/tools/change-impact/validate-change-impact.cjs b/tools/change-impact/validate-change-impact.cjs index 636634a8..2d40e8bd 100644 --- a/tools/change-impact/validate-change-impact.cjs +++ b/tools/change-impact/validate-change-impact.cjs @@ -11,7 +11,7 @@ const MAX_BYTES = 256 * 1024; const MAX_DEPTH = 16; const MAX_ARRAY = 200; const MAX_OBJECT_KEYS = 60; -const PHASES = new Set(["discovery", "pre-build", "completion"]); +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"]); @@ -123,6 +123,8 @@ function validateCapture(capture, expected, reasons) { if (reasons.length) return; 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); @@ -240,9 +242,9 @@ function validateAssessment(assessment, capture, expected, phase, reasons) { exactKeys(actual, ["verification_id", "outcome", "executed_source_identity", "evidence_ref"]) && nonBlank(actual.verification_id) && actual.outcome === "passed" && 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.actual_verifications.length > 0) reasons.push(issue("PREBUILD_EXECUTION_NOT_ALLOWED")); 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 === "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"); From 467d1008ec3d39d894e3a2bb8ef873a923fd399f Mon Sep 17 00:00:00 2001 From: Laimis Date: Sun, 13 Sep 2026 07:22:51 +0300 Subject: [PATCH 03/14] Keep review context within the first-pass budget --- skills/assistant-review/SKILL.md | 182 +++++++++++------- tests/p0-p4/change-impact-contracts.sh | 18 ++ .../p0-p4/context-budget-report-contracts.sh | 2 +- 3 files changed, 128 insertions(+), 74 deletions(-) diff --git a/skills/assistant-review/SKILL.md b/skills/assistant-review/SKILL.md index d6764995..189521a5 100644 --- a/skills/assistant-review/SKILL.md +++ b/skills/assistant-review/SKILL.md @@ -7,76 +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`. -- `change_impact`: load its reference only when triggered; keep it out of reviewer bundles. -- `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 resolve canonical fields. Entry loads no review guidance. +- 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. Persisted v6 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. Run the bounded review policy, keep interim +results inside its loop, and present one evidence-calibrated result. Required QA +follows build/test and code-review evidence. ## 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. +- A frozen-snapshot batch has at least two COMPLETED passes, independently + performed before aggregation, fix, or exit. Audit returns terminal coverage after one started batch, complete or + `HAS_REMAINING_ITEMS` when incomplete, with non-empty `reviewed_scope`. +- 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`. @@ -85,49 +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 -For medium+ reviews, run the **Design Coherence Pass** and return evidence-bound -`principle_checks.design_coherence`; findings name surface, risk, and smallest fix. +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. - -Impact projection preserves canonical review closure; it never makes audit or -review-fix results clean by itself. - -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 @@ -135,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/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index f9d6f4ec..3bfeb82e 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -205,4 +205,22 @@ else fail "local output, discovery, or Architect/CodeWriter/BuilderTester change-impact field applicability regressed" 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 94d1986a..589ad7aa 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: 5643, bytes: 45789} + and .selected_load_set_context.declared_boundary_closure == {words: 5232, bytes: 42787} 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 == From f3c405799069129b5b1e7be55271114c564c7e16 Mon Sep 17 00:00:00 2001 From: Laimis Date: Sun, 13 Sep 2026 09:22:25 +0300 Subject: [PATCH 04/14] Require current pre-build evidence before mutation --- docs/skill-contract-design-guide.md | 15 +- .../contracts/handoffs.yaml | 7 +- .../contracts/phase-gates.yaml | 7 +- .../references/change-impact.md | 11 +- .../contracts/phase-gates.yaml | 11 +- .../references/review-loop.md | 4 +- .../references/skill-contract-design-guide.md | 15 +- .../contracts/handoffs.yaml | 16 +- .../contracts/phase-gates.yaml | 11 +- .../references/build-worker-protocol.md | 14 ++ .../references/change-impact.md | 19 ++- tests/change-impact/change-impact.test.cjs | 7 +- tests/p0-p4/change-impact-contracts.sh | 150 +++++++++++++----- tools/change-impact/README.md | 5 +- 14 files changed, 198 insertions(+), 94 deletions(-) diff --git a/docs/skill-contract-design-guide.md b/docs/skill-contract-design-guide.md index 0e97b8f5..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,7 +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** — phase-aware evidence preserves - identity. Discovery may report gaps; completion needs verification and - review. Loaded-path tools missing runtime block. Fanout alone +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-debugging/contracts/handoffs.yaml b/skills/assistant-debugging/contracts/handoffs.yaml index c47f399d..5362480e 100644 --- a/skills/assistant-debugging/contracts/handoffs.yaml +++ b/skills/assistant-debugging/contracts/handoffs.yaml @@ -58,15 +58,16 @@ handoffs: type: object required: conditional condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" - validation: "artifact_identity, phase, impact_scope, and applicable capture/expected/assessment/checker refs are passed unchanged; discovery carries no assessment ref; a symptom-only consumer list is not impact evidence." + validation: "The common checker was run or revalidated against the referenced capture_ref, independent expected_context_ref, and assessment_ref for a current valid pre_build result that resolves artifact_identity and validator_result_ref before Fixer dispatch; impact_scope and status remain bound unchanged. A status or result ref alone is insufficient, reuse requires demonstrably current same-input evidence, 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: [discovery, pre_build, completion]} + - {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: conditional, condition: "phase in [pre_build, completion]"} + - {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/phase-gates.yaml b/skills/assistant-debugging/contracts/phase-gates.yaml index 2644aff1..28672d85 100644 --- a/skills/assistant-debugging/contracts/phase-gates.yaml +++ b/skills/assistant-debugging/contracts/phase-gates.yaml @@ -63,11 +63,12 @@ gates: checkpoint_start: "--- PHASE: FIX ---" checkpoint_end: "--- PHASE: FIX COMPLETE ---" condition: "when can_modify_code is true and a fix or mitigation is attempted" - exit_assertions: + entry_assertions: - id: FX_CHANGE_IMPACT_PREBUILD - check: "Every behavior repair records compact applicability evidence. When the diagnosis/proposed repair has shared or unresolved material impact, or explicitly carries an expanded artifact, a current common-checker pre-build assessment covers the bounded base/candidate consumer union and planned obligations before source mutation. Evidenced local repairs keep ordinary regression verification and self-review." + check: "Before any source/test mutation or mutation dispatch, every behavior repair records compact applicability evidence. 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; load references/change-impact.md and repair the assessment or report its explicit blocker." + 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" on_fail: "Remove unrelated changes or document approved scope expansion" diff --git a/skills/assistant-debugging/references/change-impact.md b/skills/assistant-debugging/references/change-impact.md index a8057ec2..e9c2df7c 100644 --- a/skills/assistant-debugging/references/change-impact.md +++ b/skills/assistant-debugging/references/change-impact.md @@ -14,10 +14,13 @@ 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 a source fix, shared or -unresolved impact needs the common pre-build assessment, including bounded -base/candidate consumers and distinct material transitions. Preserve the same -artifact identity through expanded verification. An expanded fix completion +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. Reuse is allowed only for demonstrably current same-input +evidence. 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/contracts/phase-gates.yaml b/skills/assistant-review/contracts/phase-gates.yaml index 74c1ec05..1a59204b 100644 --- a/skills/assistant-review/contracts/phase-gates.yaml +++ b/skills/assistant-review/contracts/phase-gates.yaml @@ -58,9 +58,9 @@ 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" @@ -231,12 +231,13 @@ gates: condition: "mode == review-fix" checkpoint_start: null checkpoint_end: null - exit_assertions: + entry_assertions: - id: F_CHANGE_IMPACT_PREBUILD - check: "Before a review-fix edits a triggered shared or unresolved impact scope, a current common-checker pre-build assessment has valid planned obligations for the bounded consumer union and material transitions." + 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; repair the pre-build assessment or return the review-fix as blocked/remaining." + 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 check: "ALL must-fix items were addressed (not just some)" diff --git a/skills/assistant-review/references/review-loop.md b/skills/assistant-review/references/review-loop.md index 844d9b38..0620cc1d 100644 --- a/skills/assistant-review/references/review-loop.md +++ b/skills/assistant-review/references/review-loop.md @@ -12,7 +12,7 @@ PREPARE - 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: @@ -32,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 0e97b8f5..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,7 +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** — phase-aware evidence preserves - identity. Discovery may report gaps; completion needs verification and - review. Loaded-path tools missing runtime block. Fanout alone +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/contracts/handoffs.yaml b/skills/assistant-workflow/contracts/handoffs.yaml index c37e7778..2f0143a8 100644 --- a/skills/assistant-workflow/contracts/handoffs.yaml +++ b/skills/assistant-workflow/contracts/handoffs.yaml @@ -1627,14 +1627,14 @@ handoffs: type: object required: conditional condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" - description: "Same compact v1 artifact reference envelope supplied to planning, Build, and Review." - validation: "artifact_identity, phase, capture_ref, expected_context_ref, validator_result_ref, impact_scope, and status exactly equal the current task packet; assessment_ref is present only for pre-build or completion. The CodeWriter may not relabel a pre-build plan as executed completion evidence." + description: "Current resolved pre-build artifact envelope that authorizes this CodeWriter mutation dispatch." + validation: "The common checker was run or revalidated against the referenced capture_ref, independent expected_context_ref, and assessment_ref for a current valid pre_build result that resolves artifact_identity and validator_result_ref against the current task packet; impact_scope and status remain bound unchanged. A status or result ref alone is insufficient, and reuse requires demonstrably current same-input evidence. 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: [discovery, pre_build, completion]} + - {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: conditional, condition: "phase in [pre_build, completion]"} + - {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]} @@ -2279,16 +2279,16 @@ handoffs: type: object required: conditional condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" - description: "Expanded common impact artifact BuilderTester validates without relabelling discovery as later-phase evidence." - validation: "Preserves artifact_identity, phase, impact_scope, status, capture_ref, expected_context_ref, and validator_result_ref; assessment_ref is carried only for pre-build or completion." + description: "Current resolved pre-build artifact envelope that authorizes this BuilderTester mutation dispatch." + validation: "The common checker was run or revalidated against the referenced capture_ref, independent expected_context_ref, and assessment_ref for a current valid pre_build result that resolves artifact_identity and validator_result_ref against the current task packet; impact_scope and status remain bound unchanged. A status or result ref alone is insufficient, reuse requires demonstrably current same-input evidence, 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: [discovery, pre_build, completion]} + - {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: conditional, condition: "phase in [pre_build, completion]"} + - {name: assessment_ref, type: string, required: true} - {name: validator_result_ref, type: string, required: true} - name: build_command type: string diff --git a/skills/assistant-workflow/contracts/phase-gates.yaml b/skills/assistant-workflow/contracts/phase-gates.yaml index a20debbe..7618740c 100644 --- a/skills/assistant-workflow/contracts/phase-gates.yaml +++ b/skills/assistant-workflow/contracts/phase-gates.yaml @@ -455,6 +455,12 @@ gates: checkpoint_start: "--- PHASE: BUILD ---" checkpoint_end: "--- PHASE: BUILD COMPLETE ---" condition: "execution_intent != prepare_only" + entry_assertions: + + - id: B_CHANGE_IMPACT_PREBUILD + check: "Before any source/test mutation or mutation dispatch, behavior changes record applicability and 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 @@ -488,11 +494,6 @@ gates: check: "At least one relevant verification exists and passed: runnable tests for code changes, or documented validation_results for docs/no-op/discovery/non-runnable workflows" on_fail: "Add or run an appropriate verification and record a passing result before proceeding" - - id: B_CHANGE_IMPACT_PREBUILD - check: "Behavior changes record applicability. Shared/unresolved/expanded work passes common pre-build with capture/expected. 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, load references/change-impact.md, resolve the active installed checker, and repair the assessment or report the explicit blocker." - - id: B3A check: "Unknown-cause bugfixes completed assistant-debugging or recorded a concrete debugging blocker before implementation; no random patching occurred before reproduction/root-cause evidence" condition: "task_type == bugfix and debugging_mode in [root_cause_unknown, completed, blocked]" diff --git a/skills/assistant-workflow/references/build-worker-protocol.md b/skills/assistant-workflow/references/build-worker-protocol.md index 93230648..524d6bdf 100644 --- a/skills/assistant-workflow/references/build-worker-protocol.md +++ b/skills/assistant-workflow/references/build-worker-protocol.md @@ -62,6 +62,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 index 8b782efb..38228595 100644 --- a/skills/assistant-workflow/references/change-impact.md +++ b/skills/assistant-workflow/references/change-impact.md @@ -1,8 +1,7 @@ # Change-impact gates -Use this reference only when a behavior-bearing change, or a claim that work is -local/shared/unresolved, affects verification scope. Cosmetic work records its -concise applicability reason and does not manufacture an impact ledger. +Use only when behavior 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 @@ -10,14 +9,18 @@ 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 an impact scope, concise applicability reason, and -causal evidence for an evidenced local decision. Local work keeps its ordinary +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 behavior -edits, that expanded path must cover relevant configuration, registration, -event, state, public, wrapper, added, and removed consumers and transitions. +materially unresolved, or explicitly carried expanded work. Before any +source/test mutation or dispatch, run/revalidate the checker using capture, +expected context, and assessment. Current valid `pre_build` resolves to the +task packet; status/ref alone is insufficient. Reuse only with demonstrably +current same inputs. The expanded path covers relevant configuration, +registration, event, state, public, wrapper, added, and removed consumers and +transitions. Completion refreshes the bounded universe and independent expected context, then binds actual verification identity and assistant-review's projection of its diff --git a/tests/change-impact/change-impact.test.cjs b/tests/change-impact/change-impact.test.cjs index 9fdf96a6..adbe00da 100644 --- a/tests/change-impact/change-impact.test.cjs +++ b/tests/change-impact/change-impact.test.cjs @@ -215,10 +215,15 @@ test("unsupported local claim, stale universe, stale verification source, and wr assert.ok(codes(validate({ phase: "completion", ...review })).includes("REVIEW_OBLIGATION_BINDING_INCOMPLETE")); }); -test("waived, blocked, unresolved, and planned-as-actual completion states remain 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")); diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index 3bfeb82e..f2ca8540 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -64,25 +64,20 @@ end abort "#{skill} output phase enum drifted" unless phase.fetch("enum_values") == %w[discovery pre_build completion] end -carrier_count = 0 -%w[assistant-workflow assistant-debugging assistant-review].each do |skill| - handoffs = YAML.load_file(File.join(framework, "skills", skill, "contracts/handoffs.yaml")) - walk = lambda do |value| - case value - when Hash - if value["name"] == "change_impact_evidence" - phase = field.call(value.fetch("object_fields"), "phase") - abort "#{skill} handoff phase enum drifted" unless phase.fetch("enum_values") == %w[discovery pre_build completion] - carrier_count += 1 - end - value.each_value { |child| walk.call(child) } - when Array - value.each { |child| walk.call(child) } - end - end - walk.call(handoffs.fetch("handoffs")) +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 -abort "missing concrete phase-aware handoff carriers" unless carrier_count >= 5 readme = File.read(File.join(framework, "tools/change-impact/README.md")) abort "README command retains legacy phase spelling" if readme.include?("--phase pre-build") @@ -127,7 +122,7 @@ else fail "$(IFS='; '; printf '%s' "${local_proportionality_failures[*]}")" fi -test_start "local debugging output and discovery handoffs require only applicable change-impact fields" +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) } @@ -162,28 +157,55 @@ abort "Architect does not receive expanded impact evidence" unless architect_imp 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 -carrier_fields = [] -%w[assistant-workflow assistant-debugging assistant-review].each do |skill| - source = load.call(File.join(framework, "skills", skill, "contracts/handoffs.yaml")) - collect = lambda do |value| - case value - when Hash - carrier_fields << value if value["name"] == "change_impact_evidence" - value.each_value { |child| collect.call(child) } - when Array - value.each { |child| collect.call(child) } - end - end - collect.call(source.fetch("handoffs")) -end -abort "missing phase-aware impact carriers" if carrier_fields.length < 5 -carrier_fields.each do |carrier| +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_impact, reviewer_impact].each do |carrier| 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") - abort "carrier lacks discovery phase" unless phase && phase["enum_values"] == ["discovery", "pre_build", "completion"] - abort "carrier requires assessment during discovery" unless assessment && assessment["condition"].include?("phase in [pre_build, completion]") - abort "carrier rejects a carried local impact artifact" unless scope && scope["enum_values"] == canonical_scopes + abort "read-only carrier loses discovery/completion evidence" unless phase && phase["enum_values"] == ["discovery", "pre_build", "completion"] + abort "read-only carrier requires discovery assessment" unless assessment && assessment["condition"].include?("phase in [pre_build, completion]") + abort "read-only carrier rejects a carried local impact artifact" unless scope && scope["enum_values"] == canonical_scopes +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"] +} +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") + abort "#{role} accepts discovery/completion mutation authorization" unless phase && phase["enum_values"] == ["pre_build"] + 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?("reuse requires demonstrably current same-input evidence") + 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| @@ -202,7 +224,57 @@ RUBY then pass else - fail "local output, discovery, or Architect/CodeWriter/BuilderTester change-impact field applicability regressed" + 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" +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 "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" diff --git a/tools/change-impact/README.md b/tools/change-impact/README.md index 73cb7bec..b15f40d3 100644 --- a/tools/change-impact/README.md +++ b/tools/change-impact/README.md @@ -37,8 +37,9 @@ not replace it with a manual pass. Discovery can record material unknown boundaries. Pre-build and completion reject them until their impact is resolved. A waiver records residual risk only -when an existing policy or authorization is cited by its `rationale_ref`; it -does not create authorization and cannot pass completion. +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 From ef21f675d0aa7ad6ebda2a5e95d107115893c69c Mon Sep 17 00:00:00 2001 From: Laimis Date: Mon, 14 Sep 2026 02:43:41 +0300 Subject: [PATCH 05/14] fix: align expanded impact review and applicability contracts --- .../variants/workflow-kernel-v1/manifest.json | 10 +- skills/assistant-debugging/SKILL.md | 2 +- .../assistant-debugging/contracts/input.yaml | 6 +- .../references/change-impact.md | 2 +- skills/assistant-review/SKILL.md | 2 +- skills/assistant-review/contracts/input.yaml | 6 +- .../references/change-impact.md | 2 +- skills/assistant-workflow/SKILL.md | 4 +- .../assistant-workflow/contracts/input.yaml | 6 +- .../assistant-workflow/contracts/output.yaml | 42 ++-- .../contracts/phase-gates.yaml | 4 +- .../references/build-worker-protocol.md | 5 +- .../references/change-impact.md | 15 +- .../assistant-workflow/references/phases.md | 6 +- .../references/phases/build.md | 4 +- .../references/phases/decompose.md | 4 +- .../references/phases/design.md | 4 +- .../references/phases/discover.md | 4 +- .../references/phases/document.md | 4 +- .../references/phases/plan.md | 4 +- .../phases/preparation-completion.md | 4 +- .../references/phases/review.md | 6 +- .../references/review-qa-router.md | 21 +- .../references/subagent-dispatch.md | 6 +- .../references/triage-rubric.md | 2 +- .../references/workflow-controller.md | 5 +- .../assistant-review-reference-contracts.sh | 4 +- tests/p0-p4/change-impact-contracts.sh | 184 ++++++++++++++++++ .../p0-p4/context-budget-report-contracts.sh | 2 +- .../skill-instruction-quality-contracts.sh | 4 +- ...kflow-architecture-completion-contracts.sh | 67 ++++--- tests/p0-p4/workflow-basics-contracts.sh | 2 +- 32 files changed, 322 insertions(+), 121 deletions(-) diff --git a/docs/evals/variants/workflow-kernel-v1/manifest.json b/docs/evals/variants/workflow-kernel-v1/manifest.json index 4e959c58..3b390e54 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": 2320, + "baseline_selected_initial_words": 2313, "candidate_selected_initial_words": 486, - "selected_initial_word_delta": -1834, - "baseline_total_initial_words": 2909, + "selected_initial_word_delta": -1827, + "baseline_total_initial_words": 2902, "candidate_total_initial_words": 1075, "baseline_selected_entry_words": 3925, - "candidate_selected_entry_words": 2091, - "selected_entry_word_delta": -1834, + "candidate_selected_entry_words": 2098, + "selected_entry_word_delta": -1827, "standing_context_growth": 0, "measured_with": "tools/context-budget-report.sh --skill-overlay" }, diff --git a/skills/assistant-debugging/SKILL.md b/skills/assistant-debugging/SKILL.md index 2191eb65..740b449d 100644 --- a/skills/assistant-debugging/SKILL.md +++ b/skills/assistant-debugging/SKILL.md @@ -39,7 +39,7 @@ 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, or locality claim affects consumer +- `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. diff --git a/skills/assistant-debugging/contracts/input.yaml b/skills/assistant-debugging/contracts/input.yaml index dede4fc6..9ff2a5bd 100644 --- a/skills/assistant-debugging/contracts/input.yaml +++ b/skills/assistant-debugging/contracts/input.yaml @@ -65,11 +65,11 @@ fields: - name: change_impact_applicability type: object required: conditional - condition: "a diagnosis or proposed repair has a behavior-bearing or locality/shared/unresolved impact decision" + 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. local requires causal_evidence_ref and ordinary regression verification/self-review; shared or unresolved requires expanded_artifact_carried=true." + 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: [local, shared, unresolved]} + - {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} diff --git a/skills/assistant-debugging/references/change-impact.md b/skills/assistant-debugging/references/change-impact.md index e9c2df7c..eaf09c6f 100644 --- a/skills/assistant-debugging/references/change-impact.md +++ b/skills/assistant-debugging/references/change-impact.md @@ -1,6 +1,6 @@ # Change-impact during debugging -Load when the reported symptom, diagnosis, or proposed fix can affect another +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. diff --git a/skills/assistant-review/SKILL.md b/skills/assistant-review/SKILL.md index 189521a5..0fc21749 100644 --- a/skills/assistant-review/SKILL.md +++ b/skills/assistant-review/SKILL.md @@ -14,7 +14,7 @@ Canonical schemas are authoritative at enforcement points. Read - `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. -Migration note: assistant-review contracts are v7. Persisted v6 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`. +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. diff --git a/skills/assistant-review/contracts/input.yaml b/skills/assistant-review/contracts/input.yaml index e7e85c00..fce8ec23 100644 --- a/skills/assistant-review/contracts/input.yaml +++ b/skills/assistant-review/contracts/input.yaml @@ -20,11 +20,11 @@ fields: - name: change_impact_applicability type: object required: conditional - condition: "the review covers a behavior-bearing change or locality/shared/unresolved impact decision" + 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. local requires causal_evidence_ref and ordinary review/verification; shared or unresolved requires expanded_artifact_carried=true." + 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: [local, shared, unresolved]} + - {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} diff --git a/skills/assistant-review/references/change-impact.md b/skills/assistant-review/references/change-impact.md index 33530542..2ff580cd 100644 --- a/skills/assistant-review/references/change-impact.md +++ b/skills/assistant-review/references/change-impact.md @@ -1,6 +1,6 @@ # Change-impact review projection -Load for a review that covers a behavior change, carries a change-impact +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 diff --git a/skills/assistant-workflow/SKILL.md b/skills/assistant-workflow/SKILL.md index 8049849b..d1934688 100644 --- a/skills/assistant-workflow/SKILL.md +++ b/skills/assistant-workflow/SKILL.md @@ -51,7 +51,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 changes or a local/shared/unresolved impact decision affects verification scope. Every behavior change records applicability; only shared, materially unresolved, or explicitly carried expanded work requires the common pre-Build/completion path. +- `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. @@ -145,7 +145,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/input.yaml b/skills/assistant-workflow/contracts/input.yaml index 35a14e0f..e8089562 100644 --- a/skills/assistant-workflow/contracts/input.yaml +++ b/skills/assistant-workflow/contracts/input.yaml @@ -124,11 +124,11 @@ fields: - name: change_impact_applicability type: object required: conditional - condition: "a behavior or locality/shared/unresolved impact decision affects verification" + condition: "a behavior, cosmetic, or locality/shared/unresolved impact decision affects verification" description: "Applicability decision." - validation: "local needs causal evidence; shared/unresolved needs expanded artifact." + validation: "not_applicable is cosmetic; local needs causal evidence; shared/unresolved or expanded_artifact_carried needs artifact." object_fields: - - {name: impact_scope, type: enum, required: true, enum_values: [local, shared, unresolved]} + - {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} diff --git a/skills/assistant-workflow/contracts/output.yaml b/skills/assistant-workflow/contracts/output.yaml index f0d020b2..1d42db40 100644 --- a/skills/assistant-workflow/contracts/output.yaml +++ b/skills/assistant-workflow/contracts/output.yaml @@ -8,8 +8,8 @@ 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_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_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_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,13 +38,13 @@ 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_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_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" @@ -411,8 +411,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 @@ -431,34 +431,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: @@ -478,13 +478,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 @@ -500,7 +500,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 diff --git a/skills/assistant-workflow/contracts/phase-gates.yaml b/skills/assistant-workflow/contracts/phase-gates.yaml index 7618740c..1f3cd70c 100644 --- a/skills/assistant-workflow/contracts/phase-gates.yaml +++ b/skills/assistant-workflow/contracts/phase-gates.yaml @@ -600,9 +600,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." diff --git a/skills/assistant-workflow/references/build-worker-protocol.md b/skills/assistant-workflow/references/build-worker-protocol.md index 524d6bdf..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. diff --git a/skills/assistant-workflow/references/change-impact.md b/skills/assistant-workflow/references/change-impact.md index 38228595..83d92afd 100644 --- a/skills/assistant-workflow/references/change-impact.md +++ b/skills/assistant-workflow/references/change-impact.md @@ -1,6 +1,6 @@ # Change-impact gates -Use only when behavior or a locality claim affects verification scope. Cosmetic +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 @@ -22,10 +22,9 @@ current same inputs. The expanded path covers relevant configuration, registration, event, state, public, wrapper, added, and removed consumers and transitions. -Completion refreshes the bounded universe and independent expected context, -then binds actual verification identity and assistant-review's projection of its -original `scope_manifest`, `coverage_ledger`, review snapshot, and concern -bindings. The projection adds no ledger and never replaces assistant-review's -own current terminal coverage and CLEAN/ISSUES_FIXED checks. Shared fanout alone -does not require an Architecture Decision Pack, strict controller, QA, or a -different delegation lane; use their existing evidence-based triggers. +Completion refreshes universe/context then binds verification identity and +assistant-review's projection of original `scope_manifest`, `coverage_ledger`, +review snapshot, and concern bindings. It 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 c323cc38..f6fe753b 100644 --- a/skills/assistant-workflow/references/phases.md +++ b/skills/assistant-workflow/references/phases.md @@ -10,13 +10,13 @@ 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 change or locality/shared-impact claim, load +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; 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 delegation lane. +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. ## Progress Updates @@ -396,7 +396,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 b1773ee8..a9119ffc 100644 --- a/skills/assistant-workflow/references/phases/build.md +++ b/skills/assistant-workflow/references/phases/build.md @@ -12,13 +12,13 @@ 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 change or locality/shared-impact claim, load +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; 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 delegation lane. +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. ## Progress Updates diff --git a/skills/assistant-workflow/references/phases/decompose.md b/skills/assistant-workflow/references/phases/decompose.md index 38a7e71f..581e00ee 100644 --- a/skills/assistant-workflow/references/phases/decompose.md +++ b/skills/assistant-workflow/references/phases/decompose.md @@ -12,13 +12,13 @@ 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 change or locality/shared-impact claim, load +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; 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 delegation lane. +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. ## Progress Updates diff --git a/skills/assistant-workflow/references/phases/design.md b/skills/assistant-workflow/references/phases/design.md index e6cac37f..f4b2cbc9 100644 --- a/skills/assistant-workflow/references/phases/design.md +++ b/skills/assistant-workflow/references/phases/design.md @@ -12,13 +12,13 @@ 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 change or locality/shared-impact claim, load +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; 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 delegation lane. +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. ## Progress Updates diff --git a/skills/assistant-workflow/references/phases/discover.md b/skills/assistant-workflow/references/phases/discover.md index 85398994..4be4ac10 100644 --- a/skills/assistant-workflow/references/phases/discover.md +++ b/skills/assistant-workflow/references/phases/discover.md @@ -12,13 +12,13 @@ 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 change or locality/shared-impact claim, load +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; 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 delegation lane. +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. ## Progress Updates diff --git a/skills/assistant-workflow/references/phases/document.md b/skills/assistant-workflow/references/phases/document.md index 9b753867..fcb337a4 100644 --- a/skills/assistant-workflow/references/phases/document.md +++ b/skills/assistant-workflow/references/phases/document.md @@ -12,13 +12,13 @@ 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 change or locality/shared-impact claim, load +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; 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 delegation lane. +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. ## Progress Updates diff --git a/skills/assistant-workflow/references/phases/plan.md b/skills/assistant-workflow/references/phases/plan.md index ab1704c5..8d9dbeb7 100644 --- a/skills/assistant-workflow/references/phases/plan.md +++ b/skills/assistant-workflow/references/phases/plan.md @@ -12,13 +12,13 @@ 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 change or locality/shared-impact claim, load +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; 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 delegation lane. +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. ## Progress Updates diff --git a/skills/assistant-workflow/references/phases/preparation-completion.md b/skills/assistant-workflow/references/phases/preparation-completion.md index b6821ec2..1e7e765a 100644 --- a/skills/assistant-workflow/references/phases/preparation-completion.md +++ b/skills/assistant-workflow/references/phases/preparation-completion.md @@ -12,13 +12,13 @@ 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 change or locality/shared-impact claim, load +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; 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 delegation lane. +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. ## Progress Updates diff --git a/skills/assistant-workflow/references/phases/review.md b/skills/assistant-workflow/references/phases/review.md index 7caa2eae..bc7ce387 100644 --- a/skills/assistant-workflow/references/phases/review.md +++ b/skills/assistant-workflow/references/phases/review.md @@ -12,13 +12,13 @@ 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 change or locality/shared-impact claim, load +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; 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 delegation lane. +Architecture Decision Pack, strict controller, QA, or a different Build/delegation lane. ## Progress Updates @@ -37,7 +37,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/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/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/p0-p4/assistant-review-reference-contracts.sh b/tests/p0-p4/assistant-review-reference-contracts.sh index b79ac7ba..74e543d3 100644 --- a/tests/p0-p4/assistant-review-reference-contracts.sh +++ b/tests/p0-p4/assistant-review-reference-contracts.sh @@ -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%%::*}" @@ -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 diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index f2ca8540..2119d637 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -4,6 +4,7 @@ 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" @@ -122,6 +123,67 @@ else fail "$(IFS='; '; printf '%s' "${local_proportionality_failures[*]}")" fi +test_start "impact applicability admits cosmetic not_applicable without relaxing local or explicit-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] +%w[assistant-workflow assistant-debugging assistant-review].each do |skill| + input = YAML.load_file(File.join(framework, "skills", skill, "contracts/input.yaml")) + applicability = field.call(input.fetch("fields"), "change_impact_applicability") + scope = field.call(applicability.fetch("object_fields"), "impact_scope") + causal = field.call(applicability.fetch("object_fields"), "causal_evidence_ref") + abort "#{skill} omits canonical cosmetic scope" unless scope.fetch("enum_values") == canonical_scopes + abort "#{skill} applicability condition excludes cosmetic decisions" unless applicability.fetch("condition").include?("cosmetic") + abort "#{skill} relaxed local causal evidence" unless causal.fetch("required") == "conditional" && causal.fetch("condition") == "impact_scope == local" + abort "#{skill} does not retain explicit expanded-artifact control" unless applicability.fetch("validation").include?("expanded_artifact_carried") +end +output = YAML.load_file(File.join(framework, "skills/assistant-workflow/contracts/output.yaml")) +tiers = 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") +end +RUBY +then + pass +else + fail "cosmetic applicability, local causality, explicit expansion, or tiered evidence declaration 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) @@ -255,6 +317,128 @@ 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) diff --git a/tests/p0-p4/context-budget-report-contracts.sh b/tests/p0-p4/context-budget-report-contracts.sh index 589ad7aa..4e9d3a53 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: 5232, bytes: 42787} + and .selected_load_set_context.declared_boundary_closure == {words: 5232, bytes: 42798} 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/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/workflow-architecture-completion-contracts.sh b/tests/p0-p4/workflow-architecture-completion-contracts.sh index 806535eb..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() { @@ -753,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" \ @@ -835,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" 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" \ From b0b3f6de87863a7050c9f9f866ad48d77e82c96e Mon Sep 17 00:00:00 2001 From: Laimis Date: Tue, 15 Sep 2026 09:56:59 +0300 Subject: [PATCH 06/14] fix: bind impact verification to executed snapshot --- .../assistant-debugging/contracts/output.yaml | 2 +- .../assistant-workflow/contracts/output.yaml | 2 +- tests/change-impact/change-impact.test.cjs | 50 ++++++++++++++++++- .../independent-fixture-adapter.test.cjs | 2 +- tests/p0-p4/change-impact-contracts.sh | 26 ++++++++++ tools/change-impact/README.md | 8 ++- .../change-impact/example.completion.v1.json | 2 +- tools/change-impact/protocol.v1.json | 2 +- .../change-impact/validate-change-impact.cjs | 10 ++-- 9 files changed, 92 insertions(+), 12 deletions(-) diff --git a/skills/assistant-debugging/contracts/output.yaml b/skills/assistant-debugging/contracts/output.yaml index adc54c40..dc3ef96e 100644 --- a/skills/assistant-debugging/contracts/output.yaml +++ b/skills/assistant-debugging/contracts/output.yaml @@ -13,7 +13,7 @@ artifacts: - {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: true} + - {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"} diff --git a/skills/assistant-workflow/contracts/output.yaml b/skills/assistant-workflow/contracts/output.yaml index 1d42db40..2e68fcf1 100644 --- a/skills/assistant-workflow/contracts/output.yaml +++ b/skills/assistant-workflow/contracts/output.yaml @@ -60,7 +60,7 @@ artifacts: - {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: true} + - {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"} diff --git a/tests/change-impact/change-impact.test.cjs b/tests/change-impact/change-impact.test.cjs index adbe00da..86bcae5c 100644 --- a/tests/change-impact/change-impact.test.cjs +++ b/tests/change-impact/change-impact.test.cjs @@ -36,7 +36,7 @@ function validDocuments() { 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_source_identity: plan.source_identity, evidence_ref: `result-${plan.id}` })); + 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; @@ -184,6 +184,52 @@ test("canonical pre_build passes through the API and CLI with the same result", assert.deepEqual(cliResult.result, apiResult); }); +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(); @@ -248,7 +294,7 @@ test("capture requirements, reciprocal equivalence, and exact plan and actual co 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_source_identity: "anything", evidence_ref: "anything" }); + 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")); }); diff --git a/tests/change-impact/independent-fixture-adapter.test.cjs b/tests/change-impact/independent-fixture-adapter.test.cjs index 2f8bc5d3..a78f2a43 100644 --- a/tests/change-impact/independent-fixture-adapter.test.cjs +++ b/tests/change-impact/independent-fixture-adapter.test.cjs @@ -112,7 +112,7 @@ function documentsFromTruth(value) { 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_source_identity: plan.source_identity, evidence_ref: `fixture-evidence:${plan.id}` })), + 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", diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index 2119d637..b85869f2 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -198,6 +198,32 @@ abort "old broad debugging condition survived" if expanded["condition"] == "a di local_output = {"status" => "root_cause_found", "symptom_summary" => "local", "reproduction" => {}, "hypotheses" => [], "confidence" => "medium", "verification" => [], "residual_risks" => []} abort "local output fabricated expanded refs" if local_output.keys.any? { |key| key.end_with?("_ref") || key == "change_impact_evidence" } +[load.call(File.join(framework, "skills/assistant-workflow/contracts/output.yaml")), debug_output].each do |contract| + envelope = field.call(contract.fetch("artifacts"), "change_impact_evidence") + 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"} + 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" }) +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") diff --git a/tools/change-impact/README.md b/tools/change-impact/README.md index b15f40d3..b857fa52 100644 --- a/tools/change-impact/README.md +++ b/tools/change-impact/README.md @@ -31,7 +31,8 @@ node tools/change-impact/validate-change-impact.cjs --phase completion \ 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 -and one exact review binding per captured requirement obligation. Missing Node 22 or a +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. @@ -68,7 +69,10 @@ valid examples. `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. Equivalent obligations may reuse a plan + 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 a named group with two or more same-contract members and a justification. * Review: `change-impact-review/v1` is a projection of the existing diff --git a/tools/change-impact/example.completion.v1.json b/tools/change-impact/example.completion.v1.json index 58a39df3..6ea3d382 100644 --- a/tools/change-impact/example.completion.v1.json +++ b/tools/change-impact/example.completion.v1.json @@ -33,7 +33,7 @@ "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_source_identity": "navigation-test-current", "evidence_ref": "test-result-navigation-current" }] + "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", diff --git a/tools/change-impact/protocol.v1.json b/tools/change-impact/protocol.v1.json index 60afedf8..523ea3f9 100644 --- a/tools/change-impact/protocol.v1.json +++ b/tools/change-impact/protocol.v1.json @@ -10,6 +10,6 @@ "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.", "Expected context independently supplies current snapshot, required scope, edges, transitions, verification source identities, and review source mappings.", "Completion requires actual verification and exact review projections; discovery completeness remains a caller-reviewed limit."], + "identity_rules": ["Capture records base/candidate union edges and required contract/state transitions.", "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.", "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 index 2d40e8bd..b506adde 100644 --- a/tools/change-impact/validate-change-impact.cjs +++ b/tools/change-impact/validate-change-impact.cjs @@ -239,8 +239,9 @@ function validateAssessment(assessment, capture, expected, phase, reasons) { 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_source_identity", "evidence_ref"]) && - nonBlank(actual.verification_id) && actual.outcome === "passed" && nonBlank(actual.executed_source_identity) && nonBlank(actual.evidence_ref)); + 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 !== "discovery" && (capture.unknown_boundaries.some((boundary) => boundary.material) || assessment.impact_scope === "unresolved")) reasons.push(issue("MATERIAL_IMPACT_UNRESOLVED")); @@ -249,7 +250,10 @@ function validateAssessment(assessment, capture, expected, phase, reasons) { 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]] : [])); - for (const [verificationId, requirementId] of needed) if (!actuals.has(verificationId) || 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")); + 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")); } } From e4064391ee0599f7d4a0a7a1220e2db3a234495b Mon Sep 17 00:00:00 2001 From: Laimis Date: Sun, 20 Sep 2026 00:00:19 +0300 Subject: [PATCH 07/14] fix: close impact validation and selective install safety gaps --- README.md | 4 +- .../variants/workflow-kernel-v1/manifest.json | 10 +- install.ps1 | 84 +++++-- install.sh | 209 ++++++++++++++---- skills/assistant-debugging/SKILL.md | 2 + skills/assistant-workflow/SKILL.md | 2 + tests/change-impact/change-impact.test.cjs | 93 ++++++++ tests/p0-p4/installer-contracts.sh | 187 +++++++++++++++- tests/windows/installer-contracts.ps1 | 70 +++++- tools/change-impact/README.md | 13 +- tools/change-impact/protocol.v1.json | 2 +- .../change-impact/validate-change-impact.cjs | 12 +- 12 files changed, 595 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 3169b46c..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 diff --git a/docs/evals/variants/workflow-kernel-v1/manifest.json b/docs/evals/variants/workflow-kernel-v1/manifest.json index 3b390e54..5db29f67 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": 2313, + "baseline_selected_initial_words": 2316, "candidate_selected_initial_words": 486, - "selected_initial_word_delta": -1827, - "baseline_total_initial_words": 2902, + "selected_initial_word_delta": -1830, + "baseline_total_initial_words": 2905, "candidate_total_initial_words": 1075, - "baseline_selected_entry_words": 3925, + "baseline_selected_entry_words": 3928, "candidate_selected_entry_words": 2098, - "selected_entry_word_delta": -1827, + "selected_entry_word_delta": -1830, "standing_context_growth": 0, "measured_with": "tools/context-budget-report.sh --skill-overlay" }, diff --git a/install.ps1 b/install.ps1 index d3ad1c64..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)) { @@ -2260,6 +2295,9 @@ function Invoke-AssistantFrameworkInstall { $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' @@ -2312,7 +2350,9 @@ 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' 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 740b449d..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 diff --git a/skills/assistant-workflow/SKILL.md b/skills/assistant-workflow/SKILL.md index d1934688..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 diff --git a/tests/change-impact/change-impact.test.cjs b/tests/change-impact/change-impact.test.cjs index 86bcae5c..9c0930cd 100644 --- a/tests/change-impact/change-impact.test.cjs +++ b/tests/change-impact/change-impact.test.cjs @@ -150,6 +150,70 @@ test("shared inventory requires captured consumers and contract/state-transition 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); @@ -298,6 +362,35 @@ test("capture requirements, reciprocal equivalence, and exact plan and actual co 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]; }, diff --git a/tests/p0-p4/installer-contracts.sh b/tests/p0-p4/installer-contracts.sh index 26c7ad77..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" @@ -614,6 +785,12 @@ for change_impact_skill in assistant-debugging assistant-review; do || ! 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 diff --git a/tests/windows/installer-contracts.ps1 b/tests/windows/installer-contracts.ps1 index a14c0d24..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' } } @@ -2140,21 +2186,31 @@ requires: 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' - $codexHome = Join-Path $root 'Codex Change Impact Home [isolated]' $fixtureRoot = Join-Path $root 'Change Impact Fixture [isolated]' - [Environment]::SetEnvironmentVariable('CODEX_HOME', $codexHome, 'Process') 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 $isolatedUserProfile (".agents\skills\$skill\references\change-impact.md") - $installedSkill = Join-Path $isolatedUserProfile (".agents\skills\$skill\SKILL.md") + $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 $isolatedUserProfile '.agents\skills\assistant-workflow'))) "$skill selective installation unexpectedly installed workflow" + 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" diff --git a/tools/change-impact/README.md b/tools/change-impact/README.md index b857fa52..877bca59 100644 --- a/tools/change-impact/README.md +++ b/tools/change-impact/README.md @@ -50,8 +50,11 @@ 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. A shared assessment or material unknown boundary requires at least - one recorded discovery root; compact cosmetic/local controls may have none. An edge has a stable ID, consumer ID, contract ID, dependency kind + 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 @@ -72,9 +75,9 @@ valid examples. 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 a named group with two or more same-contract members and a - justification. + 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 diff --git a/tools/change-impact/protocol.v1.json b/tools/change-impact/protocol.v1.json index 523ea3f9..dae445c8 100644 --- a/tools/change-impact/protocol.v1.json +++ b/tools/change-impact/protocol.v1.json @@ -10,6 +10,6 @@ "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.", "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.", "Completion requires actual verification and exact review projections; discovery completeness remains a caller-reviewed limit."], + "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; 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 verification plan serves one direct obligation or one named equivalence group, never more than one equivalence group.", "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 index b506adde..93cb84a2 100644 --- a/tools/change-impact/validate-change-impact.cjs +++ b/tools/change-impact/validate-change-impact.cjs @@ -99,7 +99,7 @@ function readDocument(path) { } } -function validateCapture(capture, expected, reasons) { +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)) { @@ -121,6 +121,11 @@ function validateCapture(capture, expected, reasons) { 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")); @@ -234,6 +239,9 @@ function validateAssessment(assessment, capture, expected, phase, reasons) { 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")); @@ -286,7 +294,7 @@ function validate({ phase, capture, expected, assessment, review }) { const reasons = []; if (!PHASES.has(phase)) return { valid: false, complete: false, phase: null, reasons: [issue("PHASE_INVALID")] }; validateExpected(expected, phase, reasons); - if (reasons.length === 0) validateCapture(capture, expected, 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); return { valid: reasons.length === 0, complete: phase === "completion" && reasons.length === 0, phase, reasons }; From a9eb51d79e79ab3464cf90643597c7eed8ad2707 Mon Sep 17 00:00:00 2001 From: Laimis Date: Sun, 20 Sep 2026 09:15:42 +0300 Subject: [PATCH 08/14] Fix change-impact completion and output contracts --- .../assistant-debugging/contracts/index.yaml | 4 +- .../assistant-debugging/contracts/output.yaml | 12 ++ skills/assistant-debugging/evals/cases.json | 39 +++++ .../contracts/phase-gates.yaml | 2 +- skills/assistant-workflow/evals/cases.json | 49 +++++++ .../references/change-impact.md | 13 +- .../assistant-workflow/references/phases.md | 6 +- .../references/phases/build.md | 6 +- .../references/phases/decompose.md | 6 +- .../references/phases/design.md | 6 +- .../references/phases/discover.md | 6 +- .../references/phases/document.md | 6 +- .../references/phases/plan.md | 6 +- .../phases/preparation-completion.md | 6 +- .../references/phases/review.md | 6 +- .../references/plan-harness-appendix.md | 4 +- .../task-journal-harness-appendix.md | 2 +- tests/change-impact/change-impact.test.cjs | 22 +++ tests/p0-p4/change-impact-contracts.sh | 134 +++++++++++++++++- .../feature-preparation-evidence-contracts.sh | 2 +- .../lib/feature-preparation-case-oracle.sh | 1 + .../feature-preparation-response-fixtures.sh | 58 ++++++++ .../p0-p4/progressive-discovery-contracts.sh | 11 +- tests/p0-p4/skill-eval-contracts.sh | 17 ++- tools/change-impact/README.md | 2 +- tools/change-impact/protocol.v1.json | 2 +- .../change-impact/validate-change-impact.cjs | 1 + 27 files changed, 390 insertions(+), 39 deletions(-) diff --git a/skills/assistant-debugging/contracts/index.yaml b/skills/assistant-debugging/contracts/index.yaml index c3496ad0..39f309eb 100644 --- a/skills/assistant-debugging/contracts/index.yaml +++ b/skills/assistant-debugging/contracts/index.yaml @@ -33,7 +33,7 @@ load_sets: path: contracts/output.yaml section: artifacts key: name - names: [change_impact_evidence] + names: [change_impact_applicability, change_impact_evidence] budget_words: 1800 current_phase: selectors: @@ -65,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, change_impact_evidence] + 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/output.yaml b/skills/assistant-debugging/contracts/output.yaml index dc3ef96e..f03fb6a4 100644 --- a/skills/assistant-debugging/contracts/output.yaml +++ b/skills/assistant-debugging/contracts/output.yaml @@ -3,6 +3,18 @@ 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 diff --git a/skills/assistant-debugging/evals/cases.json b/skills/assistant-debugging/evals/cases.json index 2af73361..5a1f447c 100644 --- a/skills/assistant-debugging/evals/cases.json +++ b/skills/assistant-debugging/evals/cases.json @@ -130,6 +130,45 @@ "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-workflow/contracts/phase-gates.yaml b/skills/assistant-workflow/contracts/phase-gates.yaml index 1f3cd70c..6fc17509 100644 --- a/skills/assistant-workflow/contracts/phase-gates.yaml +++ b/skills/assistant-workflow/contracts/phase-gates.yaml @@ -819,7 +819,7 @@ invariants: 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 evidence. Expanded artifacts preserve identity through discovery, pre-build, Build, and review; completion needs common validation and assistant-review terminal coverage." + 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." diff --git a/skills/assistant-workflow/evals/cases.json b/skills/assistant-workflow/evals/cases.json index bb49fc58..3df422fc 100644 --- a/skills/assistant-workflow/evals/cases.json +++ b/skills/assistant-workflow/evals/cases.json @@ -6124,6 +6124,55 @@ "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_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_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/change-impact.md b/skills/assistant-workflow/references/change-impact.md index 83d92afd..c2968427 100644 --- a/skills/assistant-workflow/references/change-impact.md +++ b/skills/assistant-workflow/references/change-impact.md @@ -22,9 +22,10 @@ current same inputs. The expanded path covers relevant configuration, registration, event, state, public, wrapper, added, and removed consumers and transitions. -Completion refreshes universe/context then binds verification identity and -assistant-review's projection of original `scope_manifest`, `coverage_ledger`, -review snapshot, and concern bindings. It 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. +`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 f6fe753b..cf328c9c 100644 --- a/skills/assistant-workflow/references/phases.md +++ b/skills/assistant-workflow/references/phases.md @@ -14,8 +14,10 @@ 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; evidenced local work keeps ordinary relevant verification -and self-review without a Node gate. Shared fanout alone does not activate an +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 diff --git a/skills/assistant-workflow/references/phases/build.md b/skills/assistant-workflow/references/phases/build.md index a9119ffc..77967fc3 100644 --- a/skills/assistant-workflow/references/phases/build.md +++ b/skills/assistant-workflow/references/phases/build.md @@ -16,8 +16,10 @@ 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; evidenced local work keeps ordinary relevant verification -and self-review without a Node gate. Shared fanout alone does not activate an +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 diff --git a/skills/assistant-workflow/references/phases/decompose.md b/skills/assistant-workflow/references/phases/decompose.md index 581e00ee..a6c6c355 100644 --- a/skills/assistant-workflow/references/phases/decompose.md +++ b/skills/assistant-workflow/references/phases/decompose.md @@ -16,8 +16,10 @@ 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; evidenced local work keeps ordinary relevant verification -and self-review without a Node gate. Shared fanout alone does not activate an +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 diff --git a/skills/assistant-workflow/references/phases/design.md b/skills/assistant-workflow/references/phases/design.md index f4b2cbc9..925e925d 100644 --- a/skills/assistant-workflow/references/phases/design.md +++ b/skills/assistant-workflow/references/phases/design.md @@ -16,8 +16,10 @@ 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; evidenced local work keeps ordinary relevant verification -and self-review without a Node gate. Shared fanout alone does not activate an +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 diff --git a/skills/assistant-workflow/references/phases/discover.md b/skills/assistant-workflow/references/phases/discover.md index 4be4ac10..850b45b8 100644 --- a/skills/assistant-workflow/references/phases/discover.md +++ b/skills/assistant-workflow/references/phases/discover.md @@ -16,8 +16,10 @@ 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; evidenced local work keeps ordinary relevant verification -and self-review without a Node gate. Shared fanout alone does not activate an +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 diff --git a/skills/assistant-workflow/references/phases/document.md b/skills/assistant-workflow/references/phases/document.md index fcb337a4..f06f4fd6 100644 --- a/skills/assistant-workflow/references/phases/document.md +++ b/skills/assistant-workflow/references/phases/document.md @@ -16,8 +16,10 @@ 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; evidenced local work keeps ordinary relevant verification -and self-review without a Node gate. Shared fanout alone does not activate an +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 diff --git a/skills/assistant-workflow/references/phases/plan.md b/skills/assistant-workflow/references/phases/plan.md index 8d9dbeb7..b0f2fd0a 100644 --- a/skills/assistant-workflow/references/phases/plan.md +++ b/skills/assistant-workflow/references/phases/plan.md @@ -16,8 +16,10 @@ 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; evidenced local work keeps ordinary relevant verification -and self-review without a Node gate. Shared fanout alone does not activate an +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 diff --git a/skills/assistant-workflow/references/phases/preparation-completion.md b/skills/assistant-workflow/references/phases/preparation-completion.md index 1e7e765a..03a2231a 100644 --- a/skills/assistant-workflow/references/phases/preparation-completion.md +++ b/skills/assistant-workflow/references/phases/preparation-completion.md @@ -16,8 +16,10 @@ 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; evidenced local work keeps ordinary relevant verification -and self-review without a Node gate. Shared fanout alone does not activate an +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 diff --git a/skills/assistant-workflow/references/phases/review.md b/skills/assistant-workflow/references/phases/review.md index bc7ce387..21f9a472 100644 --- a/skills/assistant-workflow/references/phases/review.md +++ b/skills/assistant-workflow/references/phases/review.md @@ -16,8 +16,10 @@ 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; evidenced local work keeps ordinary relevant verification -and self-review without a Node gate. Shared fanout alone does not activate an +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 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/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/tests/change-impact/change-impact.test.cjs b/tests/change-impact/change-impact.test.cjs index 9c0930cd..dd10cbaa 100644 --- a/tests/change-impact/change-impact.test.cjs +++ b/tests/change-impact/change-impact.test.cjs @@ -65,6 +65,28 @@ test("portable completion example is an executable protocol document", () => { assert.equal(validate({ phase: "completion", ...commonExample }).complete, true); }); +test("behavior completion rejects 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: "pre_build", ...documents }).valid, true); + 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"); diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index b85869f2..c6b95e5b 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -150,6 +150,125 @@ else fail "cosmetic applicability, local causality, explicit expansion, or tiered evidence declaration 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_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) @@ -195,8 +314,21 @@ 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" => []} +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" }) [load.call(File.join(framework, "skills/assistant-workflow/contracts/output.yaml")), debug_output].each do |contract| envelope = field.call(contract.fetch("artifacts"), "change_impact_evidence") 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/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 42da71fa..2218247e 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,63 @@ 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_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" 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..c1d033a2 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 @@ -2584,7 +2599,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/tools/change-impact/README.md b/tools/change-impact/README.md index 877bca59..62dd01bd 100644 --- a/tools/change-impact/README.md +++ b/tools/change-impact/README.md @@ -67,7 +67,7 @@ valid examples. internally consistent. * Assessment: `change-impact-assessment/v1` creates exactly one canonical obligation per captured contract/state-transition requirement. `preserve` and `change` require planned verification; - `unaffected` needs a rationale and cannot carry verification; `waived` and + a behavior completion must retain at least one such bound verification and 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. diff --git a/tools/change-impact/protocol.v1.json b/tools/change-impact/protocol.v1.json index dae445c8..33fd4946 100644 --- a/tools/change-impact/protocol.v1.json +++ b/tools/change-impact/protocol.v1.json @@ -10,6 +10,6 @@ "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; 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 verification plan serves one direct obligation or one named equivalence group, never more than one equivalence group.", "Completion requires actual verification and exact review projections; discovery completeness remains a caller-reviewed limit."], + "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; 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.", "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 index 93cb84a2..12c1b91c 100644 --- a/tools/change-impact/validate-change-impact.cjs +++ b/tools/change-impact/validate-change-impact.cjs @@ -258,6 +258,7 @@ function validateAssessment(assessment, capture, expected, phase, reasons) { 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 || From cc5bedcb64ab608e3be5e4e2b68622579f0b9a20 Mon Sep 17 00:00:00 2001 From: Laimis Date: Sun, 20 Sep 2026 10:40:52 +0300 Subject: [PATCH 09/14] fix: retain compact workflow and review impact evidence --- .../variants/workflow-kernel-v1/manifest.json | 12 ++-- skills/assistant-review/contracts/index.yaml | 4 +- skills/assistant-review/contracts/output.yaml | 13 ++++ skills/assistant-review/evals/cases.json | 62 ++++++++++++++-- .../assistant-workflow/contracts/index.yaml | 4 +- .../assistant-workflow/contracts/output.yaml | 29 +++++--- skills/assistant-workflow/evals/cases.json | 6 +- tests/p0-p4/change-impact-contracts.sh | 71 ++++++++++++++++--- .../p0-p4/context-budget-report-contracts.sh | 2 +- .../feature-preparation-response-fixtures.sh | 7 +- tests/p0-p4/skill-eval-contracts.sh | 45 ++++++++++++ 11 files changed, 220 insertions(+), 35 deletions(-) diff --git a/docs/evals/variants/workflow-kernel-v1/manifest.json b/docs/evals/variants/workflow-kernel-v1/manifest.json index 5db29f67..a34735fa 100644 --- a/docs/evals/variants/workflow-kernel-v1/manifest.json +++ b/docs/evals/variants/workflow-kernel-v1/manifest.json @@ -22,13 +22,13 @@ "raw_response_retained": false }, "static_measurement": { - "baseline_selected_initial_words": 2316, - "candidate_selected_initial_words": 486, + "baseline_selected_initial_words": 2318, + "candidate_selected_initial_words": 488, "selected_initial_word_delta": -1830, - "baseline_total_initial_words": 2905, - "candidate_total_initial_words": 1075, - "baseline_selected_entry_words": 3928, - "candidate_selected_entry_words": 2098, + "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/skills/assistant-review/contracts/index.yaml b/skills/assistant-review/contracts/index.yaml index 8eaaa9f1..5fd4c281 100644 --- a/skills/assistant-review/contracts/index.yaml +++ b/skills/assistant-review/contracts/index.yaml @@ -41,7 +41,7 @@ load_sets: path: contracts/output.yaml section: artifacts key: name - names: [change_impact_review_projection] + names: [change_impact_applicability, change_impact_review_projection] budget_words: 2550 current_round: selectors: @@ -94,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, change_impact_review_projection] + 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/output.yaml b/skills/assistant-review/contracts/output.yaml index 8fd83c1c..5644bdb8 100644 --- a/skills/assistant-review/contracts/output.yaml +++ b/skills/assistant-review/contracts/output.yaml @@ -6,6 +6,19 @@ 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 diff --git a/skills/assistant-review/evals/cases.json b/skills/assistant-review/evals/cases.json index ed5e2751..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": [ { @@ -2028,6 +2036,52 @@ ] } }, + { + "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", diff --git a/skills/assistant-workflow/contracts/index.yaml b/skills/assistant-workflow/contracts/index.yaml index da5ac8c7..b1a8e014 100644 --- a/skills/assistant-workflow/contracts/index.yaml +++ b/skills/assistant-workflow/contracts/index.yaml @@ -66,7 +66,7 @@ load_sets: path: contracts/output.yaml section: artifacts key: name - names: [change_impact_evidence] + names: [change_impact_applicability, change_impact_evidence] budget_words: 3000 progressive_discovery: references: @@ -121,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, 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] + 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/output.yaml b/skills/assistant-workflow/contracts/output.yaml index 2e68fcf1..d6be009e 100644 --- a/skills/assistant-workflow/contracts/output.yaml +++ b/skills/assistant-workflow/contracts/output.yaml @@ -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, change_impact_evidence] + 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, change_impact_evidence] + 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, change_impact_evidence] + 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,24 +38,37 @@ 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, change_impact_evidence] + 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, change_impact_evidence] + 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; compact local needs causal_evidence_ref, no checker; shared/unresolved/expanded needs expanded_artifact_carried=true." + on_fail: "Add scope/reason; local needs causal evidence; shared/unresolved/expanded need change_impact_evidence 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_evidence type: object required: conditional condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" - description: "Cross-phase reference envelope for explicitly expanded common change-impact v1 documents; it does not duplicate their obligation ledger." - validation: "artifact_identity is retained unchanged from discovery through Build and Review. discovery may report gaps or unavailable checker without claiming planned/executed verification or review. pre_build requires a valid common-checker assessment with no material unknown boundary. completion additionally requires actual verification and a review projection from assistant-review that binds the original canonical scope_manifest, coverage_ledger, and current review snapshot; that projection never substitutes for assistant-review terminal coverage/result checks. Evidenced local work records compact applicability plus ordinary verification/self-review instead." - on_fail: "For an expanded gate, resolve the checker from the active skill installation and run its explicit discovery, pre-build, or completion phase. Missing Node 22 or checker is blocked/unverified; do not replace it with manual-pass prose." + description: "Expanded evidence." + validation: "Keep artifact_identity across phases. Discovery gaps/checker blocks cannot claim verification/review. pre_build requires valid assessment, no material unknown. completion requires verification plus assistant-review projection binding original scope_manifest, coverage_ledger, current snapshot; it cannot replace terminal checks." + 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]} diff --git a/skills/assistant-workflow/evals/cases.json b/skills/assistant-workflow/evals/cases.json index 3df422fc..d8410bcc 100644 --- a/skills/assistant-workflow/evals/cases.json +++ b/skills/assistant-workflow/evals/cases.json @@ -6136,7 +6136,7 @@ "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_evidence", "discovery", "capture-prepare-only-current"], + "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"}, @@ -6154,6 +6154,10 @@ {"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"}, diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index c6b95e5b..d5c0082a 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -123,31 +123,74 @@ else fail "$(IFS='; '; printf '%s' "${local_proportionality_failures[*]}")" fi -test_start "impact applicability admits cosmetic not_applicable without relaxing local or explicit-expanded obligations" +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] -%w[assistant-workflow assistant-debugging assistant-review].each do |skill| +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")) - applicability = field.call(input.fetch("fields"), "change_impact_applicability") - scope = field.call(applicability.fetch("object_fields"), "impact_scope") - causal = field.call(applicability.fetch("object_fields"), "causal_evidence_ref") + 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} applicability condition excludes cosmetic decisions" unless applicability.fetch("condition").include?("cosmetic") + 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} does not retain explicit expanded-artifact control" unless applicability.fetch("validation").include?("expanded_artifact_carried") + 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 -output = YAML.load_file(File.join(framework, "skills/assistant-workflow/contracts/output.yaml")) -tiers = output.fetch("completion_tiers") +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 "cosmetic applicability, local causality, explicit expansion, or tiered evidence declaration regressed" + 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" @@ -195,6 +238,10 @@ if ! run_semantic_eval assistant-workflow "$workflow_semantic_case" "$workflow_s 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"' \ @@ -354,6 +401,10 @@ abort "local output accepts missing causal evidence" if accepts_local.call(local 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" }) + 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| diff --git a/tests/p0-p4/context-budget-report-contracts.sh b/tests/p0-p4/context-budget-report-contracts.sh index 4e9d3a53..8d5b4f84 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: 5232, bytes: 42798} + and .selected_load_set_context.declared_boundary_closure == {words: 5234, bytes: 42856} 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/lib/feature-preparation-response-fixtures.sh b/tests/p0-p4/lib/feature-preparation-response-fixtures.sh index 2218247e..b7a3a005 100644 --- a/tests/p0-p4/lib/feature-preparation-response-fixtures.sh +++ b/tests/p0-p4/lib/feature-preparation-response-fixtures.sh @@ -416,7 +416,12 @@ build_medium_prepare_only_shared_impact_response() { build_medium_prepare_only_response "$response_path" "$summary" none jq ' - .change_impact_evidence = { + .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", diff --git a/tests/p0-p4/skill-eval-contracts.sh b/tests/p0-p4/skill-eval-contracts.sh index c1d033a2..3665fe8b 100644 --- a/tests/p0-p4/skill-eval-contracts.sh +++ b/tests/p0-p4/skill-eval-contracts.sh @@ -656,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 ;; @@ -1620,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" From 609be9027fb0eb6cecd383d52a1b3aafac8beb2b Mon Sep 17 00:00:00 2001 From: Laimis Date: Sun, 20 Sep 2026 12:04:53 +0300 Subject: [PATCH 10/14] Bind change-impact receipts to validated input snapshots --- .../contracts/handoffs.yaml | 2 +- .../assistant-debugging/contracts/input.yaml | 3 +- .../references/change-impact.md | 4 +- skills/assistant-review/contracts/input.yaml | 3 +- .../references/change-impact.md | 4 +- .../contracts/handoffs.yaml | 4 +- .../assistant-workflow/contracts/input.yaml | 5 +- .../references/change-impact.md | 6 +- tests/change-impact/change-impact.test.cjs | 203 +++++++++++++++++- .../assistant-review-reference-contracts.sh | 2 +- tests/p0-p4/change-impact-contracts.sh | 29 ++- tools/change-impact/README.md | 24 ++- tools/change-impact/protocol.v1.json | 2 +- .../change-impact/validate-change-impact.cjs | 129 ++++++++++- 14 files changed, 393 insertions(+), 27 deletions(-) diff --git a/skills/assistant-debugging/contracts/handoffs.yaml b/skills/assistant-debugging/contracts/handoffs.yaml index 5362480e..1f0b3a7f 100644 --- a/skills/assistant-debugging/contracts/handoffs.yaml +++ b/skills/assistant-debugging/contracts/handoffs.yaml @@ -58,7 +58,7 @@ handoffs: type: object required: conditional condition: "impact_scope in [shared, unresolved] or an expanded change-impact artifact is explicitly carried" - validation: "The common checker was run or revalidated against the referenced capture_ref, independent expected_context_ref, and assessment_ref for a current valid pre_build result that resolves artifact_identity and validator_result_ref before Fixer dispatch; impact_scope and status remain bound unchanged. A status or result ref alone is insufficient, reuse requires demonstrably current same-input evidence, and discovery or completion evidence does not authorize a fix mutation." + 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]} diff --git a/skills/assistant-debugging/contracts/input.yaml b/skills/assistant-debugging/contracts/input.yaml index 9ff2a5bd..54b6b3c6 100644 --- a/skills/assistant-debugging/contracts/input.yaml +++ b/skills/assistant-debugging/contracts/input.yaml @@ -79,7 +79,7 @@ fields: type: object required: false description: "Current common change-impact artifact references for a shared, unresolved, or explicitly carried expanded repair." - validation: "Preserves artifact_identity and capture/expected-context refs. Discovery may report gaps; an expanded repair may not claim pre-build or completion evidence without the common checker." + validation: "Preserves identity/refs; valid carries validator result. 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]} @@ -87,6 +87,7 @@ fields: - {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: 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/references/change-impact.md b/skills/assistant-debugging/references/change-impact.md index eaf09c6f..f7d94942 100644 --- a/skills/assistant-debugging/references/change-impact.md +++ b/skills/assistant-debugging/references/change-impact.md @@ -19,8 +19,8 @@ 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. Reuse is allowed only for demonstrably current same-input -evidence. Preserve the same artifact identity through expanded verification. An expanded fix completion +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/contracts/input.yaml b/skills/assistant-review/contracts/input.yaml index fce8ec23..66fb987e 100644 --- a/skills/assistant-review/contracts/input.yaml +++ b/skills/assistant-review/contracts/input.yaml @@ -34,7 +34,7 @@ fields: 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 the supplied artifact_identity, capture/expected/assessment refs, and current review scope. Read-only audit may report a missing projection or material gap but cannot claim CLEAN from expanded impact evidence." + validation: "Uses artifact identity, refs, validator result when valid, and review scope. 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]} @@ -42,6 +42,7 @@ fields: - {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: 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-review/references/change-impact.md b/skills/assistant-review/references/change-impact.md index 2ff580cd..90eae6d8 100644 --- a/skills/assistant-review/references/change-impact.md +++ b/skills/assistant-review/references/change-impact.md @@ -17,6 +17,8 @@ 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. A valid impact projection +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-workflow/contracts/handoffs.yaml b/skills/assistant-workflow/contracts/handoffs.yaml index 2f0143a8..d158908a 100644 --- a/skills/assistant-workflow/contracts/handoffs.yaml +++ b/skills/assistant-workflow/contracts/handoffs.yaml @@ -1628,7 +1628,7 @@ handoffs: 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 run or revalidated against the referenced capture_ref, independent expected_context_ref, and assessment_ref for a current valid pre_build result that resolves artifact_identity and validator_result_ref against the current task packet; impact_scope and status remain bound unchanged. A status or result ref alone is insufficient, and reuse requires demonstrably current same-input evidence. The CodeWriter may not use discovery or completion evidence as mutation authorization." + 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]} @@ -2280,7 +2280,7 @@ handoffs: 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 run or revalidated against the referenced capture_ref, independent expected_context_ref, and assessment_ref for a current valid pre_build result that resolves artifact_identity and validator_result_ref against the current task packet; impact_scope and status remain bound unchanged. A status or result ref alone is insufficient, reuse requires demonstrably current same-input evidence, and BuilderTester must not reuse CodeWriter's pre-build result after earlier mutations." + 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]} diff --git a/skills/assistant-workflow/contracts/input.yaml b/skills/assistant-workflow/contracts/input.yaml index e8089562..f77a06fe 100644 --- a/skills/assistant-workflow/contracts/input.yaml +++ b/skills/assistant-workflow/contracts/input.yaml @@ -137,8 +137,8 @@ fields: - name: change_impact_context type: object required: false - description: "Expanded assessment refs." - validation: "Includes identity, phase, capture, expected context, and an assessment only for pre-build or completion." + description: "Expanded refs." + validation: "Valid includes identity, phase, refs, validator result; assessment pre-build/completion." object_fields: - {name: artifact_identity, type: string, required: true} - {name: status, type: enum, required: true, enum_values: [valid, gaps_reported, blocked, invalid]} @@ -146,6 +146,7 @@ fields: - {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: 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-workflow/references/change-impact.md b/skills/assistant-workflow/references/change-impact.md index c2968427..72aeabb4 100644 --- a/skills/assistant-workflow/references/change-impact.md +++ b/skills/assistant-workflow/references/change-impact.md @@ -16,9 +16,9 @@ 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. Current valid `pre_build` resolves to the -task packet; status/ref alone is insufficient. Reuse only with demonstrably -current same inputs. The expanded path covers relevant configuration, +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. diff --git a/tests/change-impact/change-impact.test.cjs b/tests/change-impact/change-impact.test.cjs index dd10cbaa..e33b5b2f 100644 --- a/tests/change-impact/change-impact.test.cjs +++ b/tests/change-impact/change-impact.test.cjs @@ -13,6 +13,12 @@ const commonExample = JSON.parse(fs.readFileSync(path.join(__dirname, "..", ".." 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); @@ -44,13 +50,17 @@ function validDocuments() { function codes(result) { return result.reasons.map((reason) => reason.code); } -function runCli(documents, phase, mutate) { +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)); + 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) }; } @@ -65,7 +75,7 @@ test("portable completion example is an executable protocol document", () => { assert.equal(validate({ phase: "completion", ...commonExample }).complete, true); }); -test("behavior completion rejects an all-unaffected assessment without a bound current verification", () => { +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, @@ -77,7 +87,12 @@ test("behavior completion rejects an all-unaffected assessment without a bound c documents.assessment.equivalence_groups = []; documents.assessment.actual_verifications = []; - assert.equal(validate({ phase: "pre_build", ...documents }).valid, true); + 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")); @@ -146,7 +161,17 @@ test("shared and material captures record a discovery root, while compact cosmet 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", () => { @@ -260,7 +285,7 @@ test("invalid phase diagnostics retain PHASE_INVALID without echoing untrusted p assert.equal(child.stdout.includes(sentinel), false); }); -test("canonical pre_build passes through the API and CLI with the same result", () => { +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 }); @@ -270,6 +295,174 @@ test("canonical pre_build passes through the API and CLI with the same result", 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); diff --git a/tests/p0-p4/assistant-review-reference-contracts.sh b/tests/p0-p4/assistant-review-reference-contracts.sh index 74e543d3..483bc219 100644 --- a/tests/p0-p4/assistant-review-reference-contracts.sh +++ b/tests/p0-p4/assistant-review-reference-contracts.sh @@ -1807,7 +1807,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 == { diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index d5c0082a..c9834b75 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -85,6 +85,18 @@ abort "README command retains legacy phase spelling" if readme.include?("--phase 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 @@ -412,8 +424,23 @@ end 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") 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") + 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 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) + %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")) @@ -456,7 +483,7 @@ mutation_handoffs.each do |role, (contract, handoff_name)| 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?("reuse requires demonstrably current same-input evidence") + 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"} diff --git a/tools/change-impact/README.md b/tools/change-impact/README.md index 62dd01bd..b291a4e9 100644 --- a/tools/change-impact/README.md +++ b/tools/change-impact/README.md @@ -27,6 +27,9 @@ node tools/change-impact/validate-change-impact.cjs --phase pre_build \ 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 @@ -36,6 +39,25 @@ plus one exact review binding per captured requirement obligation. Missing Node 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 @@ -67,7 +89,7 @@ valid examples. 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 completion must retain at least one such bound verification and its current execution record. `unaffected` needs a rationale and cannot carry verification; `waived` and + 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. diff --git a/tools/change-impact/protocol.v1.json b/tools/change-impact/protocol.v1.json index 33fd4946..f6708931 100644 --- a/tools/change-impact/protocol.v1.json +++ b/tools/change-impact/protocol.v1.json @@ -10,6 +10,6 @@ "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; 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.", "Completion requires actual verification and exact review projections; discovery completeness remains a caller-reviewed limit."], + "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 index 12c1b91c..e392b20d 100644 --- a/tools/change-impact/validate-change-impact.cjs +++ b/tools/change-impact/validate-change-impact.cjs @@ -6,11 +6,14 @@ // 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"]); @@ -22,6 +25,110 @@ function issue(code) { return { code }; } +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); } @@ -252,6 +359,7 @@ function validateAssessment(assessment, capture, expected, phase, reasons) { 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") { @@ -291,21 +399,31 @@ function validateReview(review, assessment, expected, reasons) { 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 }) { +function validate({ phase, capture, expected, assessment, review, priorResult }) { const reasons = []; - if (!PHASES.has(phase)) return { valid: false, complete: false, phase: null, reasons: [issue("PHASE_INVALID")] }; + 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); - return { valid: reasons.length === 0, complete: phase === "completion" && reasons.length === 0, phase, 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"].includes(key) || value === undefined || options[key]) throw new Error("CLI_USAGE_INVALID"); + 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"] || @@ -324,12 +442,13 @@ function main() { 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({ valid: false, complete: false, phase: null, reasons: [issue(code)] })}\n`); + 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; } } From ca90df6ed6cbb3b24a1b335a48c612c446fafaf7 Mon Sep 17 00:00:00 2001 From: Laimis Date: Sun, 20 Sep 2026 14:45:56 +0300 Subject: [PATCH 11/14] fix(review): retain completion validation result reference --- skills/assistant-review/contracts/output.yaml | 5 +++-- tests/p0-p4/change-impact-contracts.sh | 21 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/skills/assistant-review/contracts/output.yaml b/skills/assistant-review/contracts/output.yaml index 5644bdb8..52db422a 100644 --- a/skills/assistant-review/contracts/output.yaml +++ b/skills/assistant-review/contracts/output.yaml @@ -24,8 +24,8 @@ artifacts: 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. 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, or report the missing/blocked impact evidence." + 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]} @@ -33,6 +33,7 @@ artifacts: - {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"} diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index c9834b75..bc02eb2f 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -389,8 +389,10 @@ 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" }) -[load.call(File.join(framework, "skills/assistant-workflow/contracts/output.yaml")), debug_output].each do |contract| - envelope = field.call(contract.fetch("artifacts"), "change_impact_evidence") +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") @@ -410,9 +412,24 @@ abort "local output accepts missing causal evidence" if accepts_local.call(local 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") From 1866d8ba1b88d52740199d644fd57b15e33b32f0 Mon Sep 17 00:00:00 2001 From: Laimis Date: Mon, 21 Sep 2026 08:11:56 +0300 Subject: [PATCH 12/14] fix(contracts): gate missing impact scope and allow incomplete audit exit --- skills/assistant-debugging/contracts/phase-gates.yaml | 5 ++++- skills/assistant-review/SKILL.md | 8 +++++--- skills/assistant-review/contracts/phase-gates.yaml | 4 ++++ skills/assistant-workflow/contracts/phase-gates.yaml | 6 +++++- tests/p0-p4/assistant-review-reference-contracts.sh | 9 ++++++++- tests/p0-p4/change-impact-contracts.sh | 10 ++++++++++ tests/p0-p4/context-budget-report-contracts.sh | 2 +- 7 files changed, 37 insertions(+), 7 deletions(-) diff --git a/skills/assistant-debugging/contracts/phase-gates.yaml b/skills/assistant-debugging/contracts/phase-gates.yaml index 28672d85..bf1e244d 100644 --- a/skills/assistant-debugging/contracts/phase-gates.yaml +++ b/skills/assistant-debugging/contracts/phase-gates.yaml @@ -64,8 +64,11 @@ gates: 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, every behavior repair records compact applicability evidence. 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." + 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: diff --git a/skills/assistant-review/SKILL.md b/skills/assistant-review/SKILL.md index 0fc21749..bc61647f 100644 --- a/skills/assistant-review/SKILL.md +++ b/skills/assistant-review/SKILL.md @@ -35,9 +35,11 @@ independently evaluates acceptance, evidence, quality, progression, and readines - Resolve scope, mode, and material before the loop; rank findings by severity, evidence, and confidence. -- A frozen-snapshot batch has at least two COMPLETED passes, independently - performed before aggregation, fix, or exit. Audit returns terminal coverage after one started batch, complete or - `HAS_REMAINING_ITEMS` when incomplete, with non-empty `reviewed_scope`. +- 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 diff --git a/skills/assistant-review/contracts/phase-gates.yaml b/skills/assistant-review/contracts/phase-gates.yaml index 1a59204b..91c14563 100644 --- a/skills/assistant-review/contracts/phase-gates.yaml +++ b/skills/assistant-review/contracts/phase-gates.yaml @@ -233,6 +233,10 @@ gates: 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" diff --git a/skills/assistant-workflow/contracts/phase-gates.yaml b/skills/assistant-workflow/contracts/phase-gates.yaml index 6fc17509..c2db74b1 100644 --- a/skills/assistant-workflow/contracts/phase-gates.yaml +++ b/skills/assistant-workflow/contracts/phase-gates.yaml @@ -457,8 +457,12 @@ gates: 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, behavior changes record applicability and 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." + 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: diff --git a/tests/p0-p4/assistant-review-reference-contracts.sh b/tests/p0-p4/assistant-review-reference-contracts.sh index 483bc219..e1e5acce 100644 --- a/tests/p0-p4/assistant-review-reference-contracts.sh +++ b/tests/p0-p4/assistant-review-reference-contracts.sh @@ -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" diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index bc02eb2f..c9a8f4e8 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -559,6 +559,16 @@ checks.each do |path, phase_name, assertion_id| 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")) diff --git a/tests/p0-p4/context-budget-report-contracts.sh b/tests/p0-p4/context-budget-report-contracts.sh index 8d5b4f84..b61243dc 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: 5234, bytes: 42856} + and .selected_load_set_context.declared_boundary_closure == {words: 5247, bytes: 42980} 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 == From fe1c79ee6275b690fea38de751ba79541cb21742 Mon Sep 17 00:00:00 2001 From: Laimis Date: Mon, 21 Sep 2026 08:48:51 +0300 Subject: [PATCH 13/14] fix(review): require applicability before dispatch and restore context budget --- skills/assistant-review/SKILL.md | 4 +--- .../contracts/phase-gates.yaml | 6 ++++- tests/p0-p4/change-impact-contracts.sh | 22 +++++++++++++++++++ .../p0-p4/context-budget-report-contracts.sh | 2 +- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/skills/assistant-review/SKILL.md b/skills/assistant-review/SKILL.md index bc61647f..08af2d88 100644 --- a/skills/assistant-review/SKILL.md +++ b/skills/assistant-review/SKILL.md @@ -21,9 +21,7 @@ non-empty `reviewed_scope` so workflow consumers can use the producer packet. 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. Run the bounded review policy, keep interim -results inside its loop, and present one evidence-calibrated result. Required QA -follows build/test and code-review evidence. +canonical file and record recovery. ## Goal diff --git a/skills/assistant-review/contracts/phase-gates.yaml b/skills/assistant-review/contracts/phase-gates.yaml index 91c14563..3f9e2d18 100644 --- a/skills/assistant-review/contracts/phase-gates.yaml +++ b/skills/assistant-review/contracts/phase-gates.yaml @@ -34,8 +34,12 @@ 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: "Every behavior review records compact applicability evidence. 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." + 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." diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index c9a8f4e8..3a2fa269 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -463,6 +463,28 @@ 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")) diff --git a/tests/p0-p4/context-budget-report-contracts.sh b/tests/p0-p4/context-budget-report-contracts.sh index b61243dc..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: 5247, bytes: 42980} + 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 == From f9a8ada1d2614003efe33fee67bf6f7b6c35ae01 Mon Sep 17 00:00:00 2001 From: Laimis Date: Mon, 21 Sep 2026 12:38:38 +0300 Subject: [PATCH 14/14] Close impact evidence transport and runtime guard gaps --- .../assistant-debugging/contracts/input.yaml | 3 +- .../assistant-review/contracts/handoffs.yaml | 7 +- skills/assistant-review/contracts/input.yaml | 3 +- .../contracts/handoffs.yaml | 12 +++- .../assistant-workflow/contracts/input.yaml | 9 +-- .../assistant-workflow/contracts/output.yaml | 6 +- tests/change-impact/change-impact.test.cjs | 40 +++++++++++ tests/p0-p4/change-impact-contracts.sh | 68 +++++++++++++++++-- .../change-impact/validate-change-impact.cjs | 11 ++- 9 files changed, 142 insertions(+), 17 deletions(-) diff --git a/skills/assistant-debugging/contracts/input.yaml b/skills/assistant-debugging/contracts/input.yaml index 54b6b3c6..7d7b8867 100644 --- a/skills/assistant-debugging/contracts/input.yaml +++ b/skills/assistant-debugging/contracts/input.yaml @@ -79,7 +79,7 @@ fields: 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. Discovery may report gaps; expanded repair requires the common checker." + 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]} @@ -87,6 +87,7 @@ fields: - {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 diff --git a/skills/assistant-review/contracts/handoffs.yaml b/skills/assistant-review/contracts/handoffs.yaml index fe83892e..b53cd19c 100644 --- a/skills/assistant-review/contracts/handoffs.yaml +++ b/skills/assistant-review/contracts/handoffs.yaml @@ -523,7 +523,7 @@ handoffs: 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; gaps_reported, blocked, or invalid carries a gap_or_blocker_ref and remains independently challengeable. It provides no sibling output or alternate ledger." + 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]} @@ -533,6 +533,11 @@ handoffs: - {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 diff --git a/skills/assistant-review/contracts/input.yaml b/skills/assistant-review/contracts/input.yaml index 66fb987e..5441e39f 100644 --- a/skills/assistant-review/contracts/input.yaml +++ b/skills/assistant-review/contracts/input.yaml @@ -34,7 +34,7 @@ fields: 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. Read-only audit may report gaps but cannot claim CLEAN from expanded impact evidence." + 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]} @@ -42,6 +42,7 @@ fields: - {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 diff --git a/skills/assistant-workflow/contracts/handoffs.yaml b/skills/assistant-workflow/contracts/handoffs.yaml index d158908a..9e2874dd 100644 --- a/skills/assistant-workflow/contracts/handoffs.yaml +++ b/skills/assistant-workflow/contracts/handoffs.yaml @@ -930,7 +930,7 @@ handoffs: 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." + 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} @@ -941,6 +941,11 @@ handoffs: - {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 @@ -2262,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 diff --git a/skills/assistant-workflow/contracts/input.yaml b/skills/assistant-workflow/contracts/input.yaml index f77a06fe..c0c1899c 100644 --- a/skills/assistant-workflow/contracts/input.yaml +++ b/skills/assistant-workflow/contracts/input.yaml @@ -125,8 +125,8 @@ fields: type: object required: conditional condition: "a behavior, cosmetic, or locality/shared/unresolved impact decision affects verification" - description: "Applicability decision." - validation: "not_applicable is cosmetic; local needs causal evidence; shared/unresolved or expanded_artifact_carried needs artifact." + 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} @@ -137,8 +137,8 @@ fields: - name: change_impact_context type: object required: false - description: "Expanded refs." - validation: "Valid includes identity, phase, refs, validator result; assessment pre-build/completion." + 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]} @@ -146,6 +146,7 @@ fields: - {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 diff --git a/skills/assistant-workflow/contracts/output.yaml b/skills/assistant-workflow/contracts/output.yaml index d6be009e..6741f0e0 100644 --- a/skills/assistant-workflow/contracts/output.yaml +++ b/skills/assistant-workflow/contracts/output.yaml @@ -54,8 +54,8 @@ artifacts: required: conditional condition: "a behavior, cosmetic, or locality/shared/unresolved impact decision affects verification" 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; local needs causal evidence; shared/unresolved/expanded need change_impact_evidence or report blocked." + 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} @@ -67,7 +67,7 @@ artifacts: 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. Discovery gaps/checker blocks cannot claim verification/review. pre_build requires valid assessment, no material unknown. completion requires verification plus assistant-review projection binding original scope_manifest, coverage_ledger, current snapshot; it cannot replace terminal checks." + 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} diff --git a/tests/change-impact/change-impact.test.cjs b/tests/change-impact/change-impact.test.cjs index e33b5b2f..7a870be9 100644 --- a/tests/change-impact/change-impact.test.cjs +++ b/tests/change-impact/change-impact.test.cjs @@ -65,6 +65,30 @@ function runCli(documents, phase, mutate, priorResult, indentation) { 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); @@ -75,6 +99,22 @@ 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) => ({ diff --git a/tests/p0-p4/change-impact-contracts.sh b/tests/p0-p4/change-impact-contracts.sh index 3a2fa269..7f678f90 100644 --- a/tests/p0-p4/change-impact-contracts.sh +++ b/tests/p0-p4/change-impact-contracts.sh @@ -442,18 +442,25 @@ end 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 in [gaps_reported, blocked, invalid]" && ["gaps_reported", "blocked", "invalid"].include?(payload["status"])) + 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) @@ -497,13 +504,56 @@ abort "Architect rejects a carried local impact artifact" unless architect_scope 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_impact, reviewer_impact].each do |carrier| +[ + ["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") - abort "read-only carrier loses discovery/completion evidence" unless phase && phase["enum_values"] == ["discovery", "pre_build", "completion"] - abort "read-only carrier requires discovery assessment" unless assessment && assessment["condition"].include?("phase in [pre_build, completion]") - abort "read-only carrier rejects a carried local impact artifact" unless scope && scope["enum_values"] == canonical_scopes + 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 = { @@ -511,6 +561,12 @@ mutation_handoffs = { "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") @@ -518,7 +574,9 @@ mutation_handoffs.each do |role, (contract, handoff_name)| 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") diff --git a/tools/change-impact/validate-change-impact.cjs b/tools/change-impact/validate-change-impact.cjs index e392b20d..985a0d61 100644 --- a/tools/change-impact/validate-change-impact.cjs +++ b/tools/change-impact/validate-change-impact.cjs @@ -25,6 +25,14 @@ 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(",")}]`; @@ -400,6 +408,7 @@ function validateReview(review, assessment, expected, reasons) { } 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); @@ -434,7 +443,7 @@ function parseArgs(argv) { function main() { try { - if (Number(process.versions.node.split(".")[0]) < 22) throw new Error("RUNTIME_NODE22_OR_NEWER_REQUIRED"); + if (!nodeRuntimeSupported()) throw new Error("RUNTIME_NODE22_OR_NEWER_REQUIRED"); const options = parseArgs(process.argv.slice(2)); const result = validate({ phase: options["--phase"],