| 1 | package completion |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "strings" |
| 6 | |
| 7 | "reasonix/internal/evidence" |
| 8 | ) |
| 9 | |
| 10 | // Verdict is the report's headline. Partial is terminal: the work is proven |
| 11 | // against its criteria, and the gaps it still carries are declared rather |
| 12 | // than hidden. |
| 13 | type Verdict uint8 |
| 14 | |
| 15 | const ( |
| 16 | VerdictUnknown Verdict = iota |
| 17 | VerdictIncomplete |
| 18 | VerdictPartial |
| 19 | VerdictDone |
| 20 | ) |
| 21 | |
| 22 | func (v Verdict) String() string { |
| 23 | switch v { |
| 24 | case VerdictIncomplete: |
| 25 | return "incomplete" |
| 26 | case VerdictPartial: |
| 27 | return "partial" |
| 28 | case VerdictDone: |
| 29 | return "done" |
| 30 | default: |
| 31 | return "unknown" |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | // Change is one path the turn mutated. Reviewed reports whether the changed |
| 36 | // result was inspected after the last write to it. |
| 37 | type Change struct { |
| 38 | Path string |
| 39 | Reviewed bool |
| 40 | } |
| 41 | |
| 42 | // Verification is a delivery-verification command's latest outcome. Stale |
| 43 | // means it last ran before the newest mutation, so it proves nothing about |
| 44 | // the current tree. |
| 45 | type Verification struct { |
| 46 | Command string |
| 47 | Passed bool |
| 48 | Stale bool |
| 49 | ToolCallID string |
| 50 | Interrupted bool |
| 51 | ExitCode *int |
| 52 | } |
| 53 | |
| 54 | // GapKind classifies one thing the report refuses to present as verified. |
| 55 | type GapKind uint8 |
| 56 | |
| 57 | const ( |
| 58 | // GapUnbackedClaim is first because it is the worst: the turn asserted a |
| 59 | // verification the ledger does not support. |
| 60 | GapUnbackedClaim GapKind = iota |
| 61 | GapUnprovenCriterion |
| 62 | GapMissingCheck |
| 63 | GapFailedVerification |
| 64 | GapStaleVerification |
| 65 | GapUnverifiedChange |
| 66 | GapUnreviewedChange |
| 67 | GapDeclaredUnverified |
| 68 | ) |
| 69 | |
| 70 | func (k GapKind) String() string { |
| 71 | switch k { |
| 72 | case GapUnbackedClaim: |
| 73 | return "unbacked_claim" |
| 74 | case GapDeclaredUnverified: |
| 75 | return "declared_unverified" |
| 76 | case GapUnprovenCriterion: |
| 77 | return "unproven_criterion" |
| 78 | case GapMissingCheck: |
| 79 | return "missing_check" |
| 80 | case GapFailedVerification: |
| 81 | return "failed_verification" |
| 82 | case GapStaleVerification: |
| 83 | return "stale_verification" |
| 84 | case GapUnverifiedChange: |
| 85 | return "unverified_change" |
| 86 | case GapUnreviewedChange: |
| 87 | return "unreviewed_change" |
| 88 | default: |
| 89 | return "unknown" |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | // Gap is one unproven thing, in the report's own words. |
| 94 | type Gap struct { |
| 95 | Kind GapKind |
| 96 | Detail string |
| 97 | } |
| 98 | |
| 99 | // Report is the host's completion record for one turn. |
| 100 | type Report struct { |
| 101 | // AssessmentKind distinguishes observed facts from historical quality assessments. |
| 102 | AssessmentKind string |
| 103 | Verdict Verdict |
| 104 | // Mutations counts every successful mutating receipt, including ones that |
| 105 | // named no path; Changes lists only the paths. |
| 106 | Mutations int |
| 107 | Changes []Change |
| 108 | Verifications []Verification |
| 109 | Gaps []Gap |
| 110 | // Claimed is what the turn said about itself; Risks is its declared risk |
| 111 | // list. Both are model-authored and never clear a host-found gap. |
| 112 | Claimed Claim |
| 113 | Risks []string |
| 114 | } |
| 115 | |
| 116 | // BuildFacts records observations and model declarations without assigning a |
| 117 | // quality verdict or inventing checks that were never run. |
| 118 | func BuildFacts(ledger *evidence.Ledger, workspaceRoot string, scratchRoots []string) Report { |
| 119 | receipts := ledger.Receipts() |
| 120 | rep := Report{ |
| 121 | AssessmentKind: "facts", |
| 122 | Verdict: VerdictUnknown, |
| 123 | Mutations: mutationsOf(receipts, workspaceRoot, scratchRoots), |
| 124 | Changes: changesOf(ledger, receipts, workspaceRoot, scratchRoots), |
| 125 | Verifications: verificationsOf(receipts, workspaceRoot, scratchRoots), |
| 126 | } |
| 127 | return reconcile(rep, claimOf(receipts), receipts) |
| 128 | } |
| 129 | |
| 130 | // changesOf lists mutated paths in first-write order and asks the ledger |
| 131 | // whether each one was inspected after its own latest write, so a review that |
| 132 | // covered one file never vouches for another. |
| 133 | func changesOf(ledger *evidence.Ledger, receipts []evidence.Receipt, workspaceRoot string, scratchRoots []string) []Change { |
| 134 | var out []Change |
| 135 | at := map[string]int{} |
| 136 | lastWrite := map[string]int{} |
| 137 | for i, r := range receipts { |
| 138 | if !evidence.IsDeliveryMutation(r, workspaceRoot, scratchRoots) { |
| 139 | continue |
| 140 | } |
| 141 | for _, p := range r.Paths { |
| 142 | if p == "" || evidence.ClassifyWriteScope(p, workspaceRoot, scratchRoots) == evidence.WriteScopeScratch { |
| 143 | continue |
| 144 | } |
| 145 | if _, seen := at[p]; !seen { |
| 146 | at[p] = len(out) |
| 147 | out = append(out, Change{Path: p}) |
| 148 | } |
| 149 | lastWrite[p] = i |
| 150 | } |
| 151 | } |
| 152 | for i := range out { |
| 153 | out[i].Reviewed = ledger.HasHostReviewCoverageAfter(lastWrite[out[i].Path], []string{out[i].Path}) |
| 154 | } |
| 155 | return out |
| 156 | } |
| 157 | |
| 158 | // mutationsOf counts successful mutating receipts, path-named or not: a |
| 159 | // `sed -i` or `rm` that named nothing still changed the workspace, and must |
| 160 | // not escape the unverified-change gap by leaving no path behind. |
| 161 | func mutationsOf(receipts []evidence.Receipt, workspaceRoot string, scratchRoots []string) int { |
| 162 | count := 0 |
| 163 | for _, r := range receipts { |
| 164 | if evidence.IsDeliveryMutation(r, workspaceRoot, scratchRoots) { |
| 165 | count++ |
| 166 | } |
| 167 | } |
| 168 | return count |
| 169 | } |
| 170 | |
| 171 | // verificationsOf keeps each delivery-verification command's latest run, in |
| 172 | // first-run order, and marks the ones that predate the newest mutation. |
| 173 | func verificationsOf(receipts []evidence.Receipt, workspaceRoot string, scratchRoots []string) []Verification { |
| 174 | lastMutation := -1 |
| 175 | for i, r := range receipts { |
| 176 | if evidence.IsDeliveryMutation(r, workspaceRoot, scratchRoots) { |
| 177 | lastMutation = i |
| 178 | } |
| 179 | } |
| 180 | var out []Verification |
| 181 | at := map[string]int{} |
| 182 | for i, r := range receipts { |
| 183 | command := strings.TrimSpace(r.Command) |
| 184 | if command == "" || r.Verification == evidence.VerificationNotVerification || r.Verification == evidence.VerificationNotRun || !evidence.IsVerificationCommand(command) { |
| 185 | continue |
| 186 | } |
| 187 | if _, seen := at[command]; !seen { |
| 188 | at[command] = len(out) |
| 189 | out = append(out, Verification{Command: command}) |
| 190 | } |
| 191 | out[at[command]].Passed = r.Success && (r.ExitCode == nil || *r.ExitCode == 0) && r.Verification != evidence.VerificationFailed |
| 192 | out[at[command]].Stale = i < lastMutation |
| 193 | out[at[command]].ToolCallID = r.ToolCallID |
| 194 | out[at[command]].Interrupted = r.Interrupted |
| 195 | out[at[command]].ExitCode = r.ExitCode |
| 196 | } |
| 197 | return out |
| 198 | } |
| 199 | |
| 200 | // Summary describes observed quantities without assigning quality. |
| 201 | func (r Report) Summary() string { |
| 202 | return fmt.Sprintf("%d changes, %d checks", len(r.Changes), len(r.Verifications)) |
| 203 | } |
| 204 | |
| 205 | func (r Report) GapKinds() []string { |
| 206 | seen := map[GapKind]bool{} |
| 207 | var out []string |
| 208 | for _, kind := range []GapKind{GapUnbackedClaim, GapUnprovenCriterion, GapMissingCheck, GapFailedVerification, GapStaleVerification, GapUnverifiedChange, GapUnreviewedChange, GapDeclaredUnverified} { |
| 209 | for _, gap := range r.Gaps { |
| 210 | if gap.Kind == kind && !seen[kind] { |
| 211 | seen[kind] = true |
| 212 | out = append(out, kind.String()) |
| 213 | } |
| 214 | } |
| 215 | } |
| 216 | return out |
| 217 | } |
| 218 |