Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 80 additions & 3 deletions bench/journeys_capture_evidence_v5.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import (
const (
captureEvidenceDescriptorNormalLineage = "capture-evidence-v5-normal"
captureEvidenceDescriptorCorrectionLineage = "capture-evidence-v5-correction"
targetedInspectionLineage = "targeted-validator-inspection"
statusSchemaV5 = "gentle-ai.review-integration.status/v5"
verificationEvidenceSchemaV1 = "https://gentle-ai.dev/schema/review/verification-evidence/v1"
verificationEvidenceRecordSchemaV2 = "gentle-ai.review-verification-evidence/v2"
)

var captureEvidenceDescriptorCapability = &Capability{Verb: []string{"review", "capture-evidence"},
Flags: []string{"--repository-context", "--lineage", "--target", "--expected-revision", "--outcome", "--input"}}
var targetedInspectionCapability = &Capability{Verb: []string{"review", "inspect-candidate"},
Flags: []string{"--repository-context", "--lineage", "--target", "--expected-revision", "--purpose", "--request-hash"}}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// captureEvidenceDescriptorJourneys proves issue #2248's V5 contract at the
// built-binary boundary. The runner executes only tokens published by STATUS;
Expand Down Expand Up @@ -62,6 +65,23 @@ func captureEvidenceDescriptorJourneys() []Journey {
{Name: "execute the advanced targeted-validation finalize descriptor", Requires: finalizeValidationCapability, Composite: completeV5DescriptorCorrection},
},
},
{
ID: "j95-targeted-validator-inspects-provider-bound-corrected-tree",
Title: "Targeted validator inspects STATUS-bound corrected trees through live worktree drift",
Source: "issue #2945: corrected targeted validation must inspect only the provider-bound immutable candidate",
Steps: []Step{
{Name: "fixture: repo", Fixture: baseRepo},
{Name: "fixture: stage correction candidate", Fixture: stageCaptureEvidenceDescriptorCorrection},
{Name: "start correction review", Requires: startNamedCapability, Args: productArgs("review", "start", "--lineage", targetedInspectionLineage)},
{Name: "capture correction finding and complete lenses", Requires: captureResultCapability, Composite: captureCorrectableFinding},
{Name: "finalize reviewer results into correction-required", Requires: finalizeResultsCapability, Args: productArgs("review", "finalize", "--lineage", targetedInspectionLineage, "--captured-results=true")},
{Name: "forecast the bounded correction", Requires: finalizeCorrectionCapability, Args: productArgs("review", "finalize", "--lineage", targetedInspectionLineage, "--correction-lines", "2")},
{Name: "fixture: correct the reviewed candidate", Fixture: writeCorrectedCandidate},
{Name: "execute the correction STATUS v5 capture-evidence descriptor", Requires: captureEvidenceDescriptorCapability, Composite: captureJ95Evidence},
{Name: "inspect frozen correction after live drift and refuse drifted FINALIZE", Requires: targetedInspectionCapability, Composite: inspectJ95CorrectedCandidate},
{Name: "finalize after restoring the corrected candidate", Requires: finalizeValidationCapability, Composite: completeJ95Correction},
},
},
}
}

Expand Down Expand Up @@ -93,7 +113,15 @@ func captureV5NormalEvidenceDescriptor(r *journeyRun) error {
}

func captureV5CorrectionEvidenceDescriptor(r *journeyRun) error {
after, err := executeV5CaptureEvidenceDescriptor(r, captureEvidenceDescriptorCorrectionLineage, "correction-v5-evidence.txt")
return captureV5CorrectionEvidenceDescriptorFor(r, captureEvidenceDescriptorCorrectionLineage)
}

func captureJ95Evidence(r *journeyRun) error {
return captureV5CorrectionEvidenceDescriptorFor(r, targetedInspectionLineage)
}

func captureV5CorrectionEvidenceDescriptorFor(r *journeyRun, lineage string) error {
after, err := executeV5CaptureEvidenceDescriptor(r, lineage, "correction-v5-evidence.txt")
if err != nil {
return err
}
Expand Down Expand Up @@ -189,7 +217,15 @@ func captureEvidenceDescriptorArguments(status waveCorrectionStatus, outcome, in
}

func completeV5DescriptorCorrection(r *journeyRun) error {
status, err := readCorrectionStatusForContract(r, captureEvidenceDescriptorCorrectionLineage, reviewContractV2)
return completeV5DescriptorCorrectionFor(r, captureEvidenceDescriptorCorrectionLineage)
}

func completeJ95Correction(r *journeyRun) error {
return completeV5DescriptorCorrectionFor(r, targetedInspectionLineage)
}

func completeV5DescriptorCorrectionFor(r *journeyRun, lineage string) error {
status, err := readCorrectionStatusForContract(r, lineage, reviewContractV2)
if err != nil {
return err
}
Expand All @@ -215,8 +251,49 @@ func completeV5DescriptorCorrection(r *journeyRun) error {
return err
}
result, err := decodeWaveOperation(r.runAt(r.sandbox.Root, arguments, false), "v5 correction finalize descriptor")
if err != nil || result.State != "approved" || result.LineageID != captureEvidenceDescriptorCorrectionLineage {
if err != nil || result.State != "approved" || result.LineageID != lineage {
return fmt.Errorf("v5 correction finalize descriptor result = %+v, %v", result, err)
}
return nil
}

func inspectJ95CorrectedCandidate(r *journeyRun) error {
status, err := readCorrectionStatusForContract(r, targetedInspectionLineage, reviewContractV2)
if err != nil || status.ValidationRequest == nil || status.NextTransition == nil || status.NextTransition.Collect == nil || len(status.NextTransition.Collect.Inputs) != 1 {
return fmt.Errorf("targeted inspection status = %+v, %v", status, err)
}
input := status.NextTransition.Collect.Inputs[0]
if input.CaptureOperation != "external.run_targeted_validation" || len(input.Arguments) != 6 {
return fmt.Errorf("targeted inspection binding = %+v", input)
}
inspection := []string{"review", "inspect-candidate"}
for _, argument := range input.Arguments {
inspection = append(inspection, "--"+argument.Name, argument.Value)
}
inspection = append(inspection, "--operation", "object", "--path-index", "0", "--side", "candidate")
if err := r.sandbox.write(filepath.Join(r.sandbox.Repo, "candidate.go"), "package candidate\n\nfunc value() int { return 3 }\n"); err != nil {
return err
}
inspectionResult := r.runAt(r.sandbox.Root, inspection, false)
if inspectionResult.ExitCode != 0 || !strings.Contains(inspectionResult.Stdout, "func value() int { return 2 }") {
return fmt.Errorf("provider-bound corrected inspection = %q: %s", inspectionResult.Stdout, firstLine(inspectionResult.Stderr))
}
payload, err := json.Marshal(map[string]any{"targeted_validation_request_hash": status.ValidationRequest.RequestHash,
"correction_target_identity": status.ValidationRequest.CorrectionTargetIdentity, "original_criteria": map[string]any{"passed": true, "evidence": []string{"acceptance passed"}},
"correction_regression": map[string]any{"passed": true, "evidence": []string{"regression passed"}}, "follow_ups": []any{}})
if err != nil {
return err
}
path, err := writeScratch(r.sandbox, "j95-validation.json", payload)
if err != nil {
return err
}
arguments, err := correctionSubmissionArguments(r, status, "targeted_validation_required", "validation", path)
if err != nil {
return err
}
if observation := r.runAt(r.sandbox.Root, arguments, false); observation.ExitCode == 0 {
return fmt.Errorf("drifted FINALIZE consumed the correction: %s", observation.Stdout)
}
return writeCorrectedCandidate(r.sandbox)
}
6 changes: 3 additions & 3 deletions bench/journeys_sdd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestPortableSDDFailClosedAuthorityJourneysAreRegistered(t *testing.T) {
// proves #2871's correction binds the immutable failure across a later interrupt;
// j88 proves #2843's unborn STATUS collects explicit untracked intent first;
// j89 proves #2758 never offers a workspace receipt for a different index; j90 proves #2016 resumes an explicit frozen reviewing lineage after workspace drift; j91 proves #1800's pre-plan exit is audited abandon; j92 proves #2879 quarantines released historical bytes without compatibility loading.
// j93 proves #2822 classifies stale managed assets before START can persist.
// j93 proves #2822 classifies stale managed assets before START can persist; j94 remains reserved for #2031; j95 proves #2945 corrected-tree inspection.
// #1993 REMOVED two: j38 (the bound-passing-finish refusal routing to the
// review router) and j39 (the stranded-successor exit it named). Review
// acts after implementation and verification, so that refusal is gone and
Expand All @@ -60,8 +60,8 @@ func TestPortableSDDFailClosedAuthorityJourneysAreRegistered(t *testing.T) {
//
// Bump this deliberately when a journey is added OR removed, and name it
// here: the count exists so a journey cannot appear or vanish unnoticed.
if got := len(seen); got != 90 {
t.Errorf("core journey count = %d, want 90", got)
if got := len(seen); got != 91 {
t.Errorf("core journey count = %d, want 91", got)
}
for id, found := range want {
if !found {
Expand Down
7 changes: 4 additions & 3 deletions bench/journeys_wave1.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ type waveCorrectionStatus struct {
ReasonCode string `json:"reason_code"`
Collect *struct {
Inputs []struct {
Name string `json:"name"`
CaptureOperation string `json:"capture_operation"`
Submission *waveSubmissionDescriptor `json:"submission"`
Name string `json:"name"`
CaptureOperation string `json:"capture_operation"`
Arguments []struct{ Name, Value string } `json:"arguments"`
Submission *waveSubmissionDescriptor `json:"submission"`
} `json:"inputs"`
} `json:"collect"`
Execute *struct {
Expand Down
1 change: 1 addition & 0 deletions docs/review-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ After a correction forecast and an actual candidate change, STATUS first collect

Execute the targeted request unchanged. Its provider-derived hash binds the lineage, expected authority revision, original target, exact frozen finding IDs, projection, corrected candidate tree and identity, and the exact canonical correction-path subset plus its digest. FINALIZE accepts the correction through one atomic state transition only when the targeted validation and passed repository record bind the same authority revision, candidate identity, paths, and ledger IDs. If the candidate did not materially change, no targeted-validation request is issued and routing stops with `corrected_candidate_unavailable`; consumers must not invent a validator request or another correction forecast. An ordinary lineage admits exactly one changed-target correction attempt, even when its measured delta is zero. It never admits a zero-edit correction or second fix transition.

For `targeted_validation_required`, copy the input's `repository-context`, `lineage`, `expected-revision`, `target`, `purpose=targeted-validation`, and `request-hash` exactly into `review inspect-candidate`, then select only an advertised inspection operation and canonical path index. That command reads the immutable corrected tree named by the passed evidence, not the live worktree. Do not pass `--lens` or `--order`: those bind only the original reviewing-lens inspector and are rejected for targeted validation.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
When the action is `recover`, negotiated status also returns the exact generic recovery disposition: `scope_changed`, `escalated`, or `invalidated`. A materially changed escalated candidate exposes only generic `review.recover`. An unchanged escalated candidate normally exposes only `stop`; it exposes `retry_final_verification` with disposition `final_verification_retry` only when native state, receipt, journal, failed evidence, ancestry, leaf, and live-current-snapshot proof all establish the dedicated boundary below. The disposition identifies the accepted provider class but never authorizes either operation. A consumer MUST NOT substitute a different disposition or route the dedicated class through generic `review recover`.

One recovery-only target expands an approved base-diff receipt into the exact staged index: request STATUS with the predecessor lineage, its original `--base-ref`, `--projection staged`, and `--workspace-overlay`. Native routing emits those same three selectors for `review recover` only when HEAD still equals the reviewed candidate, the index retains every reviewed path and adds at least one path, and the canonical predecessor receipt is present. The authorization binds the distinct successor lineage and the staged overlay identity, which already commits the base tree, index tree, projection, paths, and their digests. Unstaged and undeclared untracked bytes are excluded. The successor starts a fresh review with newly derived risk, lenses, changed-line count, and budget; it inherits no approval or evidence. Direct staged-overlay START, unchanged or disjoint scope, a removed reviewed path, selector drift, stale authority, or index drift stops without mutation.
Expand Down
52 changes: 46 additions & 6 deletions internal/cli/review_inspect_candidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type reviewInspectCandidateDeps struct {
timeout time.Duration
operationContext func(context.Context, time.Duration) (context.Context, context.CancelFunc)
resolve func(context.Context, string, reviewtransaction.ReviewRepositoryContextBinding) (string, error)
resolveCorrected func(context.Context, string, reviewtransaction.ReviewRepositoryContextBinding, string) (reviewtransaction.SnapshotBuilder, reviewtransaction.Snapshot, error)
discover func(context.Context, string, string, bool) (reviewtransaction.CompactStore, reviewtransaction.CompactRecord, error)
inspect func(reviewtransaction.SnapshotBuilder, context.Context, reviewtransaction.Snapshot, string, int, string) ([]byte, error)
}
Expand All @@ -33,8 +34,15 @@ func reviewInspectCandidateDependencies() reviewInspectCandidateDeps {
timeout: reviewInspectCandidateTimeout,
operationContext: context.WithTimeout,
resolve: resolveOpaqueReviewRepositoryRoot,
discover: discoverCompactFacadeReview,
inspect: reviewtransaction.SnapshotBuilder.InspectCandidate,
resolveCorrected: func(ctx context.Context, handle string, binding reviewtransaction.ReviewRepositoryContextBinding, requestHash string) (reviewtransaction.SnapshotBuilder, reviewtransaction.Snapshot, error) {
builder, snapshot, err := reviewtransaction.ResolveCorrectedCandidateInspectionBinding(ctx, handle, binding, requestHash)
if err != nil {
return reviewtransaction.SnapshotBuilder{}, reviewtransaction.Snapshot{}, reviewRepositoryContextResolutionFailure(err)
}
return builder, snapshot, nil
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
discover: discoverCompactFacadeReview,
inspect: reviewtransaction.SnapshotBuilder.InspectCandidate,
}
}

Expand All @@ -58,6 +66,8 @@ func runReviewInspectCandidate(args []string, help io.Writer, deps reviewInspect
revision := flags.String("expected-revision", "", "exact reviewing authority revision")
lineage := flags.String("lineage", "", "exact review lineage identifier")
target := flags.String("target", "", "exact frozen target identity")
purpose := flags.String("purpose", "reviewing-lens", "reviewing-lens or targeted-validation")
requestHash := flags.String("request-hash", "", "exact targeted-validation request hash")
lens := flags.String("lens", "", "exact selected lens")
order := flags.Int("order", -1, "zero-based selected lens order")
operation := flags.String("operation", "", "name-status, numstat, stat, patch, or object")
Expand All @@ -66,9 +76,17 @@ func runReviewInspectCandidate(args []string, help io.Writer, deps reviewInspect
if err := parseReviewFlags(flags, args); err != nil || reviewHelpRequested(args) {
return nil, err
}
if flags.NArg() != 0 || strings.TrimSpace(*repositoryContext) == "" || strings.TrimSpace(*revision) == "" ||
strings.TrimSpace(*lineage) == "" || strings.TrimSpace(*target) == "" || strings.TrimSpace(*lens) == "" || *order < 0 {
return nil, reviewPreflightError(errors.New("review inspect-candidate requires the exact provider-issued repository context, revision, lineage, target, lens, and order; run `gentle-ai review inspect-candidate --help` for the closed command forms"))
if *purpose != "targeted-validation" {
if *purpose != "reviewing-lens" {
return nil, reviewPreflightError(errors.New("review inspect-candidate purpose must be reviewing-lens or targeted-validation")) // refusal:by-design operator-knowledge: the native inspector has exactly two disjoint authority modes
}
if reviewFlagWasProvided(flags, "request-hash") {
return nil, reviewPreflightError(errors.New("review inspect-candidate request hash is valid only for targeted validation")) // refusal:by-design operator-knowledge: reviewing-lens authority never carries a correction request
}
if flags.NArg() != 0 || strings.TrimSpace(*repositoryContext) == "" || strings.TrimSpace(*revision) == "" ||
strings.TrimSpace(*lineage) == "" || strings.TrimSpace(*target) == "" || strings.TrimSpace(*lens) == "" || *order < 0 {
return nil, reviewPreflightError(errors.New("review inspect-candidate requires the exact provider-issued repository context, revision, lineage, target, lens, and order; run `gentle-ai review inspect-candidate --help` for the closed command forms"))
}
}
pathProvided := reviewFlagWasProvided(flags, "path-index")
sideProvided := reviewFlagWasProvided(flags, "side")
Expand All @@ -88,7 +106,29 @@ func runReviewInspectCandidate(args []string, help io.Writer, deps reviewInspect
default:
return nil, reviewPreflightError(fmt.Errorf("unknown candidate inspection operation %q; run `gentle-ai review inspect-candidate --help` for the closed command forms", *operation))
}

if *purpose == "targeted-validation" {
if flags.NArg() != 0 || strings.TrimSpace(*repositoryContext) == "" || strings.TrimSpace(*revision) == "" ||
strings.TrimSpace(*lineage) == "" || strings.TrimSpace(*target) == "" || strings.TrimSpace(*requestHash) == "" {
return nil, reviewPreflightError(errors.New("review inspect-candidate targeted validation requires the exact provider-issued repository context, revision, lineage, target, and request hash; run `gentle-ai review inspect-candidate --help` for the closed command forms"))
}
if reviewFlagWasProvided(flags, "lens") || reviewFlagWasProvided(flags, "order") {
return nil, reviewPreflightError(errors.New("review inspect-candidate targeted validation does not accept --lens or --order")) // refusal:by-design operator-knowledge: only a fresh targeted transition names the lens-free inspector binding
}
builder, snapshot, err := deps.resolveCorrected(ctx, *repositoryContext, reviewtransaction.ReviewRepositoryContextBinding{
LineageID: *lineage, TargetIdentity: *target, Revision: *revision,
}, *requestHash)
if err != nil {
return nil, reviewInspectCandidateError(err)
}
payload, err := deps.inspect(builder, ctx, snapshot, *operation, *pathIndex, *side)
if err != nil {
return nil, reviewInspectCandidateError(reviewPreflightError(fmt.Errorf("inspect frozen candidate: %w", err)))
}
if ctx.Err() != nil {
return nil, reviewInspectCandidateError(ctx.Err())
}
return payload, nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
root, err := deps.resolve(ctx, *repositoryContext, reviewtransaction.ReviewRepositoryContextBinding{
LineageID: *lineage, TargetIdentity: *target, Revision: *revision,
})
Expand Down
Loading
Loading