diff --git a/.gitignore b/.gitignore index 9d116ea..90fcfdd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ reviews/ evals/fixtures/logs/ node_modules/ dist/ +evals/packet-dilution/logs/ +evals/skill-semantics/logs/ diff --git a/evals/packet-dilution/cap1.yml b/evals/packet-dilution/cap1.yml new file mode 100644 index 0000000..5baa5ba --- /dev/null +++ b/evals/packet-dilution/cap1.yml @@ -0,0 +1,40 @@ +name: packet-dilution-cap1 +repeat: 6 +repo: + fixture: repos/settlement +command: + branch: feature + base: main +review: + provider: anthropic + model: claude-opus-4-8 + reasoning: high + depth: normal + verify: true + cache: false + concurrency: 1 + maxTimeMinutes: 20 + lenses: + - lang/go + packRelatedHunks: true + packMaxHunks: 1 + pinnedPlanPath: evals/packet-dilution/plans/frozen.json +expect: + maxCostUSD: 3 +should_find: + - id: dilution-split-remainder + path: settlement/fees.go + failureModePattern: remainder|dust|lost|unallocated|sum|drop + titlePattern: split|fee|remainder|share + - id: dilution-cut-order + path: settlement/fees.go + failureModePattern: cap|order|before|under|rate|incorrect + titlePattern: platform|cut|cap|rate + - id: dilution-refund-gross + path: settlement/fees.go + failureModePattern: refund|gross|net|fee|over.?pay|double + titlePattern: refund|gross|net +should_not_find: + - id: dilution-rate-guard-control + path: settlement/fees.go + titlePattern: Rule[0-9]+ diff --git a/evals/packet-dilution/cap3.yml b/evals/packet-dilution/cap3.yml new file mode 100644 index 0000000..ef68ed1 --- /dev/null +++ b/evals/packet-dilution/cap3.yml @@ -0,0 +1,40 @@ +name: packet-dilution-cap3 +repeat: 6 +repo: + fixture: repos/settlement +command: + branch: feature + base: main +review: + provider: anthropic + model: claude-opus-4-8 + reasoning: high + depth: normal + verify: true + cache: false + concurrency: 1 + maxTimeMinutes: 20 + lenses: + - lang/go + packRelatedHunks: true + packMaxHunks: 3 + pinnedPlanPath: evals/packet-dilution/plans/frozen.json +expect: + maxCostUSD: 3 +should_find: + - id: dilution-split-remainder + path: settlement/fees.go + failureModePattern: remainder|dust|lost|unallocated|sum|drop + titlePattern: split|fee|remainder|share + - id: dilution-cut-order + path: settlement/fees.go + failureModePattern: cap|order|before|under|rate|incorrect + titlePattern: platform|cut|cap|rate + - id: dilution-refund-gross + path: settlement/fees.go + failureModePattern: refund|gross|net|fee|over.?pay|double + titlePattern: refund|gross|net +should_not_find: + - id: dilution-rate-guard-control + path: settlement/fees.go + titlePattern: Rule[0-9]+ diff --git a/evals/packet-dilution/cap5.yml b/evals/packet-dilution/cap5.yml new file mode 100644 index 0000000..3413ad0 --- /dev/null +++ b/evals/packet-dilution/cap5.yml @@ -0,0 +1,40 @@ +name: packet-dilution-cap5 +repeat: 6 +repo: + fixture: repos/settlement +command: + branch: feature + base: main +review: + provider: anthropic + model: claude-opus-4-8 + reasoning: high + depth: normal + verify: true + cache: false + concurrency: 1 + maxTimeMinutes: 20 + lenses: + - lang/go + packRelatedHunks: true + packMaxHunks: 5 + pinnedPlanPath: evals/packet-dilution/plans/frozen.json +expect: + maxCostUSD: 3 +should_find: + - id: dilution-split-remainder + path: settlement/fees.go + failureModePattern: remainder|dust|lost|unallocated|sum|drop + titlePattern: split|fee|remainder|share + - id: dilution-cut-order + path: settlement/fees.go + failureModePattern: cap|order|before|under|rate|incorrect + titlePattern: platform|cut|cap|rate + - id: dilution-refund-gross + path: settlement/fees.go + failureModePattern: refund|gross|net|fee|over.?pay|double + titlePattern: refund|gross|net +should_not_find: + - id: dilution-rate-guard-control + path: settlement/fees.go + titlePattern: Rule[0-9]+ diff --git a/evals/packet-dilution/plans/frozen.json b/evals/packet-dilution/plans/frozen.json new file mode 100644 index 0000000..3beaf25 --- /dev/null +++ b/evals/packet-dilution/plans/frozen.json @@ -0,0 +1,164 @@ +{ + "schemaVersion": 1, + "baseSha": "*", + "headSha": "*", + "planSha256": "52e62561b47bfa7ea5cdb59c7bf8a193a9183b01109c7ea644394f2d1f704dda", + "plan": { + "diffUnderstanding": { + "declaredIntent": "authored pinned plan for the packet-size recall curve", + "inferredBehavior": "uniform coverage so packet size is the only variable across arms" + }, + "coverage": [ + { + "hunkId": "d6df72ba", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "175c79f4", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "68109ebd", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "1ff3f41e", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "a8071418", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "53cf00c2", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "ffd09816", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "c9acf88b", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "18197703", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "bbacb370", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "a0be9fac", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "6ae88c80", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "b6e69463", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "d726dbb6", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + }, + { + "hunkId": "fdc5646d", + "path": "limits/limits.go", + "coverage": "normal", + "lenses": [ + "lang/go" + ], + "surroundingContextHints": [], + "reason": "authored uniform coverage" + } + ] + } +} diff --git a/evals/packet-dilution/repos/dilution/base/go.mod b/evals/packet-dilution/repos/dilution/base/go.mod new file mode 100644 index 0000000..9582519 --- /dev/null +++ b/evals/packet-dilution/repos/dilution/base/go.mod @@ -0,0 +1,3 @@ +module dilution + +go 1.23 diff --git a/evals/packet-dilution/repos/dilution/base/limits/limits.go b/evals/packet-dilution/repos/dilution/base/limits/limits.go new file mode 100644 index 0000000..031f784 --- /dev/null +++ b/evals/packet-dilution/repos/dilution/base/limits/limits.go @@ -0,0 +1,934 @@ +package limits + +// guard 1 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 1. +// spacing line 2 for guard 1. +// spacing line 3 for guard 1. +// spacing line 4 for guard 1. +// spacing line 5 for guard 1. +// spacing line 6 for guard 1. +// spacing line 7 for guard 1. +// spacing line 8 for guard 1. +// spacing line 9 for guard 1. +// spacing line 10 for guard 1. +// spacing line 11 for guard 1. +// spacing line 12 for guard 1. +// spacing line 13 for guard 1. +// spacing line 14 for guard 1. +// spacing line 15 for guard 1. +// spacing line 16 for guard 1. +// spacing line 17 for guard 1. +// spacing line 18 for guard 1. +// spacing line 19 for guard 1. +// spacing line 20 for guard 1. +// spacing line 21 for guard 1. +// spacing line 22 for guard 1. +// spacing line 23 for guard 1. +// spacing line 24 for guard 1. +// spacing line 25 for guard 1. +// spacing line 26 for guard 1 +// spacing line 27 for guard 1 +// spacing line 28 for guard 1 +// spacing line 29 for guard 1 +// spacing line 30 for guard 1 +// spacing line 31 for guard 1 +// spacing line 32 for guard 1 +// spacing line 33 for guard 1 +// spacing line 34 for guard 1 +// spacing line 35 for guard 1 +// spacing line 36 for guard 1 +// spacing line 37 for guard 1 +// spacing line 38 for guard 1 +// spacing line 39 for guard 1 +// spacing line 40 for guard 1 +// spacing line 41 for guard 1 +// spacing line 42 for guard 1 +// spacing line 43 for guard 1 +// spacing line 44 for guard 1 +// spacing line 45 for guard 1 +// spacing line 46 for guard 1 +// spacing line 47 for guard 1 +// spacing line 48 for guard 1 +// spacing line 49 for guard 1 +// spacing line 50 for guard 1 +// spacing line 51 for guard 1 +// spacing line 52 for guard 1 +// spacing line 53 for guard 1 +// spacing line 54 for guard 1 +// spacing line 55 for guard 1 + +// WithinTransferLimit reports whether amount may be transferred. +func WithinTransferLimit(amount, limit int64) bool { + return amount <= limit +} + +// guard 2 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 2. +// spacing line 2 for guard 2. +// spacing line 3 for guard 2. +// spacing line 4 for guard 2. +// spacing line 5 for guard 2. +// spacing line 6 for guard 2. +// spacing line 7 for guard 2. +// spacing line 8 for guard 2. +// spacing line 9 for guard 2. +// spacing line 10 for guard 2. +// spacing line 11 for guard 2. +// spacing line 12 for guard 2. +// spacing line 13 for guard 2. +// spacing line 14 for guard 2. +// spacing line 15 for guard 2. +// spacing line 16 for guard 2. +// spacing line 17 for guard 2. +// spacing line 18 for guard 2. +// spacing line 19 for guard 2. +// spacing line 20 for guard 2. +// spacing line 21 for guard 2. +// spacing line 22 for guard 2. +// spacing line 23 for guard 2. +// spacing line 24 for guard 2. +// spacing line 25 for guard 2. +// spacing line 26 for guard 2 +// spacing line 27 for guard 2 +// spacing line 28 for guard 2 +// spacing line 29 for guard 2 +// spacing line 30 for guard 2 +// spacing line 31 for guard 2 +// spacing line 32 for guard 2 +// spacing line 33 for guard 2 +// spacing line 34 for guard 2 +// spacing line 35 for guard 2 +// spacing line 36 for guard 2 +// spacing line 37 for guard 2 +// spacing line 38 for guard 2 +// spacing line 39 for guard 2 +// spacing line 40 for guard 2 +// spacing line 41 for guard 2 +// spacing line 42 for guard 2 +// spacing line 43 for guard 2 +// spacing line 44 for guard 2 +// spacing line 45 for guard 2 +// spacing line 46 for guard 2 +// spacing line 47 for guard 2 +// spacing line 48 for guard 2 +// spacing line 49 for guard 2 +// spacing line 50 for guard 2 +// spacing line 51 for guard 2 +// spacing line 52 for guard 2 +// spacing line 53 for guard 2 +// spacing line 54 for guard 2 +// spacing line 55 for guard 2 + +// Guard2 validates request field 2. +func Guard2(value int) bool { + return value >= 2 +} + +// guard 3 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 3. +// spacing line 2 for guard 3. +// spacing line 3 for guard 3. +// spacing line 4 for guard 3. +// spacing line 5 for guard 3. +// spacing line 6 for guard 3. +// spacing line 7 for guard 3. +// spacing line 8 for guard 3. +// spacing line 9 for guard 3. +// spacing line 10 for guard 3. +// spacing line 11 for guard 3. +// spacing line 12 for guard 3. +// spacing line 13 for guard 3. +// spacing line 14 for guard 3. +// spacing line 15 for guard 3. +// spacing line 16 for guard 3. +// spacing line 17 for guard 3. +// spacing line 18 for guard 3. +// spacing line 19 for guard 3. +// spacing line 20 for guard 3. +// spacing line 21 for guard 3. +// spacing line 22 for guard 3. +// spacing line 23 for guard 3. +// spacing line 24 for guard 3. +// spacing line 25 for guard 3. +// spacing line 26 for guard 3 +// spacing line 27 for guard 3 +// spacing line 28 for guard 3 +// spacing line 29 for guard 3 +// spacing line 30 for guard 3 +// spacing line 31 for guard 3 +// spacing line 32 for guard 3 +// spacing line 33 for guard 3 +// spacing line 34 for guard 3 +// spacing line 35 for guard 3 +// spacing line 36 for guard 3 +// spacing line 37 for guard 3 +// spacing line 38 for guard 3 +// spacing line 39 for guard 3 +// spacing line 40 for guard 3 +// spacing line 41 for guard 3 +// spacing line 42 for guard 3 +// spacing line 43 for guard 3 +// spacing line 44 for guard 3 +// spacing line 45 for guard 3 +// spacing line 46 for guard 3 +// spacing line 47 for guard 3 +// spacing line 48 for guard 3 +// spacing line 49 for guard 3 +// spacing line 50 for guard 3 +// spacing line 51 for guard 3 +// spacing line 52 for guard 3 +// spacing line 53 for guard 3 +// spacing line 54 for guard 3 +// spacing line 55 for guard 3 + +// Guard3 validates request field 3. +func Guard3(value int) bool { + return value >= 3 +} + +// guard 4 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 4. +// spacing line 2 for guard 4. +// spacing line 3 for guard 4. +// spacing line 4 for guard 4. +// spacing line 5 for guard 4. +// spacing line 6 for guard 4. +// spacing line 7 for guard 4. +// spacing line 8 for guard 4. +// spacing line 9 for guard 4. +// spacing line 10 for guard 4. +// spacing line 11 for guard 4. +// spacing line 12 for guard 4. +// spacing line 13 for guard 4. +// spacing line 14 for guard 4. +// spacing line 15 for guard 4. +// spacing line 16 for guard 4. +// spacing line 17 for guard 4. +// spacing line 18 for guard 4. +// spacing line 19 for guard 4. +// spacing line 20 for guard 4. +// spacing line 21 for guard 4. +// spacing line 22 for guard 4. +// spacing line 23 for guard 4. +// spacing line 24 for guard 4. +// spacing line 25 for guard 4. +// spacing line 26 for guard 4 +// spacing line 27 for guard 4 +// spacing line 28 for guard 4 +// spacing line 29 for guard 4 +// spacing line 30 for guard 4 +// spacing line 31 for guard 4 +// spacing line 32 for guard 4 +// spacing line 33 for guard 4 +// spacing line 34 for guard 4 +// spacing line 35 for guard 4 +// spacing line 36 for guard 4 +// spacing line 37 for guard 4 +// spacing line 38 for guard 4 +// spacing line 39 for guard 4 +// spacing line 40 for guard 4 +// spacing line 41 for guard 4 +// spacing line 42 for guard 4 +// spacing line 43 for guard 4 +// spacing line 44 for guard 4 +// spacing line 45 for guard 4 +// spacing line 46 for guard 4 +// spacing line 47 for guard 4 +// spacing line 48 for guard 4 +// spacing line 49 for guard 4 +// spacing line 50 for guard 4 +// spacing line 51 for guard 4 +// spacing line 52 for guard 4 +// spacing line 53 for guard 4 +// spacing line 54 for guard 4 +// spacing line 55 for guard 4 + +// Guard4 validates request field 4. +func Guard4(value int) bool { + return value >= 4 +} + +// guard 5 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 5. +// spacing line 2 for guard 5. +// spacing line 3 for guard 5. +// spacing line 4 for guard 5. +// spacing line 5 for guard 5. +// spacing line 6 for guard 5. +// spacing line 7 for guard 5. +// spacing line 8 for guard 5. +// spacing line 9 for guard 5. +// spacing line 10 for guard 5. +// spacing line 11 for guard 5. +// spacing line 12 for guard 5. +// spacing line 13 for guard 5. +// spacing line 14 for guard 5. +// spacing line 15 for guard 5. +// spacing line 16 for guard 5. +// spacing line 17 for guard 5. +// spacing line 18 for guard 5. +// spacing line 19 for guard 5. +// spacing line 20 for guard 5. +// spacing line 21 for guard 5. +// spacing line 22 for guard 5. +// spacing line 23 for guard 5. +// spacing line 24 for guard 5. +// spacing line 25 for guard 5. +// spacing line 26 for guard 5 +// spacing line 27 for guard 5 +// spacing line 28 for guard 5 +// spacing line 29 for guard 5 +// spacing line 30 for guard 5 +// spacing line 31 for guard 5 +// spacing line 32 for guard 5 +// spacing line 33 for guard 5 +// spacing line 34 for guard 5 +// spacing line 35 for guard 5 +// spacing line 36 for guard 5 +// spacing line 37 for guard 5 +// spacing line 38 for guard 5 +// spacing line 39 for guard 5 +// spacing line 40 for guard 5 +// spacing line 41 for guard 5 +// spacing line 42 for guard 5 +// spacing line 43 for guard 5 +// spacing line 44 for guard 5 +// spacing line 45 for guard 5 +// spacing line 46 for guard 5 +// spacing line 47 for guard 5 +// spacing line 48 for guard 5 +// spacing line 49 for guard 5 +// spacing line 50 for guard 5 +// spacing line 51 for guard 5 +// spacing line 52 for guard 5 +// spacing line 53 for guard 5 +// spacing line 54 for guard 5 +// spacing line 55 for guard 5 + +// Guard5 validates request field 5. +func Guard5(value int) bool { + return value >= 5 +} + +// guard 6 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 6. +// spacing line 2 for guard 6. +// spacing line 3 for guard 6. +// spacing line 4 for guard 6. +// spacing line 5 for guard 6. +// spacing line 6 for guard 6. +// spacing line 7 for guard 6. +// spacing line 8 for guard 6. +// spacing line 9 for guard 6. +// spacing line 10 for guard 6. +// spacing line 11 for guard 6. +// spacing line 12 for guard 6. +// spacing line 13 for guard 6. +// spacing line 14 for guard 6. +// spacing line 15 for guard 6. +// spacing line 16 for guard 6. +// spacing line 17 for guard 6. +// spacing line 18 for guard 6. +// spacing line 19 for guard 6. +// spacing line 20 for guard 6. +// spacing line 21 for guard 6. +// spacing line 22 for guard 6. +// spacing line 23 for guard 6. +// spacing line 24 for guard 6. +// spacing line 25 for guard 6. +// spacing line 26 for guard 6 +// spacing line 27 for guard 6 +// spacing line 28 for guard 6 +// spacing line 29 for guard 6 +// spacing line 30 for guard 6 +// spacing line 31 for guard 6 +// spacing line 32 for guard 6 +// spacing line 33 for guard 6 +// spacing line 34 for guard 6 +// spacing line 35 for guard 6 +// spacing line 36 for guard 6 +// spacing line 37 for guard 6 +// spacing line 38 for guard 6 +// spacing line 39 for guard 6 +// spacing line 40 for guard 6 +// spacing line 41 for guard 6 +// spacing line 42 for guard 6 +// spacing line 43 for guard 6 +// spacing line 44 for guard 6 +// spacing line 45 for guard 6 +// spacing line 46 for guard 6 +// spacing line 47 for guard 6 +// spacing line 48 for guard 6 +// spacing line 49 for guard 6 +// spacing line 50 for guard 6 +// spacing line 51 for guard 6 +// spacing line 52 for guard 6 +// spacing line 53 for guard 6 +// spacing line 54 for guard 6 +// spacing line 55 for guard 6 + +// Guard6 validates request field 6. +func Guard6(value int) bool { + return value >= 6 +} + +// guard 7 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 7. +// spacing line 2 for guard 7. +// spacing line 3 for guard 7. +// spacing line 4 for guard 7. +// spacing line 5 for guard 7. +// spacing line 6 for guard 7. +// spacing line 7 for guard 7. +// spacing line 8 for guard 7. +// spacing line 9 for guard 7. +// spacing line 10 for guard 7. +// spacing line 11 for guard 7. +// spacing line 12 for guard 7. +// spacing line 13 for guard 7. +// spacing line 14 for guard 7. +// spacing line 15 for guard 7. +// spacing line 16 for guard 7. +// spacing line 17 for guard 7. +// spacing line 18 for guard 7. +// spacing line 19 for guard 7. +// spacing line 20 for guard 7. +// spacing line 21 for guard 7. +// spacing line 22 for guard 7. +// spacing line 23 for guard 7. +// spacing line 24 for guard 7. +// spacing line 25 for guard 7. +// spacing line 26 for guard 7 +// spacing line 27 for guard 7 +// spacing line 28 for guard 7 +// spacing line 29 for guard 7 +// spacing line 30 for guard 7 +// spacing line 31 for guard 7 +// spacing line 32 for guard 7 +// spacing line 33 for guard 7 +// spacing line 34 for guard 7 +// spacing line 35 for guard 7 +// spacing line 36 for guard 7 +// spacing line 37 for guard 7 +// spacing line 38 for guard 7 +// spacing line 39 for guard 7 +// spacing line 40 for guard 7 +// spacing line 41 for guard 7 +// spacing line 42 for guard 7 +// spacing line 43 for guard 7 +// spacing line 44 for guard 7 +// spacing line 45 for guard 7 +// spacing line 46 for guard 7 +// spacing line 47 for guard 7 +// spacing line 48 for guard 7 +// spacing line 49 for guard 7 +// spacing line 50 for guard 7 +// spacing line 51 for guard 7 +// spacing line 52 for guard 7 +// spacing line 53 for guard 7 +// spacing line 54 for guard 7 +// spacing line 55 for guard 7 + +// AtCapacity reports whether the queue is full. +func AtCapacity(used, capacity int) bool { + return used >= capacity +} + +// guard 8 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 8. +// spacing line 2 for guard 8. +// spacing line 3 for guard 8. +// spacing line 4 for guard 8. +// spacing line 5 for guard 8. +// spacing line 6 for guard 8. +// spacing line 7 for guard 8. +// spacing line 8 for guard 8. +// spacing line 9 for guard 8. +// spacing line 10 for guard 8. +// spacing line 11 for guard 8. +// spacing line 12 for guard 8. +// spacing line 13 for guard 8. +// spacing line 14 for guard 8. +// spacing line 15 for guard 8. +// spacing line 16 for guard 8. +// spacing line 17 for guard 8. +// spacing line 18 for guard 8. +// spacing line 19 for guard 8. +// spacing line 20 for guard 8. +// spacing line 21 for guard 8. +// spacing line 22 for guard 8. +// spacing line 23 for guard 8. +// spacing line 24 for guard 8. +// spacing line 25 for guard 8. +// spacing line 26 for guard 8 +// spacing line 27 for guard 8 +// spacing line 28 for guard 8 +// spacing line 29 for guard 8 +// spacing line 30 for guard 8 +// spacing line 31 for guard 8 +// spacing line 32 for guard 8 +// spacing line 33 for guard 8 +// spacing line 34 for guard 8 +// spacing line 35 for guard 8 +// spacing line 36 for guard 8 +// spacing line 37 for guard 8 +// spacing line 38 for guard 8 +// spacing line 39 for guard 8 +// spacing line 40 for guard 8 +// spacing line 41 for guard 8 +// spacing line 42 for guard 8 +// spacing line 43 for guard 8 +// spacing line 44 for guard 8 +// spacing line 45 for guard 8 +// spacing line 46 for guard 8 +// spacing line 47 for guard 8 +// spacing line 48 for guard 8 +// spacing line 49 for guard 8 +// spacing line 50 for guard 8 +// spacing line 51 for guard 8 +// spacing line 52 for guard 8 +// spacing line 53 for guard 8 +// spacing line 54 for guard 8 +// spacing line 55 for guard 8 + +// Guard8 validates request field 8. +func Guard8(value int) bool { + return value >= 8 +} + +// guard 9 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 9. +// spacing line 2 for guard 9. +// spacing line 3 for guard 9. +// spacing line 4 for guard 9. +// spacing line 5 for guard 9. +// spacing line 6 for guard 9. +// spacing line 7 for guard 9. +// spacing line 8 for guard 9. +// spacing line 9 for guard 9. +// spacing line 10 for guard 9. +// spacing line 11 for guard 9. +// spacing line 12 for guard 9. +// spacing line 13 for guard 9. +// spacing line 14 for guard 9. +// spacing line 15 for guard 9. +// spacing line 16 for guard 9. +// spacing line 17 for guard 9. +// spacing line 18 for guard 9. +// spacing line 19 for guard 9. +// spacing line 20 for guard 9. +// spacing line 21 for guard 9. +// spacing line 22 for guard 9. +// spacing line 23 for guard 9. +// spacing line 24 for guard 9. +// spacing line 25 for guard 9. +// spacing line 26 for guard 9 +// spacing line 27 for guard 9 +// spacing line 28 for guard 9 +// spacing line 29 for guard 9 +// spacing line 30 for guard 9 +// spacing line 31 for guard 9 +// spacing line 32 for guard 9 +// spacing line 33 for guard 9 +// spacing line 34 for guard 9 +// spacing line 35 for guard 9 +// spacing line 36 for guard 9 +// spacing line 37 for guard 9 +// spacing line 38 for guard 9 +// spacing line 39 for guard 9 +// spacing line 40 for guard 9 +// spacing line 41 for guard 9 +// spacing line 42 for guard 9 +// spacing line 43 for guard 9 +// spacing line 44 for guard 9 +// spacing line 45 for guard 9 +// spacing line 46 for guard 9 +// spacing line 47 for guard 9 +// spacing line 48 for guard 9 +// spacing line 49 for guard 9 +// spacing line 50 for guard 9 +// spacing line 51 for guard 9 +// spacing line 52 for guard 9 +// spacing line 53 for guard 9 +// spacing line 54 for guard 9 +// spacing line 55 for guard 9 + +// Guard9 validates request field 9. +func Guard9(value int) bool { + return value >= 9 +} + +// guard 10 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 10. +// spacing line 2 for guard 10. +// spacing line 3 for guard 10. +// spacing line 4 for guard 10. +// spacing line 5 for guard 10. +// spacing line 6 for guard 10. +// spacing line 7 for guard 10. +// spacing line 8 for guard 10. +// spacing line 9 for guard 10. +// spacing line 10 for guard 10. +// spacing line 11 for guard 10. +// spacing line 12 for guard 10. +// spacing line 13 for guard 10. +// spacing line 14 for guard 10. +// spacing line 15 for guard 10. +// spacing line 16 for guard 10. +// spacing line 17 for guard 10. +// spacing line 18 for guard 10. +// spacing line 19 for guard 10. +// spacing line 20 for guard 10. +// spacing line 21 for guard 10. +// spacing line 22 for guard 10. +// spacing line 23 for guard 10. +// spacing line 24 for guard 10. +// spacing line 25 for guard 10. +// spacing line 26 for guard 10 +// spacing line 27 for guard 10 +// spacing line 28 for guard 10 +// spacing line 29 for guard 10 +// spacing line 30 for guard 10 +// spacing line 31 for guard 10 +// spacing line 32 for guard 10 +// spacing line 33 for guard 10 +// spacing line 34 for guard 10 +// spacing line 35 for guard 10 +// spacing line 36 for guard 10 +// spacing line 37 for guard 10 +// spacing line 38 for guard 10 +// spacing line 39 for guard 10 +// spacing line 40 for guard 10 +// spacing line 41 for guard 10 +// spacing line 42 for guard 10 +// spacing line 43 for guard 10 +// spacing line 44 for guard 10 +// spacing line 45 for guard 10 +// spacing line 46 for guard 10 +// spacing line 47 for guard 10 +// spacing line 48 for guard 10 +// spacing line 49 for guard 10 +// spacing line 50 for guard 10 +// spacing line 51 for guard 10 +// spacing line 52 for guard 10 +// spacing line 53 for guard 10 +// spacing line 54 for guard 10 +// spacing line 55 for guard 10 + +// ShouldRetry reports whether another attempt is allowed. +func ShouldRetry(attempt, maxAttempts int) bool { + return attempt < maxAttempts +} + +// guard 11 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 11. +// spacing line 2 for guard 11. +// spacing line 3 for guard 11. +// spacing line 4 for guard 11. +// spacing line 5 for guard 11. +// spacing line 6 for guard 11. +// spacing line 7 for guard 11. +// spacing line 8 for guard 11. +// spacing line 9 for guard 11. +// spacing line 10 for guard 11. +// spacing line 11 for guard 11. +// spacing line 12 for guard 11. +// spacing line 13 for guard 11. +// spacing line 14 for guard 11. +// spacing line 15 for guard 11. +// spacing line 16 for guard 11. +// spacing line 17 for guard 11. +// spacing line 18 for guard 11. +// spacing line 19 for guard 11. +// spacing line 20 for guard 11. +// spacing line 21 for guard 11. +// spacing line 22 for guard 11. +// spacing line 23 for guard 11. +// spacing line 24 for guard 11. +// spacing line 25 for guard 11. +// spacing line 26 for guard 11 +// spacing line 27 for guard 11 +// spacing line 28 for guard 11 +// spacing line 29 for guard 11 +// spacing line 30 for guard 11 +// spacing line 31 for guard 11 +// spacing line 32 for guard 11 +// spacing line 33 for guard 11 +// spacing line 34 for guard 11 +// spacing line 35 for guard 11 +// spacing line 36 for guard 11 +// spacing line 37 for guard 11 +// spacing line 38 for guard 11 +// spacing line 39 for guard 11 +// spacing line 40 for guard 11 +// spacing line 41 for guard 11 +// spacing line 42 for guard 11 +// spacing line 43 for guard 11 +// spacing line 44 for guard 11 +// spacing line 45 for guard 11 +// spacing line 46 for guard 11 +// spacing line 47 for guard 11 +// spacing line 48 for guard 11 +// spacing line 49 for guard 11 +// spacing line 50 for guard 11 +// spacing line 51 for guard 11 +// spacing line 52 for guard 11 +// spacing line 53 for guard 11 +// spacing line 54 for guard 11 +// spacing line 55 for guard 11 + +// Guard11 validates request field 11. +func Guard11(value int) bool { + return value >= 11 +} + +// guard 12 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 12. +// spacing line 2 for guard 12. +// spacing line 3 for guard 12. +// spacing line 4 for guard 12. +// spacing line 5 for guard 12. +// spacing line 6 for guard 12. +// spacing line 7 for guard 12. +// spacing line 8 for guard 12. +// spacing line 9 for guard 12. +// spacing line 10 for guard 12. +// spacing line 11 for guard 12. +// spacing line 12 for guard 12. +// spacing line 13 for guard 12. +// spacing line 14 for guard 12. +// spacing line 15 for guard 12. +// spacing line 16 for guard 12. +// spacing line 17 for guard 12. +// spacing line 18 for guard 12. +// spacing line 19 for guard 12. +// spacing line 20 for guard 12. +// spacing line 21 for guard 12. +// spacing line 22 for guard 12. +// spacing line 23 for guard 12. +// spacing line 24 for guard 12. +// spacing line 25 for guard 12. +// spacing line 26 for guard 12 +// spacing line 27 for guard 12 +// spacing line 28 for guard 12 +// spacing line 29 for guard 12 +// spacing line 30 for guard 12 +// spacing line 31 for guard 12 +// spacing line 32 for guard 12 +// spacing line 33 for guard 12 +// spacing line 34 for guard 12 +// spacing line 35 for guard 12 +// spacing line 36 for guard 12 +// spacing line 37 for guard 12 +// spacing line 38 for guard 12 +// spacing line 39 for guard 12 +// spacing line 40 for guard 12 +// spacing line 41 for guard 12 +// spacing line 42 for guard 12 +// spacing line 43 for guard 12 +// spacing line 44 for guard 12 +// spacing line 45 for guard 12 +// spacing line 46 for guard 12 +// spacing line 47 for guard 12 +// spacing line 48 for guard 12 +// spacing line 49 for guard 12 +// spacing line 50 for guard 12 +// spacing line 51 for guard 12 +// spacing line 52 for guard 12 +// spacing line 53 for guard 12 +// spacing line 54 for guard 12 +// spacing line 55 for guard 12 + +// Guard12 validates request field 12. +func Guard12(value int) bool { + return value >= 12 +} + +// guard 13 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 13. +// spacing line 2 for guard 13. +// spacing line 3 for guard 13. +// spacing line 4 for guard 13. +// spacing line 5 for guard 13. +// spacing line 6 for guard 13. +// spacing line 7 for guard 13. +// spacing line 8 for guard 13. +// spacing line 9 for guard 13. +// spacing line 10 for guard 13. +// spacing line 11 for guard 13. +// spacing line 12 for guard 13. +// spacing line 13 for guard 13. +// spacing line 14 for guard 13. +// spacing line 15 for guard 13. +// spacing line 16 for guard 13. +// spacing line 17 for guard 13. +// spacing line 18 for guard 13. +// spacing line 19 for guard 13. +// spacing line 20 for guard 13. +// spacing line 21 for guard 13. +// spacing line 22 for guard 13. +// spacing line 23 for guard 13. +// spacing line 24 for guard 13. +// spacing line 25 for guard 13. +// spacing line 26 for guard 13 +// spacing line 27 for guard 13 +// spacing line 28 for guard 13 +// spacing line 29 for guard 13 +// spacing line 30 for guard 13 +// spacing line 31 for guard 13 +// spacing line 32 for guard 13 +// spacing line 33 for guard 13 +// spacing line 34 for guard 13 +// spacing line 35 for guard 13 +// spacing line 36 for guard 13 +// spacing line 37 for guard 13 +// spacing line 38 for guard 13 +// spacing line 39 for guard 13 +// spacing line 40 for guard 13 +// spacing line 41 for guard 13 +// spacing line 42 for guard 13 +// spacing line 43 for guard 13 +// spacing line 44 for guard 13 +// spacing line 45 for guard 13 +// spacing line 46 for guard 13 +// spacing line 47 for guard 13 +// spacing line 48 for guard 13 +// spacing line 49 for guard 13 +// spacing line 50 for guard 13 +// spacing line 51 for guard 13 +// spacing line 52 for guard 13 +// spacing line 53 for guard 13 +// spacing line 54 for guard 13 +// spacing line 55 for guard 13 + +// ShardIndex maps a key onto the configured shard range. +func ShardIndex(key, shards int) int { + if shards <= 0 { + return 0 + } + return key % shards +} + +// guard 14 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 14. +// spacing line 2 for guard 14. +// spacing line 3 for guard 14. +// spacing line 4 for guard 14. +// spacing line 5 for guard 14. +// spacing line 6 for guard 14. +// spacing line 7 for guard 14. +// spacing line 8 for guard 14. +// spacing line 9 for guard 14. +// spacing line 10 for guard 14. +// spacing line 11 for guard 14. +// spacing line 12 for guard 14. +// spacing line 13 for guard 14. +// spacing line 14 for guard 14. +// spacing line 15 for guard 14. +// spacing line 16 for guard 14. +// spacing line 17 for guard 14. +// spacing line 18 for guard 14. +// spacing line 19 for guard 14. +// spacing line 20 for guard 14. +// spacing line 21 for guard 14. +// spacing line 22 for guard 14. +// spacing line 23 for guard 14. +// spacing line 24 for guard 14. +// spacing line 25 for guard 14. +// spacing line 26 for guard 14 +// spacing line 27 for guard 14 +// spacing line 28 for guard 14 +// spacing line 29 for guard 14 +// spacing line 30 for guard 14 +// spacing line 31 for guard 14 +// spacing line 32 for guard 14 +// spacing line 33 for guard 14 +// spacing line 34 for guard 14 +// spacing line 35 for guard 14 +// spacing line 36 for guard 14 +// spacing line 37 for guard 14 +// spacing line 38 for guard 14 +// spacing line 39 for guard 14 +// spacing line 40 for guard 14 +// spacing line 41 for guard 14 +// spacing line 42 for guard 14 +// spacing line 43 for guard 14 +// spacing line 44 for guard 14 +// spacing line 45 for guard 14 +// spacing line 46 for guard 14 +// spacing line 47 for guard 14 +// spacing line 48 for guard 14 +// spacing line 49 for guard 14 +// spacing line 50 for guard 14 +// spacing line 51 for guard 14 +// spacing line 52 for guard 14 +// spacing line 53 for guard 14 +// spacing line 54 for guard 14 +// spacing line 55 for guard 14 + +// Guard14 validates request field 14. +func Guard14(value int) bool { + return value >= 14 +} + +// guard 15 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 15. +// spacing line 2 for guard 15. +// spacing line 3 for guard 15. +// spacing line 4 for guard 15. +// spacing line 5 for guard 15. +// spacing line 6 for guard 15. +// spacing line 7 for guard 15. +// spacing line 8 for guard 15. +// spacing line 9 for guard 15. +// spacing line 10 for guard 15. +// spacing line 11 for guard 15. +// spacing line 12 for guard 15. +// spacing line 13 for guard 15. +// spacing line 14 for guard 15. +// spacing line 15 for guard 15. +// spacing line 16 for guard 15. +// spacing line 17 for guard 15. +// spacing line 18 for guard 15. +// spacing line 19 for guard 15. +// spacing line 20 for guard 15. +// spacing line 21 for guard 15. +// spacing line 22 for guard 15. +// spacing line 23 for guard 15. +// spacing line 24 for guard 15. +// spacing line 25 for guard 15. +// spacing line 26 for guard 15 +// spacing line 27 for guard 15 +// spacing line 28 for guard 15 +// spacing line 29 for guard 15 +// spacing line 30 for guard 15 +// spacing line 31 for guard 15 +// spacing line 32 for guard 15 +// spacing line 33 for guard 15 +// spacing line 34 for guard 15 +// spacing line 35 for guard 15 +// spacing line 36 for guard 15 +// spacing line 37 for guard 15 +// spacing line 38 for guard 15 +// spacing line 39 for guard 15 +// spacing line 40 for guard 15 +// spacing line 41 for guard 15 +// spacing line 42 for guard 15 +// spacing line 43 for guard 15 +// spacing line 44 for guard 15 +// spacing line 45 for guard 15 +// spacing line 46 for guard 15 +// spacing line 47 for guard 15 +// spacing line 48 for guard 15 +// spacing line 49 for guard 15 +// spacing line 50 for guard 15 +// spacing line 51 for guard 15 +// spacing line 52 for guard 15 +// spacing line 53 for guard 15 +// spacing line 54 for guard 15 +// spacing line 55 for guard 15 + +// Guard15 validates request field 15. +func Guard15(value int) bool { + return value >= 15 +} diff --git a/evals/packet-dilution/repos/dilution/feature/go.mod b/evals/packet-dilution/repos/dilution/feature/go.mod new file mode 100644 index 0000000..9582519 --- /dev/null +++ b/evals/packet-dilution/repos/dilution/feature/go.mod @@ -0,0 +1,3 @@ +module dilution + +go 1.23 diff --git a/evals/packet-dilution/repos/dilution/feature/limits/limits.go b/evals/packet-dilution/repos/dilution/feature/limits/limits.go new file mode 100644 index 0000000..452c316 --- /dev/null +++ b/evals/packet-dilution/repos/dilution/feature/limits/limits.go @@ -0,0 +1,934 @@ +package limits + +// guard 1 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 1. +// spacing line 2 for guard 1. +// spacing line 3 for guard 1. +// spacing line 4 for guard 1. +// spacing line 5 for guard 1. +// spacing line 6 for guard 1. +// spacing line 7 for guard 1. +// spacing line 8 for guard 1. +// spacing line 9 for guard 1. +// spacing line 10 for guard 1. +// spacing line 11 for guard 1. +// spacing line 12 for guard 1. +// spacing line 13 for guard 1. +// spacing line 14 for guard 1. +// spacing line 15 for guard 1. +// spacing line 16 for guard 1. +// spacing line 17 for guard 1. +// spacing line 18 for guard 1. +// spacing line 19 for guard 1. +// spacing line 20 for guard 1. +// spacing line 21 for guard 1. +// spacing line 22 for guard 1. +// spacing line 23 for guard 1. +// spacing line 24 for guard 1. +// spacing line 25 for guard 1. +// spacing line 26 for guard 1 +// spacing line 27 for guard 1 +// spacing line 28 for guard 1 +// spacing line 29 for guard 1 +// spacing line 30 for guard 1 +// spacing line 31 for guard 1 +// spacing line 32 for guard 1 +// spacing line 33 for guard 1 +// spacing line 34 for guard 1 +// spacing line 35 for guard 1 +// spacing line 36 for guard 1 +// spacing line 37 for guard 1 +// spacing line 38 for guard 1 +// spacing line 39 for guard 1 +// spacing line 40 for guard 1 +// spacing line 41 for guard 1 +// spacing line 42 for guard 1 +// spacing line 43 for guard 1 +// spacing line 44 for guard 1 +// spacing line 45 for guard 1 +// spacing line 46 for guard 1 +// spacing line 47 for guard 1 +// spacing line 48 for guard 1 +// spacing line 49 for guard 1 +// spacing line 50 for guard 1 +// spacing line 51 for guard 1 +// spacing line 52 for guard 1 +// spacing line 53 for guard 1 +// spacing line 54 for guard 1 +// spacing line 55 for guard 1 + +// WithinTransferLimit reports whether amount may be transferred. +func WithinTransferLimit(amount, limit int64) bool { + return amount < limit +} + +// guard 2 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 2. +// spacing line 2 for guard 2. +// spacing line 3 for guard 2. +// spacing line 4 for guard 2. +// spacing line 5 for guard 2. +// spacing line 6 for guard 2. +// spacing line 7 for guard 2. +// spacing line 8 for guard 2. +// spacing line 9 for guard 2. +// spacing line 10 for guard 2. +// spacing line 11 for guard 2. +// spacing line 12 for guard 2. +// spacing line 13 for guard 2. +// spacing line 14 for guard 2. +// spacing line 15 for guard 2. +// spacing line 16 for guard 2. +// spacing line 17 for guard 2. +// spacing line 18 for guard 2. +// spacing line 19 for guard 2. +// spacing line 20 for guard 2. +// spacing line 21 for guard 2. +// spacing line 22 for guard 2. +// spacing line 23 for guard 2. +// spacing line 24 for guard 2. +// spacing line 25 for guard 2. +// spacing line 26 for guard 2 +// spacing line 27 for guard 2 +// spacing line 28 for guard 2 +// spacing line 29 for guard 2 +// spacing line 30 for guard 2 +// spacing line 31 for guard 2 +// spacing line 32 for guard 2 +// spacing line 33 for guard 2 +// spacing line 34 for guard 2 +// spacing line 35 for guard 2 +// spacing line 36 for guard 2 +// spacing line 37 for guard 2 +// spacing line 38 for guard 2 +// spacing line 39 for guard 2 +// spacing line 40 for guard 2 +// spacing line 41 for guard 2 +// spacing line 42 for guard 2 +// spacing line 43 for guard 2 +// spacing line 44 for guard 2 +// spacing line 45 for guard 2 +// spacing line 46 for guard 2 +// spacing line 47 for guard 2 +// spacing line 48 for guard 2 +// spacing line 49 for guard 2 +// spacing line 50 for guard 2 +// spacing line 51 for guard 2 +// spacing line 52 for guard 2 +// spacing line 53 for guard 2 +// spacing line 54 for guard 2 +// spacing line 55 for guard 2 + +// Guard2 validates request field 2. +func Guard2(value int) bool { + return value >= 2 && value < 1_000_002 +} + +// guard 3 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 3. +// spacing line 2 for guard 3. +// spacing line 3 for guard 3. +// spacing line 4 for guard 3. +// spacing line 5 for guard 3. +// spacing line 6 for guard 3. +// spacing line 7 for guard 3. +// spacing line 8 for guard 3. +// spacing line 9 for guard 3. +// spacing line 10 for guard 3. +// spacing line 11 for guard 3. +// spacing line 12 for guard 3. +// spacing line 13 for guard 3. +// spacing line 14 for guard 3. +// spacing line 15 for guard 3. +// spacing line 16 for guard 3. +// spacing line 17 for guard 3. +// spacing line 18 for guard 3. +// spacing line 19 for guard 3. +// spacing line 20 for guard 3. +// spacing line 21 for guard 3. +// spacing line 22 for guard 3. +// spacing line 23 for guard 3. +// spacing line 24 for guard 3. +// spacing line 25 for guard 3. +// spacing line 26 for guard 3 +// spacing line 27 for guard 3 +// spacing line 28 for guard 3 +// spacing line 29 for guard 3 +// spacing line 30 for guard 3 +// spacing line 31 for guard 3 +// spacing line 32 for guard 3 +// spacing line 33 for guard 3 +// spacing line 34 for guard 3 +// spacing line 35 for guard 3 +// spacing line 36 for guard 3 +// spacing line 37 for guard 3 +// spacing line 38 for guard 3 +// spacing line 39 for guard 3 +// spacing line 40 for guard 3 +// spacing line 41 for guard 3 +// spacing line 42 for guard 3 +// spacing line 43 for guard 3 +// spacing line 44 for guard 3 +// spacing line 45 for guard 3 +// spacing line 46 for guard 3 +// spacing line 47 for guard 3 +// spacing line 48 for guard 3 +// spacing line 49 for guard 3 +// spacing line 50 for guard 3 +// spacing line 51 for guard 3 +// spacing line 52 for guard 3 +// spacing line 53 for guard 3 +// spacing line 54 for guard 3 +// spacing line 55 for guard 3 + +// Guard3 validates request field 3. +func Guard3(value int) bool { + return value >= 3 && value < 1_000_003 +} + +// guard 4 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 4. +// spacing line 2 for guard 4. +// spacing line 3 for guard 4. +// spacing line 4 for guard 4. +// spacing line 5 for guard 4. +// spacing line 6 for guard 4. +// spacing line 7 for guard 4. +// spacing line 8 for guard 4. +// spacing line 9 for guard 4. +// spacing line 10 for guard 4. +// spacing line 11 for guard 4. +// spacing line 12 for guard 4. +// spacing line 13 for guard 4. +// spacing line 14 for guard 4. +// spacing line 15 for guard 4. +// spacing line 16 for guard 4. +// spacing line 17 for guard 4. +// spacing line 18 for guard 4. +// spacing line 19 for guard 4. +// spacing line 20 for guard 4. +// spacing line 21 for guard 4. +// spacing line 22 for guard 4. +// spacing line 23 for guard 4. +// spacing line 24 for guard 4. +// spacing line 25 for guard 4. +// spacing line 26 for guard 4 +// spacing line 27 for guard 4 +// spacing line 28 for guard 4 +// spacing line 29 for guard 4 +// spacing line 30 for guard 4 +// spacing line 31 for guard 4 +// spacing line 32 for guard 4 +// spacing line 33 for guard 4 +// spacing line 34 for guard 4 +// spacing line 35 for guard 4 +// spacing line 36 for guard 4 +// spacing line 37 for guard 4 +// spacing line 38 for guard 4 +// spacing line 39 for guard 4 +// spacing line 40 for guard 4 +// spacing line 41 for guard 4 +// spacing line 42 for guard 4 +// spacing line 43 for guard 4 +// spacing line 44 for guard 4 +// spacing line 45 for guard 4 +// spacing line 46 for guard 4 +// spacing line 47 for guard 4 +// spacing line 48 for guard 4 +// spacing line 49 for guard 4 +// spacing line 50 for guard 4 +// spacing line 51 for guard 4 +// spacing line 52 for guard 4 +// spacing line 53 for guard 4 +// spacing line 54 for guard 4 +// spacing line 55 for guard 4 + +// Guard4 validates request field 4. +func Guard4(value int) bool { + return value >= 4 && value < 1_000_004 +} + +// guard 5 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 5. +// spacing line 2 for guard 5. +// spacing line 3 for guard 5. +// spacing line 4 for guard 5. +// spacing line 5 for guard 5. +// spacing line 6 for guard 5. +// spacing line 7 for guard 5. +// spacing line 8 for guard 5. +// spacing line 9 for guard 5. +// spacing line 10 for guard 5. +// spacing line 11 for guard 5. +// spacing line 12 for guard 5. +// spacing line 13 for guard 5. +// spacing line 14 for guard 5. +// spacing line 15 for guard 5. +// spacing line 16 for guard 5. +// spacing line 17 for guard 5. +// spacing line 18 for guard 5. +// spacing line 19 for guard 5. +// spacing line 20 for guard 5. +// spacing line 21 for guard 5. +// spacing line 22 for guard 5. +// spacing line 23 for guard 5. +// spacing line 24 for guard 5. +// spacing line 25 for guard 5. +// spacing line 26 for guard 5 +// spacing line 27 for guard 5 +// spacing line 28 for guard 5 +// spacing line 29 for guard 5 +// spacing line 30 for guard 5 +// spacing line 31 for guard 5 +// spacing line 32 for guard 5 +// spacing line 33 for guard 5 +// spacing line 34 for guard 5 +// spacing line 35 for guard 5 +// spacing line 36 for guard 5 +// spacing line 37 for guard 5 +// spacing line 38 for guard 5 +// spacing line 39 for guard 5 +// spacing line 40 for guard 5 +// spacing line 41 for guard 5 +// spacing line 42 for guard 5 +// spacing line 43 for guard 5 +// spacing line 44 for guard 5 +// spacing line 45 for guard 5 +// spacing line 46 for guard 5 +// spacing line 47 for guard 5 +// spacing line 48 for guard 5 +// spacing line 49 for guard 5 +// spacing line 50 for guard 5 +// spacing line 51 for guard 5 +// spacing line 52 for guard 5 +// spacing line 53 for guard 5 +// spacing line 54 for guard 5 +// spacing line 55 for guard 5 + +// Guard5 validates request field 5. +func Guard5(value int) bool { + return value >= 5 && value < 1_000_005 +} + +// guard 6 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 6. +// spacing line 2 for guard 6. +// spacing line 3 for guard 6. +// spacing line 4 for guard 6. +// spacing line 5 for guard 6. +// spacing line 6 for guard 6. +// spacing line 7 for guard 6. +// spacing line 8 for guard 6. +// spacing line 9 for guard 6. +// spacing line 10 for guard 6. +// spacing line 11 for guard 6. +// spacing line 12 for guard 6. +// spacing line 13 for guard 6. +// spacing line 14 for guard 6. +// spacing line 15 for guard 6. +// spacing line 16 for guard 6. +// spacing line 17 for guard 6. +// spacing line 18 for guard 6. +// spacing line 19 for guard 6. +// spacing line 20 for guard 6. +// spacing line 21 for guard 6. +// spacing line 22 for guard 6. +// spacing line 23 for guard 6. +// spacing line 24 for guard 6. +// spacing line 25 for guard 6. +// spacing line 26 for guard 6 +// spacing line 27 for guard 6 +// spacing line 28 for guard 6 +// spacing line 29 for guard 6 +// spacing line 30 for guard 6 +// spacing line 31 for guard 6 +// spacing line 32 for guard 6 +// spacing line 33 for guard 6 +// spacing line 34 for guard 6 +// spacing line 35 for guard 6 +// spacing line 36 for guard 6 +// spacing line 37 for guard 6 +// spacing line 38 for guard 6 +// spacing line 39 for guard 6 +// spacing line 40 for guard 6 +// spacing line 41 for guard 6 +// spacing line 42 for guard 6 +// spacing line 43 for guard 6 +// spacing line 44 for guard 6 +// spacing line 45 for guard 6 +// spacing line 46 for guard 6 +// spacing line 47 for guard 6 +// spacing line 48 for guard 6 +// spacing line 49 for guard 6 +// spacing line 50 for guard 6 +// spacing line 51 for guard 6 +// spacing line 52 for guard 6 +// spacing line 53 for guard 6 +// spacing line 54 for guard 6 +// spacing line 55 for guard 6 + +// Guard6 validates request field 6. +func Guard6(value int) bool { + return value >= 6 && value < 1_000_006 +} + +// guard 7 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 7. +// spacing line 2 for guard 7. +// spacing line 3 for guard 7. +// spacing line 4 for guard 7. +// spacing line 5 for guard 7. +// spacing line 6 for guard 7. +// spacing line 7 for guard 7. +// spacing line 8 for guard 7. +// spacing line 9 for guard 7. +// spacing line 10 for guard 7. +// spacing line 11 for guard 7. +// spacing line 12 for guard 7. +// spacing line 13 for guard 7. +// spacing line 14 for guard 7. +// spacing line 15 for guard 7. +// spacing line 16 for guard 7. +// spacing line 17 for guard 7. +// spacing line 18 for guard 7. +// spacing line 19 for guard 7. +// spacing line 20 for guard 7. +// spacing line 21 for guard 7. +// spacing line 22 for guard 7. +// spacing line 23 for guard 7. +// spacing line 24 for guard 7. +// spacing line 25 for guard 7. +// spacing line 26 for guard 7 +// spacing line 27 for guard 7 +// spacing line 28 for guard 7 +// spacing line 29 for guard 7 +// spacing line 30 for guard 7 +// spacing line 31 for guard 7 +// spacing line 32 for guard 7 +// spacing line 33 for guard 7 +// spacing line 34 for guard 7 +// spacing line 35 for guard 7 +// spacing line 36 for guard 7 +// spacing line 37 for guard 7 +// spacing line 38 for guard 7 +// spacing line 39 for guard 7 +// spacing line 40 for guard 7 +// spacing line 41 for guard 7 +// spacing line 42 for guard 7 +// spacing line 43 for guard 7 +// spacing line 44 for guard 7 +// spacing line 45 for guard 7 +// spacing line 46 for guard 7 +// spacing line 47 for guard 7 +// spacing line 48 for guard 7 +// spacing line 49 for guard 7 +// spacing line 50 for guard 7 +// spacing line 51 for guard 7 +// spacing line 52 for guard 7 +// spacing line 53 for guard 7 +// spacing line 54 for guard 7 +// spacing line 55 for guard 7 + +// AtCapacity reports whether the queue is full. +func AtCapacity(used, capacity int) bool { + return !(used < capacity) +} + +// guard 8 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 8. +// spacing line 2 for guard 8. +// spacing line 3 for guard 8. +// spacing line 4 for guard 8. +// spacing line 5 for guard 8. +// spacing line 6 for guard 8. +// spacing line 7 for guard 8. +// spacing line 8 for guard 8. +// spacing line 9 for guard 8. +// spacing line 10 for guard 8. +// spacing line 11 for guard 8. +// spacing line 12 for guard 8. +// spacing line 13 for guard 8. +// spacing line 14 for guard 8. +// spacing line 15 for guard 8. +// spacing line 16 for guard 8. +// spacing line 17 for guard 8. +// spacing line 18 for guard 8. +// spacing line 19 for guard 8. +// spacing line 20 for guard 8. +// spacing line 21 for guard 8. +// spacing line 22 for guard 8. +// spacing line 23 for guard 8. +// spacing line 24 for guard 8. +// spacing line 25 for guard 8. +// spacing line 26 for guard 8 +// spacing line 27 for guard 8 +// spacing line 28 for guard 8 +// spacing line 29 for guard 8 +// spacing line 30 for guard 8 +// spacing line 31 for guard 8 +// spacing line 32 for guard 8 +// spacing line 33 for guard 8 +// spacing line 34 for guard 8 +// spacing line 35 for guard 8 +// spacing line 36 for guard 8 +// spacing line 37 for guard 8 +// spacing line 38 for guard 8 +// spacing line 39 for guard 8 +// spacing line 40 for guard 8 +// spacing line 41 for guard 8 +// spacing line 42 for guard 8 +// spacing line 43 for guard 8 +// spacing line 44 for guard 8 +// spacing line 45 for guard 8 +// spacing line 46 for guard 8 +// spacing line 47 for guard 8 +// spacing line 48 for guard 8 +// spacing line 49 for guard 8 +// spacing line 50 for guard 8 +// spacing line 51 for guard 8 +// spacing line 52 for guard 8 +// spacing line 53 for guard 8 +// spacing line 54 for guard 8 +// spacing line 55 for guard 8 + +// Guard8 validates request field 8. +func Guard8(value int) bool { + return value >= 8 && value < 1_000_008 +} + +// guard 9 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 9. +// spacing line 2 for guard 9. +// spacing line 3 for guard 9. +// spacing line 4 for guard 9. +// spacing line 5 for guard 9. +// spacing line 6 for guard 9. +// spacing line 7 for guard 9. +// spacing line 8 for guard 9. +// spacing line 9 for guard 9. +// spacing line 10 for guard 9. +// spacing line 11 for guard 9. +// spacing line 12 for guard 9. +// spacing line 13 for guard 9. +// spacing line 14 for guard 9. +// spacing line 15 for guard 9. +// spacing line 16 for guard 9. +// spacing line 17 for guard 9. +// spacing line 18 for guard 9. +// spacing line 19 for guard 9. +// spacing line 20 for guard 9. +// spacing line 21 for guard 9. +// spacing line 22 for guard 9. +// spacing line 23 for guard 9. +// spacing line 24 for guard 9. +// spacing line 25 for guard 9. +// spacing line 26 for guard 9 +// spacing line 27 for guard 9 +// spacing line 28 for guard 9 +// spacing line 29 for guard 9 +// spacing line 30 for guard 9 +// spacing line 31 for guard 9 +// spacing line 32 for guard 9 +// spacing line 33 for guard 9 +// spacing line 34 for guard 9 +// spacing line 35 for guard 9 +// spacing line 36 for guard 9 +// spacing line 37 for guard 9 +// spacing line 38 for guard 9 +// spacing line 39 for guard 9 +// spacing line 40 for guard 9 +// spacing line 41 for guard 9 +// spacing line 42 for guard 9 +// spacing line 43 for guard 9 +// spacing line 44 for guard 9 +// spacing line 45 for guard 9 +// spacing line 46 for guard 9 +// spacing line 47 for guard 9 +// spacing line 48 for guard 9 +// spacing line 49 for guard 9 +// spacing line 50 for guard 9 +// spacing line 51 for guard 9 +// spacing line 52 for guard 9 +// spacing line 53 for guard 9 +// spacing line 54 for guard 9 +// spacing line 55 for guard 9 + +// Guard9 validates request field 9. +func Guard9(value int) bool { + return value >= 9 && value < 1_000_009 +} + +// guard 10 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 10. +// spacing line 2 for guard 10. +// spacing line 3 for guard 10. +// spacing line 4 for guard 10. +// spacing line 5 for guard 10. +// spacing line 6 for guard 10. +// spacing line 7 for guard 10. +// spacing line 8 for guard 10. +// spacing line 9 for guard 10. +// spacing line 10 for guard 10. +// spacing line 11 for guard 10. +// spacing line 12 for guard 10. +// spacing line 13 for guard 10. +// spacing line 14 for guard 10. +// spacing line 15 for guard 10. +// spacing line 16 for guard 10. +// spacing line 17 for guard 10. +// spacing line 18 for guard 10. +// spacing line 19 for guard 10. +// spacing line 20 for guard 10. +// spacing line 21 for guard 10. +// spacing line 22 for guard 10. +// spacing line 23 for guard 10. +// spacing line 24 for guard 10. +// spacing line 25 for guard 10. +// spacing line 26 for guard 10 +// spacing line 27 for guard 10 +// spacing line 28 for guard 10 +// spacing line 29 for guard 10 +// spacing line 30 for guard 10 +// spacing line 31 for guard 10 +// spacing line 32 for guard 10 +// spacing line 33 for guard 10 +// spacing line 34 for guard 10 +// spacing line 35 for guard 10 +// spacing line 36 for guard 10 +// spacing line 37 for guard 10 +// spacing line 38 for guard 10 +// spacing line 39 for guard 10 +// spacing line 40 for guard 10 +// spacing line 41 for guard 10 +// spacing line 42 for guard 10 +// spacing line 43 for guard 10 +// spacing line 44 for guard 10 +// spacing line 45 for guard 10 +// spacing line 46 for guard 10 +// spacing line 47 for guard 10 +// spacing line 48 for guard 10 +// spacing line 49 for guard 10 +// spacing line 50 for guard 10 +// spacing line 51 for guard 10 +// spacing line 52 for guard 10 +// spacing line 53 for guard 10 +// spacing line 54 for guard 10 +// spacing line 55 for guard 10 + +// ShouldRetry reports whether another attempt is allowed. +func ShouldRetry(attempt, maxAttempts int) bool { + return attempt <= maxAttempts +} + +// guard 11 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 11. +// spacing line 2 for guard 11. +// spacing line 3 for guard 11. +// spacing line 4 for guard 11. +// spacing line 5 for guard 11. +// spacing line 6 for guard 11. +// spacing line 7 for guard 11. +// spacing line 8 for guard 11. +// spacing line 9 for guard 11. +// spacing line 10 for guard 11. +// spacing line 11 for guard 11. +// spacing line 12 for guard 11. +// spacing line 13 for guard 11. +// spacing line 14 for guard 11. +// spacing line 15 for guard 11. +// spacing line 16 for guard 11. +// spacing line 17 for guard 11. +// spacing line 18 for guard 11. +// spacing line 19 for guard 11. +// spacing line 20 for guard 11. +// spacing line 21 for guard 11. +// spacing line 22 for guard 11. +// spacing line 23 for guard 11. +// spacing line 24 for guard 11. +// spacing line 25 for guard 11. +// spacing line 26 for guard 11 +// spacing line 27 for guard 11 +// spacing line 28 for guard 11 +// spacing line 29 for guard 11 +// spacing line 30 for guard 11 +// spacing line 31 for guard 11 +// spacing line 32 for guard 11 +// spacing line 33 for guard 11 +// spacing line 34 for guard 11 +// spacing line 35 for guard 11 +// spacing line 36 for guard 11 +// spacing line 37 for guard 11 +// spacing line 38 for guard 11 +// spacing line 39 for guard 11 +// spacing line 40 for guard 11 +// spacing line 41 for guard 11 +// spacing line 42 for guard 11 +// spacing line 43 for guard 11 +// spacing line 44 for guard 11 +// spacing line 45 for guard 11 +// spacing line 46 for guard 11 +// spacing line 47 for guard 11 +// spacing line 48 for guard 11 +// spacing line 49 for guard 11 +// spacing line 50 for guard 11 +// spacing line 51 for guard 11 +// spacing line 52 for guard 11 +// spacing line 53 for guard 11 +// spacing line 54 for guard 11 +// spacing line 55 for guard 11 + +// Guard11 validates request field 11. +func Guard11(value int) bool { + return value >= 11 && value < 1_000_011 +} + +// guard 12 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 12. +// spacing line 2 for guard 12. +// spacing line 3 for guard 12. +// spacing line 4 for guard 12. +// spacing line 5 for guard 12. +// spacing line 6 for guard 12. +// spacing line 7 for guard 12. +// spacing line 8 for guard 12. +// spacing line 9 for guard 12. +// spacing line 10 for guard 12. +// spacing line 11 for guard 12. +// spacing line 12 for guard 12. +// spacing line 13 for guard 12. +// spacing line 14 for guard 12. +// spacing line 15 for guard 12. +// spacing line 16 for guard 12. +// spacing line 17 for guard 12. +// spacing line 18 for guard 12. +// spacing line 19 for guard 12. +// spacing line 20 for guard 12. +// spacing line 21 for guard 12. +// spacing line 22 for guard 12. +// spacing line 23 for guard 12. +// spacing line 24 for guard 12. +// spacing line 25 for guard 12. +// spacing line 26 for guard 12 +// spacing line 27 for guard 12 +// spacing line 28 for guard 12 +// spacing line 29 for guard 12 +// spacing line 30 for guard 12 +// spacing line 31 for guard 12 +// spacing line 32 for guard 12 +// spacing line 33 for guard 12 +// spacing line 34 for guard 12 +// spacing line 35 for guard 12 +// spacing line 36 for guard 12 +// spacing line 37 for guard 12 +// spacing line 38 for guard 12 +// spacing line 39 for guard 12 +// spacing line 40 for guard 12 +// spacing line 41 for guard 12 +// spacing line 42 for guard 12 +// spacing line 43 for guard 12 +// spacing line 44 for guard 12 +// spacing line 45 for guard 12 +// spacing line 46 for guard 12 +// spacing line 47 for guard 12 +// spacing line 48 for guard 12 +// spacing line 49 for guard 12 +// spacing line 50 for guard 12 +// spacing line 51 for guard 12 +// spacing line 52 for guard 12 +// spacing line 53 for guard 12 +// spacing line 54 for guard 12 +// spacing line 55 for guard 12 + +// Guard12 validates request field 12. +func Guard12(value int) bool { + return value >= 12 && value < 1_000_012 +} + +// guard 13 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 13. +// spacing line 2 for guard 13. +// spacing line 3 for guard 13. +// spacing line 4 for guard 13. +// spacing line 5 for guard 13. +// spacing line 6 for guard 13. +// spacing line 7 for guard 13. +// spacing line 8 for guard 13. +// spacing line 9 for guard 13. +// spacing line 10 for guard 13. +// spacing line 11 for guard 13. +// spacing line 12 for guard 13. +// spacing line 13 for guard 13. +// spacing line 14 for guard 13. +// spacing line 15 for guard 13. +// spacing line 16 for guard 13. +// spacing line 17 for guard 13. +// spacing line 18 for guard 13. +// spacing line 19 for guard 13. +// spacing line 20 for guard 13. +// spacing line 21 for guard 13. +// spacing line 22 for guard 13. +// spacing line 23 for guard 13. +// spacing line 24 for guard 13. +// spacing line 25 for guard 13. +// spacing line 26 for guard 13 +// spacing line 27 for guard 13 +// spacing line 28 for guard 13 +// spacing line 29 for guard 13 +// spacing line 30 for guard 13 +// spacing line 31 for guard 13 +// spacing line 32 for guard 13 +// spacing line 33 for guard 13 +// spacing line 34 for guard 13 +// spacing line 35 for guard 13 +// spacing line 36 for guard 13 +// spacing line 37 for guard 13 +// spacing line 38 for guard 13 +// spacing line 39 for guard 13 +// spacing line 40 for guard 13 +// spacing line 41 for guard 13 +// spacing line 42 for guard 13 +// spacing line 43 for guard 13 +// spacing line 44 for guard 13 +// spacing line 45 for guard 13 +// spacing line 46 for guard 13 +// spacing line 47 for guard 13 +// spacing line 48 for guard 13 +// spacing line 49 for guard 13 +// spacing line 50 for guard 13 +// spacing line 51 for guard 13 +// spacing line 52 for guard 13 +// spacing line 53 for guard 13 +// spacing line 54 for guard 13 +// spacing line 55 for guard 13 + +// ShardIndex maps a key onto the configured shard range. +func ShardIndex(key, shards int) int { + if shards <= 0 { + return 0 + } + return key % (shards + 1) +} + +// guard 14 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 14. +// spacing line 2 for guard 14. +// spacing line 3 for guard 14. +// spacing line 4 for guard 14. +// spacing line 5 for guard 14. +// spacing line 6 for guard 14. +// spacing line 7 for guard 14. +// spacing line 8 for guard 14. +// spacing line 9 for guard 14. +// spacing line 10 for guard 14. +// spacing line 11 for guard 14. +// spacing line 12 for guard 14. +// spacing line 13 for guard 14. +// spacing line 14 for guard 14. +// spacing line 15 for guard 14. +// spacing line 16 for guard 14. +// spacing line 17 for guard 14. +// spacing line 18 for guard 14. +// spacing line 19 for guard 14. +// spacing line 20 for guard 14. +// spacing line 21 for guard 14. +// spacing line 22 for guard 14. +// spacing line 23 for guard 14. +// spacing line 24 for guard 14. +// spacing line 25 for guard 14. +// spacing line 26 for guard 14 +// spacing line 27 for guard 14 +// spacing line 28 for guard 14 +// spacing line 29 for guard 14 +// spacing line 30 for guard 14 +// spacing line 31 for guard 14 +// spacing line 32 for guard 14 +// spacing line 33 for guard 14 +// spacing line 34 for guard 14 +// spacing line 35 for guard 14 +// spacing line 36 for guard 14 +// spacing line 37 for guard 14 +// spacing line 38 for guard 14 +// spacing line 39 for guard 14 +// spacing line 40 for guard 14 +// spacing line 41 for guard 14 +// spacing line 42 for guard 14 +// spacing line 43 for guard 14 +// spacing line 44 for guard 14 +// spacing line 45 for guard 14 +// spacing line 46 for guard 14 +// spacing line 47 for guard 14 +// spacing line 48 for guard 14 +// spacing line 49 for guard 14 +// spacing line 50 for guard 14 +// spacing line 51 for guard 14 +// spacing line 52 for guard 14 +// spacing line 53 for guard 14 +// spacing line 54 for guard 14 +// spacing line 55 for guard 14 + +// Guard14 validates request field 14. +func Guard14(value int) bool { + return value >= 14 && value < 1_000_014 +} + +// guard 15 is intentionally verbose so hunks stay far apart. +// spacing line 1 for guard 15. +// spacing line 2 for guard 15. +// spacing line 3 for guard 15. +// spacing line 4 for guard 15. +// spacing line 5 for guard 15. +// spacing line 6 for guard 15. +// spacing line 7 for guard 15. +// spacing line 8 for guard 15. +// spacing line 9 for guard 15. +// spacing line 10 for guard 15. +// spacing line 11 for guard 15. +// spacing line 12 for guard 15. +// spacing line 13 for guard 15. +// spacing line 14 for guard 15. +// spacing line 15 for guard 15. +// spacing line 16 for guard 15. +// spacing line 17 for guard 15. +// spacing line 18 for guard 15. +// spacing line 19 for guard 15. +// spacing line 20 for guard 15. +// spacing line 21 for guard 15. +// spacing line 22 for guard 15. +// spacing line 23 for guard 15. +// spacing line 24 for guard 15. +// spacing line 25 for guard 15. +// spacing line 26 for guard 15 +// spacing line 27 for guard 15 +// spacing line 28 for guard 15 +// spacing line 29 for guard 15 +// spacing line 30 for guard 15 +// spacing line 31 for guard 15 +// spacing line 32 for guard 15 +// spacing line 33 for guard 15 +// spacing line 34 for guard 15 +// spacing line 35 for guard 15 +// spacing line 36 for guard 15 +// spacing line 37 for guard 15 +// spacing line 38 for guard 15 +// spacing line 39 for guard 15 +// spacing line 40 for guard 15 +// spacing line 41 for guard 15 +// spacing line 42 for guard 15 +// spacing line 43 for guard 15 +// spacing line 44 for guard 15 +// spacing line 45 for guard 15 +// spacing line 46 for guard 15 +// spacing line 47 for guard 15 +// spacing line 48 for guard 15 +// spacing line 49 for guard 15 +// spacing line 50 for guard 15 +// spacing line 51 for guard 15 +// spacing line 52 for guard 15 +// spacing line 53 for guard 15 +// spacing line 54 for guard 15 +// spacing line 55 for guard 15 + +// Guard15 validates request field 15. +func Guard15(value int) bool { + return value >= 15 && value < 1_000_015 +} diff --git a/evals/packet-dilution/repos/settlement/base/go.mod b/evals/packet-dilution/repos/settlement/base/go.mod new file mode 100644 index 0000000..06cf459 --- /dev/null +++ b/evals/packet-dilution/repos/settlement/base/go.mod @@ -0,0 +1,3 @@ +module settlement + +go 1.23 diff --git a/evals/packet-dilution/repos/settlement/base/settlement/fees.go b/evals/packet-dilution/repos/settlement/base/settlement/fees.go new file mode 100644 index 0000000..92c7bef --- /dev/null +++ b/evals/packet-dilution/repos/settlement/base/settlement/fees.go @@ -0,0 +1,973 @@ +package settlement + +// Settlement rule 1. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// SplitFee divides fee across recipients, returning each share. +func SplitFee(fee int64, recipients int64) []int64 { + if recipients <= 0 { + return nil + } + share := fee / recipients + shares := make([]int64, 0, recipients) + var assigned int64 + for i := int64(0); i < recipients; i++ { + shares = append(shares, share) + assigned += share + } + if remainder := fee - assigned; remainder > 0 { + shares[0] += remainder + } + return shares +} + +// Settlement rule 2. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule2 settles component 2 of the fee ledger. +func Rule2(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 3. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule3 settles component 3 of the fee ledger. +func Rule3(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 4. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule4 settles component 4 of the fee ledger. +func Rule4(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 5. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule5 settles component 5 of the fee ledger. +func Rule5(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 6. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule6 settles component 6 of the fee ledger. +func Rule6(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 7. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// PlatformCut applies the platform rate, bounded by maxCut. +func PlatformCut(amount, rateBps, maxCut int64) int64 { + cut := amount * rateBps / 10_000 + if cut > maxCut { + return maxCut + } + return cut +} + +// Settlement rule 8. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule8 settles component 8 of the fee ledger. +func Rule8(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 9. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule9 settles component 9 of the fee ledger. +func Rule9(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 10. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule10 settles component 10 of the fee ledger. +func Rule10(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 11. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule11 settles component 11 of the fee ledger. +func Rule11(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 12. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule12 settles component 12 of the fee ledger. +func Rule12(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 13. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// RefundAmount returns what is owed back after fees are deducted. +func RefundAmount(gross, feesTaken int64) int64 { + net := gross - feesTaken + if net < 0 { + return 0 + } + return net +} + +// Settlement rule 14. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule14 settles component 14 of the fee ledger. +func Rule14(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 15. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule15 settles component 15 of the fee ledger. +func Rule15(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + return amount * rateBps / 10_000 +} diff --git a/evals/packet-dilution/repos/settlement/feature/go.mod b/evals/packet-dilution/repos/settlement/feature/go.mod new file mode 100644 index 0000000..06cf459 --- /dev/null +++ b/evals/packet-dilution/repos/settlement/feature/go.mod @@ -0,0 +1,3 @@ +module settlement + +go 1.23 diff --git a/evals/packet-dilution/repos/settlement/feature/settlement/fees.go b/evals/packet-dilution/repos/settlement/feature/settlement/fees.go new file mode 100644 index 0000000..ee2dc0a --- /dev/null +++ b/evals/packet-dilution/repos/settlement/feature/settlement/fees.go @@ -0,0 +1,1002 @@ +package settlement + +// Settlement rule 1. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// SplitFee divides fee across recipients, returning each share. +func SplitFee(fee int64, recipients int64) []int64 { + if recipients <= 0 { + return nil + } + share := fee / recipients + shares := make([]int64, 0, recipients) + for i := int64(0); i < recipients; i++ { + shares = append(shares, share) + } + return shares +} + +// Settlement rule 2. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule2 settles component 2 of the fee ledger. +func Rule2(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 3. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule3 settles component 3 of the fee ledger. +func Rule3(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 4. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule4 settles component 4 of the fee ledger. +func Rule4(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 5. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule5 settles component 5 of the fee ledger. +func Rule5(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 6. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule6 settles component 6 of the fee ledger. +func Rule6(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 7. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// PlatformCut applies the platform rate, bounded by maxCut. +func PlatformCut(amount, rateBps, maxCut int64) int64 { + if amount > maxCut { + amount = maxCut + } + return amount * rateBps / 10_000 +} + +// Settlement rule 8. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule8 settles component 8 of the fee ledger. +func Rule8(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 9. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule9 settles component 9 of the fee ledger. +func Rule9(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 10. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule10 settles component 10 of the fee ledger. +func Rule10(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 11. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule11 settles component 11 of the fee ledger. +func Rule11(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 12. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule12 settles component 12 of the fee ledger. +func Rule12(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 13. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// RefundAmount returns what is owed back after fees are deducted. +func RefundAmount(gross, feesTaken int64) int64 { + if gross < 0 { + return 0 + } + return gross +} + +// Settlement rule 14. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule14 settles component 14 of the fee ledger. +func Rule14(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} + +// Settlement rule 15. See docs/fees.md for the ledger contract. +// invariant note 1: amounts are minor units; never float. +// invariant note 2: amounts are minor units; never float. +// invariant note 3: amounts are minor units; never float. +// invariant note 4: amounts are minor units; never float. +// invariant note 5: amounts are minor units; never float. +// invariant note 6: amounts are minor units; never float. +// invariant note 7: amounts are minor units; never float. +// invariant note 8: amounts are minor units; never float. +// invariant note 9: amounts are minor units; never float. +// invariant note 10: amounts are minor units; never float. +// invariant note 11: amounts are minor units; never float. +// invariant note 12: amounts are minor units; never float. +// invariant note 13: amounts are minor units; never float. +// invariant note 14: amounts are minor units; never float. +// invariant note 15: amounts are minor units; never float. +// invariant note 16: amounts are minor units; never float. +// invariant note 17: amounts are minor units; never float. +// invariant note 18: amounts are minor units; never float. +// invariant note 19: amounts are minor units; never float. +// invariant note 20: amounts are minor units; never float. +// invariant note 21: amounts are minor units; never float. +// invariant note 22: amounts are minor units; never float. +// invariant note 23: amounts are minor units; never float. +// invariant note 24: amounts are minor units; never float. +// invariant note 25: amounts are minor units; never float. +// invariant note 26: amounts are minor units; never float. +// invariant note 27: amounts are minor units; never float. +// invariant note 28: amounts are minor units; never float. +// invariant note 29: amounts are minor units; never float. +// invariant note 30: amounts are minor units; never float. +// invariant note 31: amounts are minor units; never float. +// invariant note 32: amounts are minor units; never float. +// invariant note 33: amounts are minor units; never float. +// invariant note 34: amounts are minor units; never float. +// invariant note 35: amounts are minor units; never float. +// invariant note 36: amounts are minor units; never float. +// invariant note 37: amounts are minor units; never float. +// invariant note 38: amounts are minor units; never float. +// invariant note 39: amounts are minor units; never float. +// invariant note 40: amounts are minor units; never float. +// invariant note 41: amounts are minor units; never float. +// invariant note 42: amounts are minor units; never float. +// invariant note 43: amounts are minor units; never float. +// invariant note 44: amounts are minor units; never float. +// invariant note 45: amounts are minor units; never float. +// invariant note 46: amounts are minor units; never float. +// invariant note 47: amounts are minor units; never float. +// invariant note 48: amounts are minor units; never float. +// invariant note 49: amounts are minor units; never float. +// invariant note 50: amounts are minor units; never float. +// invariant note 51: amounts are minor units; never float. +// invariant note 52: amounts are minor units; never float. +// invariant note 53: amounts are minor units; never float. +// invariant note 54: amounts are minor units; never float. + +// Rule15 settles component 15 of the fee ledger. +func Rule15(amount, rateBps int64) int64 { + if amount <= 0 { + return 0 + } + if rateBps < 0 { + return 0 + } + return amount * rateBps / 10_000 +} diff --git a/scripts/draw-pinned-plan.ts b/scripts/draw-pinned-plan.ts new file mode 100644 index 0000000..fe36a1a --- /dev/null +++ b/scripts/draw-pinned-plan.ts @@ -0,0 +1,101 @@ +#!/usr/bin/env tsx +// Plan 103 (experiment-only): write a PinnedPlanArtifact. Internal script, not +// a CLI verb — the pinned-plan seam is eval-only and adding a user-facing +// command would contradict that scope. +// +// Two modes: +// +// --from-run wrap the REAL planner draw recorded by an existing +// run, so an A/B can replay one Stage-5 output across +// both arms. This is what you want for a production +// A/B: it preserves the planner's per-hunk deep/normal/ +// light grading exactly as it was drawn. +// +// (default) author a UNIFORM plan - every hunk gets the same +// coverage and lenses. Built for the dilution fixture, +// where making packet size the only variable was the +// point. Not representative of production grading. +import { readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { parseDiff } from "../src/git/diff-parser.js"; +import { createGitClient } from "../src/git/git-client.js"; +import { buildPinnedPlanArtifact } from "../src/pipeline/pinned-plan.js"; +import type { CoverageLevel, ReviewPlan } from "../src/types.js"; + +function arg(name: string, fallback?: string): string | undefined { + const index = process.argv.indexOf(`--${name}`); + return index >= 0 ? process.argv[index + 1] : fallback; +} + +const repo = arg("repo"); +const base = arg("base", "main") ?? "main"; +const head = arg("head", "feature") ?? "feature"; +const output = arg("output"); +const coverage = (arg("coverage", "normal") ?? "normal") as Exclude; +const lenses = (arg("lenses", "lang/go") ?? "lang/go").split(","); +const anchor = arg("anchor", "content") ?? "content"; + +if (repo === undefined || output === undefined) { + console.error("usage: draw-pinned-plan.ts --repo [--base ref] [--head ref] [--coverage level] [--lenses a,b] --output "); + process.exit(2); +} + +const fromRun = arg("from-run"); +const git = createGitClient(repo); +const rawDiff = await git.diff(base, head); +const diff = parseDiff(rawDiff); +const diffHunkIds = new Set(diff.files.flatMap((file) => file.hunks.map((hunk) => hunk.id))); + +const recordedPlan: ReviewPlan | undefined = fromRun === undefined + ? undefined + : JSON.parse(readFileSync(path.join(fromRun, "stages/05-planner/review-plan.json"), "utf8")) as ReviewPlan; + +if (recordedPlan !== undefined) { + const stale = recordedPlan.coverage.filter((entry) => !diffHunkIds.has(entry.hunkId)).map((entry) => entry.hunkId); + if (stale.length > 0) { + console.error(`recorded plan references ${String(stale.length)} hunk id(s) absent from this diff: ${stale.slice(0, 5).join(", ")}`); + console.error("the run's diff and this base/head do not match; re-draw against the same refs"); + process.exit(1); + } +} + +const authored: ReviewPlan = { + diffUnderstanding: { + declaredIntent: "authored pinned plan for the packet-size recall curve", + inferredBehavior: "uniform coverage so packet size is the only variable across arms" + }, + coverage: diff.files.flatMap((file) => + file.hunks.map((hunk) => ({ + hunkId: hunk.id, + path: file.path, + coverage, + lenses: [...lenses], + surroundingContextHints: [], + reason: "authored uniform coverage" + })) + ) +}; + +const plan = recordedPlan ?? authored; + +// "*" marks a content-anchored artifact: fixture repos are materialized fresh +// per run so their SHAs vary, while content-derived hunk IDs do not. +const artifact = buildPinnedPlanArtifact({ + baseSha: anchor === "content" ? "*" : await git.revParse(base), + headSha: anchor === "content" ? "*" : await git.revParse(head), + plan +}); +writeFileSync(output, `${JSON.stringify(artifact, null, 2)}\n`); +const byCoverage: Record = {}; +for (const entry of plan.coverage) { + byCoverage[entry.coverage] = (byCoverage[entry.coverage] ?? 0) + 1; +} +console.log(JSON.stringify({ + output, + source: recordedPlan !== undefined ? `recorded draw: ${String(fromRun)}` : "authored uniform plan", + coverageEntries: plan.coverage.length, + diffHunks: diffHunkIds.size, + byCoverage, + planSha256: artifact.planSha256, + anchor +}, null, 1)); diff --git a/scripts/packet-packing-report.ts b/scripts/packet-packing-report.ts new file mode 100644 index 0000000..ae5ffe8 --- /dev/null +++ b/scripts/packet-packing-report.ts @@ -0,0 +1,406 @@ +#!/usr/bin/env tsx +// Plan 103 packet-packing report. Rebuilds Stage 6 from recorded run artifacts +// with packing off and on, using the real builder and zero model calls, and +// fails closed on any invariant violation. +// +// Modes: +// replay --repo --run ... [--dispatch-slots N] [--distinct-diffs] --output +// +// Failure records are structured and templated: a closed-set code plus typed +// fields, rendered from a template. Raw exception text and repository source +// are never interpolated into a message. + +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { parseDiff } from "../src/git/diff-parser.js"; +import { filterDiffFiles, classifyChangedFiles } from "../src/git/file-classifier.js"; +import { createGitClient } from "../src/git/git-client.js"; +import { buildRepositoryIndex } from "../src/repo/repository-index.js"; +import { buildReviewPackets } from "../src/pipeline/packet-builder.js"; +import { defaultConfig } from "../src/config/schema.js"; +import { applyRepoConfigLayer } from "../src/config/config-loader.js"; +import type { + CodegenieConfig, + ResolvedReviewInput, + ReviewPacket, + ReviewPlan, + TelemetryEvent +} from "../src/types.js"; +import type { TelemetryRecorder } from "../src/telemetry/telemetry-recorder.js"; + +const DEFAULT_DISPATCH_SLOTS = 56; +const HISTORICAL_BASELINE_HUNKS = 89; + +type FailureCode = + | "run_artifacts_missing" + | "ref_unavailable" + | "model_call_observed" + | "hunk_not_unique" + | "atom_split" + | "cap_exceeded" + | "coverage_changed" + | "profile_downgraded" + | "budget_downgraded" + | "lens_dropped" + | "dispatch_rank_invalid" + | "estimator_unreconciled" + | "treatment_shape_unmet" + | "targets_share_packet"; + +export type Failure = { code: FailureCode; run: string; message: string; fields: Record }; + +export function fail(code: FailureCode, run: string, fields: Record): Failure { + const rendered = Object.entries(fields) + .map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : String(value)}`) + .join(" "); + return { code, run, message: `${code}: ${rendered}`, fields }; +} + +// Counts every repository tool call so a replay that silently reaches the +// network or a model can be caught; the builder must stay offline. +function countingTelemetry(counters: { events: number; modelCalls: number }): TelemetryRecorder { + return { + event: (entry: TelemetryEvent) => { + counters.events += 1; + if (typeof entry.message === "string" && entry.message.startsWith("model_call")) { + counters.modelCalls += 1; + } + }, + writeArtifact: async () => undefined + } as unknown as TelemetryRecorder; +} + +function readJson(file: string): T { + return JSON.parse(readFileSync(file, "utf8")) as T; +} + +// The reviewed repository's own codegenie.toml carries the classification path +// rules that decide which files are reviewed at all. Replaying without it keeps +// generated files the real run skipped and silently changes the workload, which +// is what the reconciliation gate exists to catch. +function packingConfig(repoRoot: string, on: boolean): CodegenieConfig { + const base = structuredClone(defaultConfig) as CodegenieConfig; + base.telemetry.enabled = false; + const config = applyRepoConfigLayer(base, repoRoot).config; + config.review.packRelatedHunks = on; + return config; +} + +const COVERAGE_RANK = { deep: 0, normal: 1, light: 2 } as const; +const PROFILE_RANK = { simple: 0, standard: 1, investigate: 2 } as const; +const PRIORITY_RANK = { critical: 0, high: 1, normal: 2, low: 3 } as const; + +// Mirrors worker-runner's scheduling comparator. The real scheduler is a +// prefix-with-holes at concurrency > 1, so this is a calibrated counterfactual +// capacity proxy, not a reproduction — the reconciliation gate below is what +// makes it trustworthy. +export function dispatchOrder(packets: ReviewPacket[]): ReviewPacket[] { + return [...packets].sort( + (a, b) => + PRIORITY_RANK[a.reviewPriority] - PRIORITY_RANK[b.reviewPriority] || + COVERAGE_RANK[a.coverage] - COVERAGE_RANK[b.coverage] || + (a.dispatchRank[0] ?? 0) - (b.dispatchRank[0] ?? 0) || + (a.dispatchRank[1] ?? 0) - (b.dispatchRank[1] ?? 0) || + a.id.localeCompare(b.id) + ); +} + +export function hunksWithinSlots(packets: ReviewPacket[], slots: number): number { + return new Set( + dispatchOrder(packets) + .slice(0, slots) + .flatMap((packet) => packet.hunks.map((hunk) => hunk.hunkId)) + ).size; +} + +async function rebuildStageSix( + resolved: ResolvedReviewInput, + plan: ReviewPlan, + on: boolean +): Promise<{ packets: ReviewPacket[]; modelCalls: number }> { + const counters = { events: 0, modelCalls: 0 }; + const telemetry = countingTelemetry(counters); + const config = packingConfig(resolved.repoRoot, on); + const diff = parseDiff(resolved.rawDiff); + const { kept, decisions } = await filterDiffFiles(resolved, diff, config, telemetry); + const facts = await classifyChangedFiles(resolved, kept, decisions, config, telemetry); + const repoIndex = await buildRepositoryIndex(resolved, kept, facts, config, telemetry); + const packets = await buildReviewPackets(plan, kept, facts, repoIndex, telemetry, { + config, + enabledLenses: [...new Set(plan.coverage.flatMap((entry) => entry.lenses))] + }); + return { packets, modelCalls: counters.modelCalls }; +} + +export function comparePackets(run: string, off: ReviewPacket[], on: ReviewPacket[], slots: number): Failure[] { + const failures: Failure[] = []; + const offHunks = off.flatMap((packet) => packet.hunks.map((hunk) => hunk.hunkId)); + const onHunks = on.flatMap((packet) => packet.hunks.map((hunk) => hunk.hunkId)); + + if (new Set(onHunks).size !== onHunks.length) { + failures.push(fail("hunk_not_unique", run, { duplicates: onHunks.length - new Set(onHunks).size })); + } + const missing = offHunks.filter((hunkId) => !onHunks.includes(hunkId)); + if (missing.length > 0) { + failures.push(fail("hunk_not_unique", run, { missingHunks: missing.length })); + } + + for (const packet of on) { + if (packet.hunks.length > defaultConfig.review.packMaxHunks) { + failures.push(fail("cap_exceeded", run, { packetId: packet.id, hunks: packet.hunks.length })); + } + } + + // Coverage, profile, and budget may never fall for any hunk relative to the + // packet that carried it with packing off. + const offByHunk = new Map(); + for (const packet of off) { + for (const hunk of packet.hunks) { + offByHunk.set(hunk.hunkId, packet); + } + } + for (const packet of on) { + for (const hunk of packet.hunks) { + const before = offByHunk.get(hunk.hunkId); + if (before === undefined) { + continue; + } + // The partition key forces identical coverage across a packet's members, + // so any change at all is a violation — promotion inflates cost and + // demotion reviews a hunk more shallowly than planned. + if (packet.coverage !== before.coverage) { + failures.push(fail("coverage_changed", run, { hunkId: hunk.hunkId, from: before.coverage, to: packet.coverage })); + } + if (PROFILE_RANK[packet.reviewProfile] < PROFILE_RANK[before.reviewProfile]) { + failures.push(fail("profile_downgraded", run, { hunkId: hunk.hunkId, from: before.reviewProfile, to: packet.reviewProfile })); + } + if (packet.toolBudget.maxToolCalls < before.toolBudget.maxToolCalls) { + failures.push(fail("budget_downgraded", run, { + hunkId: hunk.hunkId, + from: before.toolBudget.maxToolCalls, + to: packet.toolBudget.maxToolCalls + })); + } + for (const lens of before.lenses) { + if (!packet.lenses.includes(lens)) { + failures.push(fail("lens_dropped", run, { hunkId: hunk.hunkId, lens })); + } + } + } + } + void slots; + return failures; +} + +// Model-free proof that a fixture actually receives the treatment the recall +// curve depends on: exact packet counts at each cap, and each target hunk in a +// packet of exactly the intended size with no two targets sharing a packet. +async function treatment(args: Map): Promise { + const repo = args.get("repo")?.[0]; + const base = args.get("base")?.[0] ?? "main"; + const head = args.get("head")?.[0] ?? "feature"; + const targets = (args.get("target") ?? []).flatMap((value) => value.split(",")); + const expectPackets = (args.get("expect-packets")?.[0] ?? "15,5,3").split(",").map(Number); + const caps = (args.get("caps")?.[0] ?? "1,3,5").split(",").map(Number); + const output = args.get("output")?.[0]; + if (repo === undefined || output === undefined) { + console.error("usage: packet-packing-report.ts treatment --repo --target ... --output "); + return 2; + } + + const git = createGitClient(repo); + const rawDiff = await git.diff(base, head); + const resolved: ResolvedReviewInput = { + mode: "commit_range", + repoRoot: repo, + baseRef: base, + headRef: head, + headSha: head, + mergeBase: base, + commits: [], + rawDiff + }; + const parsed = parseDiff(rawDiff); + const targetHunkIds = parsed.files + .flatMap((file) => file.hunks) + .filter((hunk) => targets.includes(String(hunk.newStart))) + .map((hunk) => hunk.id); + + const failures: Failure[] = []; + const shapes: Array> = []; + for (const [index, cap] of caps.entries()) { + const config = packingConfig(repo, true); + config.review.packMaxHunks = cap; + const counters = { events: 0, modelCalls: 0 }; + const telemetry = countingTelemetry(counters); + const { kept, decisions } = await filterDiffFiles(resolved, parsed, config, telemetry); + const facts = await classifyChangedFiles(resolved, kept, decisions, config, telemetry); + const repoIndex = await buildRepositoryIndex(resolved, kept, facts, config, telemetry); + const packets = await buildReviewPackets( + { diffUnderstanding: { declaredIntent: "dilution fixture", inferredBehavior: "boundary guards" }, coverage: [] }, + kept, + facts, + repoIndex, + telemetry, + { config, enabledLenses: ["lang/go"] } + ); + const expected = expectPackets[index]; + if (expected !== undefined && packets.length !== expected) { + failures.push(fail("treatment_shape_unmet", `cap-${cap}`, { expected, actual: packets.length })); + } + const targetPackets = targetHunkIds.map((hunkId) => packets.find((packet) => packet.hunks.some((hunk) => hunk.hunkId === hunkId))); + const targetSizes = targetPackets.map((packet) => packet?.hunks.length ?? 0); + if (targetSizes.some((size) => size !== cap)) { + failures.push(fail("treatment_shape_unmet", `cap-${cap}`, { expectedTargetHunks: cap, actual: targetSizes })); + } + const targetPacketIds = targetPackets.map((packet) => packet?.id ?? ""); + if (new Set(targetPacketIds).size !== targetPacketIds.length) { + failures.push(fail("targets_share_packet", `cap-${cap}`, { packetIds: targetPacketIds })); + } + if (counters.modelCalls > 0) { + failures.push(fail("model_call_observed", `cap-${cap}`, { modelCalls: counters.modelCalls })); + } + shapes.push({ cap, packets: packets.length, targetPacketHunks: targetSizes, distinctTargetPackets: new Set(targetPacketIds).size }); + } + + const report = { schemaVersion: 1, mode: "treatment", repo, targets, shapes, failures }; + writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`); + console.log(JSON.stringify({ shapes, failures }, null, 1)); + return failures.length === 0 ? 0 : 1; +} + +async function replay(args: Map): Promise { + const repo = args.get("repo")?.[0]; + const runs = args.get("run") ?? []; + const output = args.get("output")?.[0]; + const slots = Number(args.get("dispatch-slots")?.[0] ?? DEFAULT_DISPATCH_SLOTS); + if (repo === undefined || runs.length === 0 || output === undefined) { + console.error("usage: packet-packing-report.ts replay --repo --run ... --output "); + return 2; + } + + const failures: Failure[] = []; + const rows: Array> = []; + const git = createGitClient(repo); + const seenDiffs = new Map(); + + for (const runDir of runs) { + const name = path.basename(runDir); + const inputPath = path.join(runDir, "stages/01-input/resolved-input.json"); + const planPath = path.join(runDir, "stages/05-planner/review-plan.json"); + if (!existsSync(inputPath) || !existsSync(planPath)) { + failures.push(fail("run_artifacts_missing", name, { inputPath: existsSync(inputPath), planPath: existsSync(planPath) })); + continue; + } + const recorded = readJson<{ baseRef?: string; headSha?: string; mergeBase?: string; commits?: unknown[] }>(inputPath); + const plan = readJson(planPath); + const base = recorded.mergeBase ?? recorded.baseRef; + const head = recorded.headSha; + if (base === undefined || head === undefined) { + failures.push(fail("run_artifacts_missing", name, { base: base ?? null, head: head ?? null })); + continue; + } + try { + await git.revParse(base); + await git.revParse(head); + } catch { + failures.push(fail("ref_unavailable", name, { base, head })); + continue; + } + + const rawDiff = await git.diff(base, head); + const diffKey = `${base}..${head}`; + const duplicateOf = seenDiffs.get(diffKey); + seenDiffs.set(diffKey, duplicateOf ?? name); + + const resolved: ResolvedReviewInput = { + mode: "commit_range", + repoRoot: repo, + baseRef: base, + headRef: head, + headSha: head, + mergeBase: base, + commits: [], + rawDiff + }; + + const off = await rebuildStageSix(resolved, plan, false); + const on = await rebuildStageSix(resolved, plan, true); + if (off.modelCalls > 0 || on.modelCalls > 0) { + failures.push(fail("model_call_observed", name, { off: off.modelCalls, on: on.modelCalls })); + } + failures.push(...comparePackets(name, off.packets, on.packets, slots)); + + const offYield = hunksWithinSlots(off.packets, slots); + const onYield = hunksWithinSlots(on.packets, slots); + if (duplicateOf === undefined && offYield !== HISTORICAL_BASELINE_HUNKS && name.endsWith("dca8d870")) { + failures.push(fail("estimator_unreconciled", name, { expected: HISTORICAL_BASELINE_HUNKS, actual: offYield, slots })); + } + + rows.push({ + run: name, + duplicateOf: duplicateOf ?? null, + offPackets: off.packets.length, + onPackets: on.packets.length, + reductionPct: Number((100 * (off.packets.length - on.packets.length) / Math.max(1, off.packets.length)).toFixed(1)), + multiHunkPackets: on.packets.filter((packet) => packet.hunks.length > 1).length, + hunkDistribution: [1, 2, 3, 4, 5].map((size) => on.packets.filter((packet) => packet.hunks.length === size).length), + reviewableHunks: new Set(off.packets.flatMap((packet) => packet.hunks.map((hunk) => hunk.hunkId))).size, + fixedSlotYield: { slots, off: offYield, on: onYield, gain: onYield - offYield }, + modelCallsObserved: off.modelCalls + on.modelCalls + }); + } + + const distinctRows = rows.filter((row) => row.duplicateOf === null); + const report = { + schemaVersion: 1, + mode: "replay", + dispatchSlots: slots, + noModelCalls: rows.every((row) => row.modelCallsObserved === 0), + distinctDiffs: distinctRows.length, + rows, + failures + }; + writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`); + console.log(JSON.stringify({ rows, failures: failures.length, distinctDiffs: distinctRows.length }, null, 1)); + return failures.length === 0 ? 0 : 1; +} + +function parseArgs(argv: string[]): Map { + const args = new Map(); + for (let i = 0; i < argv.length; i += 1) { + const token = argv[i]; + if (token === undefined || !token.startsWith("--")) { + continue; + } + const key = token.slice(2); + const value = argv[i + 1]; + if (value === undefined || value.startsWith("--")) { + args.set(key, [...(args.get(key) ?? []), "true"]); + continue; + } + args.set(key, [...(args.get(key) ?? []), value]); + i += 1; + } + return args; +} + +async function main(): Promise { + const [mode, ...rest] = process.argv.slice(2); + const args = parseArgs(rest); + if (mode === "replay") { + process.exitCode = await replay(args); + return; + } + if (mode === "treatment") { + process.exitCode = await treatment(args); + return; + } + console.error("usage: packet-packing-report.ts replay --repo --run ... --output "); + process.exitCode = 2; +} + +// Importable for focused tests; only the CLI entry point runs main(). +if (process.argv[1]?.includes("packet-packing-report")) { + await main(); +} diff --git a/scripts/packing-diagnostics.mjs b/scripts/packing-diagnostics.mjs new file mode 100644 index 0000000..1ca19a5 --- /dev/null +++ b/scripts/packing-diagnostics.mjs @@ -0,0 +1,337 @@ +#!/usr/bin/env node +// Packing diagnostics over recorded run artifacts (Plan 103 planning aid). +// +// Answers three questions about same-file packet packing without running a +// review or calling a model: +// +// pairs which atoms are related but currently split, and what blocks them +// sweep how packet count responds to the hunk/patch caps +// simulate packet count under a chosen packing predicate +// +// APPROXIMATIONS — this is a planning aid, not the authoritative measurement. +// Plan 103 step 8 specifies a real-builder replay; that is what gates the work. +// This tool differs in three known ways: +// +// 1. Patch size uses each packet hunk's recorded `contentWithLineNumbers` +// length. The builder uses combinedPatchChars() over raw diff lines. +// Plan 102 documented the two differing by about one packet per run. +// 2. Container identity is recovered by splitting the owner prefix out of +// the recorded `enclosingSymbol` display string ("(*T).Method" -> "T"), +// which collides across duplicate or nested same-named owners. Only the +// `related` predicate uses it, and that predicate was measured and +// rejected — Plan 103 ships neither container identity nor relationship +// ordering. This mode exists to reproduce that rejection. +// 3. Planner lens signatures are only known for hunks the planner issued a +// coverage entry for. Undeclared hunks fall back to +// defaultLensesForLanguage(), which is constant within a file, so they +// share a synthetic DEFAULT signature. That is sound within a file and +// wrong across files — this tool never compares across files. +// +// Calibration: `sweep` at 5h/12K reproduces Plan 102's measured packet counts +// (75/68/85) within one packet on all three distinct retained diffs. +// +// Usage: +// node scripts/packing-diagnostics.mjs pairs +// node scripts/packing-diagnostics.mjs sweep +// node scripts/packing-diagnostics.mjs simulate [--predicate source|compatibility|related] [--max-hunks N] [--max-patch N] +// +// A runDir is a recorded telemetry run, e.g. +// /.codegenie/runs/20260724-184952-dca8d870 + +import { readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; + +const DEFAULT_MAX_HUNKS = 5; +const DEFAULT_MAX_PATCH = 12_000; +const SWEEP_GRID = [[5, 12_000], [6, 12_000], [8, 12_000], [8, 16_000], [10, 16_000], [10, 20_000], [12, 24_000]]; + +function readJson(file) { + return JSON.parse(readFileSync(file, "utf8")); +} + +// "(*FeeCalculator).CalculateIntentFees" -> "FeeCalculator"; "Foo.bar" -> "Foo". +function ownerPrefix(symbol) { + const dot = symbol.lastIndexOf("."); + if (dot <= 0) { + return undefined; + } + const owner = symbol.slice(0, dot).replace(/^\(\*?|\)$/gu, "").trim(); + return owner.length > 0 ? owner : undefined; +} + +// Baseline runs did no packing, so each recorded packet is exactly one atom. +function loadRun(runDir) { + const plan = readJson(path.join(runDir, "stages/05-planner/review-plan.json")); + const graph = readJson(path.join(runDir, "stages/06-packets/hunk-relationships.json")); + const coverageByHunk = new Map((plan.coverage ?? []).map((entry) => [entry.hunkId, entry.coverage])); + const lensesByHunk = new Map((plan.coverage ?? []).map((entry) => [entry.hunkId, [...(entry.lenses ?? [])].sort().join(",")])); + + const atoms = []; + const atomByHunk = new Map(); + const packetsDir = path.join(runDir, "stages/06-packets/packets"); + for (const file of readdirSync(packetsDir)) { + if (!file.endsWith(".json")) { + continue; + } + const packet = readJson(path.join(packetsDir, file)); + const hunks = packet.hunks ?? []; + if (hunks.length === 0) { + continue; + } + const owners = new Set(); + for (const hunk of hunks) { + const fact = (packet.symbolFacts ?? []).find((entry) => entry.hunkId === hunk.hunkId && entry.enclosingSymbol); + const owner = fact ? ownerPrefix(fact.enclosingSymbol) : undefined; + if (owner !== undefined) { + owners.add(owner); + } + } + const atom = { + id: packet.id, + path: packet.path, + kind: packet.kind, + coverage: packet.coverage, + owners, + hunkIds: hunks.map((hunk) => hunk.hunkId), + hunkCount: hunks.length, + patchChars: hunks.reduce((sum, hunk) => sum + (hunk.contentWithLineNumbers ?? "").length, 0), + sourcePos: Math.min(...hunks.map((hunk) => hunk.newStart ?? hunk.oldStart ?? 0)), + lensSignature: [...new Set(hunks.map((hunk) => lensesByHunk.get(hunk.hunkId) ?? "DEFAULT"))].sort().join("|") + }; + atoms.push(atom); + for (const hunkId of atom.hunkIds) { + atomByHunk.set(hunkId, atom.id); + } + } + + return { + name: path.basename(runDir), + atoms, + atomByHunk, + graph, + plannerEntries: (plan.coverage ?? []).length, + effectiveCoverage: (hunkId) => coverageByHunk.get(hunkId) ?? "normal" + }; +} + +// Packing-affinity adjacency between atoms: strong recorded edges plus shared +// container. Used only by the `pairs` and `related` analyses, which exist to +// reproduce why Plan 103 rejected relationship-based packing. +function affinity(run) { + const byId = new Map(run.atoms.map((atom) => [atom.id, atom])); + const adjacency = new Map(run.atoms.map((atom) => [atom.id, new Set()])); + const pairSources = new Map(); + const pairKey = (a, b) => [a, b].sort().join("|"); + + const link = (a, b, source) => { + if (a === b) { + return; + } + adjacency.get(a)?.add(b); + adjacency.get(b)?.add(a); + const key = pairKey(a, b); + const sources = pairSources.get(key) ?? new Set(); + sources.add(source); + pairSources.set(key, sources); + }; + + for (const edge of run.graph.edges ?? []) { + if (edge.strength !== "strong" || !edge.toHunkId) { + continue; + } + const from = run.atomByHunk.get(edge.fromHunkId); + const to = run.atomByHunk.get(edge.toHunkId); + if (from === undefined || to === undefined || from === to) { + continue; + } + if (byId.get(from).path !== byId.get(to).path) { + continue; + } + link(from, to, edge.source); + } + + const byFile = new Map(); + for (const atom of run.atoms) { + byFile.set(atom.path, [...(byFile.get(atom.path) ?? []), atom]); + } + for (const group of byFile.values()) { + for (let i = 0; i < group.length; i += 1) { + for (let j = i + 1; j < group.length; j += 1) { + if ([...group[i].owners].some((owner) => group[j].owners.has(owner))) { + link(group[i].id, group[j].id, "same_container"); + } + } + } + } + + return { byId, adjacency, pairSources, byFile }; +} + +function bypassesPacking(atom) { + return atom.kind === "whole-file" || atom.kind === "file-diff"; +} + +// Which related-but-split atom pairs exist, and what prevents each from packing. +function pairs(run) { + const { byId, pairSources } = affinity(run); + const summary = { coverage: 0, lenses: 0, caps: 0, bypass: 0, eligible: 0 }; + const detail = []; + for (const [key, sources] of pairSources) { + const [x, y] = key.split("|"); + const a = byId.get(x); + const b = byId.get(y); + // Checked in the same order the production predicate applies them, so the + // first blocker reported is the one that actually decides the pair. + let blocker; + if (bypassesPacking(a) || bypassesPacking(b)) { + blocker = "bypass"; + } else if (a.coverage !== b.coverage) { + blocker = "coverage"; + } else if (a.lensSignature !== b.lensSignature) { + blocker = "lenses"; + } else if (a.hunkCount + b.hunkCount > DEFAULT_MAX_HUNKS || a.patchChars + b.patchChars > DEFAULT_MAX_PATCH) { + blocker = "caps"; + } else { + blocker = "eligible"; + } + summary[blocker] += 1; + detail.push({ + file: a.path, + sources: [...sources].sort().join("+"), + blocker, + a: `${a.kind} ${a.coverage} ${a.hunkCount}h/${a.patchChars}c`, + b: `${b.kind} ${b.coverage} ${b.hunkCount}h/${b.patchChars}c` + }); + } + return { + run: run.name, + atoms: run.atoms.length, + plannerEntries: run.plannerEntries, + relatedSplitAtomPairs: pairSources.size, + blockedBy: summary, + detail: detail.sort((p, q) => p.file.localeCompare(q.file)) + }; +} + +// All three predicates share Plan 102's partition (same file + same effective +// coverage + same planner lens signature) and differ only in admission order: +// +// source fill in source order — Plan 102's rule +// compatibility prefer related, else source — Plan 103's rule +// related admit only related atoms — the rejected draft +function simulate(run, { predicate, maxHunks, maxPatch }) { + const { adjacency } = affinity(run); + const partitions = new Map(); + const packets = []; + + for (const atom of run.atoms) { + if (bypassesPacking(atom)) { + packets.push([atom]); + continue; + } + const key = `${atom.path}${atom.coverage}${atom.lensSignature}`; + partitions.set(key, [...(partitions.get(key) ?? []), atom]); + } + + for (const group of partitions.values()) { + const remaining = [...group].sort((a, b) => a.sourcePos - b.sourcePos); + while (remaining.length > 0) { + const seed = remaining.shift(); + const members = [seed]; + let hunks = seed.hunkCount; + let patch = seed.patchChars; + for (;;) { + const fits = (atom) => hunks + atom.hunkCount <= maxHunks && patch + atom.patchChars <= maxPatch; + const related = predicate === "source" + ? undefined + : remaining.find((atom) => fits(atom) && members.some((member) => adjacency.get(member.id)?.has(atom.id))); + const next = predicate === "related" ? related : related ?? remaining.find(fits); + if (next === undefined) { + break; + } + remaining.splice(remaining.indexOf(next), 1); + members.push(next); + hunks += next.hunkCount; + patch += next.patchChars; + } + packets.push(members); + } + } + + const sizes = packets.map((packet) => packet.reduce((sum, atom) => sum + atom.hunkCount, 0)); + const atomsPerPacket = {}; + for (const packet of packets) { + atomsPerPacket[packet.length] = (atomsPerPacket[packet.length] ?? 0) + 1; + } + return { + run: run.name, + predicate, + caps: { maxHunks, maxPatch }, + atomsOff: run.atoms.length, + packetsOn: packets.length, + reductionPct: Number((100 * (run.atoms.length - packets.length) / run.atoms.length).toFixed(1)), + multiAtomPackets: packets.filter((packet) => packet.length > 1).length, + largestPacketHunks: Math.max(...sizes), + packetsOverFiveHunks: sizes.filter((size) => size > 5).length, + atomsPerPacket + }; +} + +function sweep(run) { + return { + run: run.name, + atomsOff: run.atoms.length, + grid: SWEEP_GRID.map(([maxHunks, maxPatch]) => { + const result = simulate(run, { predicate: "compatibility", maxHunks, maxPatch }); + return { + cap: `${maxHunks}h/${maxPatch / 1000}K`, + packets: result.packetsOn, + reductionPct: result.reductionPct, + multiAtomPackets: result.multiAtomPackets, + largestPacketHunks: result.largestPacketHunks, + packetsOverFiveHunks: result.packetsOverFiveHunks + }; + }) + }; +} + +function main() { + const [mode, ...rest] = process.argv.slice(2); + const flags = new Map(); + const runDirs = []; + for (let i = 0; i < rest.length; i += 1) { + const arg = rest[i]; + if (arg.startsWith("--")) { + flags.set(arg.slice(2), rest[i + 1]); + i += 1; // consume the value so it is never mistaken for a run directory + continue; + } + runDirs.push(arg); + } + const flag = (name, fallback) => flags.get(name) ?? fallback; + + if (!mode || runDirs.length === 0 || !["pairs", "sweep", "simulate"].includes(mode)) { + console.error("usage: packing-diagnostics.mjs [--predicate source|compatibility|related] [--max-hunks N] [--max-patch N]"); + process.exitCode = 2; + return; + } + + const options = { + predicate: flag("predicate", "compatibility"), + maxHunks: Number(flag("max-hunks", DEFAULT_MAX_HUNKS)), + maxPatch: Number(flag("max-patch", DEFAULT_MAX_PATCH)) + }; + if (!["source", "compatibility", "related"].includes(options.predicate)) { + console.error(`unknown predicate: ${options.predicate}`); + process.exitCode = 2; + return; + } + + for (const runDir of runDirs) { + const run = loadRun(runDir); + const report = mode === "pairs" ? pairs(run) : mode === "sweep" ? sweep(run) : simulate(run, options); + console.log(JSON.stringify(report, null, 1)); + } +} + +main(); diff --git a/specs/plans/102-issue-102-same-file-packet-packing.md b/specs/plans/102-issue-102-same-file-packet-packing.md index e1f41c5..048cc8e 100644 --- a/specs/plans/102-issue-102-same-file-packet-packing.md +++ b/specs/plans/102-issue-102-same-file-packet-packing.md @@ -1,11 +1,11 @@ # Issue 102: Same-File Packet Packing -Status: PENDING +Status: COMPLETE — failed treatment gate; baseline restored Related: Plan 100 (COMPLETE; dispatch rank), Plans 40/44 (recall calibration), Plan 79 (repeat/recall harness) Planned from: production run `.codegenie/runs/20260724-184952-dca8d870` against `0xsequence/trails-api` PR 846 (88 files, 217 hunks, `--max-time 60`, concurrency 4), plus retained runs `740d73f2`, `fe1548ae`, and `81f806a6`, 2026-07-24 Production replay refs: base/merge-base `d1c49bdf6a8002ec2ec27faac94a932d736532b2`; head `fbb5f8761c2c296e115af17e919a7c35d9de8373` Planned at: commit `6909e1a` (branch `next`) -Recommended priority: next throughput plan. Plan 100 is complete, and run `dca8d870` is the clean planner-survival/dispatch-order baseline: 32/32 planner entries survived and all 19 deep hunks were dispatched. Its dependency is satisfied. +Final outcome: the repaired one-repeat treatment gate failed, so same-file packet packing did not roll out. Step 12 restored the baseline packet path and removed the experiment; the immutable reports and paid logs retain the rejected design's measurement record. > Executor instructions: preserve the output of today's semantic hunk grouper as indivisible **atoms**. Do not replace `canJoinGroup` with an affinity sort: proximity is not transitive, and a sort cannot preserve its semantics. Never combine atoms with different effective coverage levels. Preserve each atom's standalone review profile as a monotonic floor when packing internalizes relationship context. Keep source order, `MAX_HUNKS_PER_PACKET = 5`, and `MAX_PATCH_CHARS = 12_000`. Land packing dark, validate deterministic packet shape before spending model calls, then record treatment for every paid execution and require at least 8/10 treated B/C executions per arm/case. Use paired repeated A/B/C evals to select the production behavior and tool-budget calculation, preserve the paid evidence, then remove every experiment-only flag and alternate path in the dedicated teardown step. > @@ -13,6 +13,149 @@ Recommended priority: next throughput plan. Plan 100 is complete, and run `dca8d > Working-tree check: `git status --short -- src/pipeline/packet-builder.ts src/config/schema.ts src/config/config-loader.ts src/types.ts src/evals/eval-runner.ts scripts/packet-packing-report.ts tests/fixtures/packet-packing-golden.json tests/pipeline-phase5.test.ts tests/config-loader.test.ts tests/evals.test.ts tests/packet-packing-report.test.ts` > If the drift check reports committed changes, reconcile the current-state claims below. If the working-tree check reports changes owned by another task, stop and wait for that work to land or move this plan to an isolated worktree; do not overwrite it. +## Phase 3 paid-validation authorization (2026-07-25; before first paid call) + +The owner supplied `approvedValidationCostUSD: $500`, which is the active total validation ceiling and supersedes the earlier `$200` authorization communicated for this phase. Before the one-repeat step-7 preflight, `actualValidationCostUSD = $0.00` and `projectedRemainingCostUSD = $60.00`, so `actualValidationCostUSD + projectedRemainingCostUSD = $60.00 <= $500.00`. The `$60.00` preflight reservation assumes six paid A/B/C executions capped at `expect.maxCostUSD: 10` each; the fixture cases use the same explicit provider/model/reasoning/concurrency/depth/lens/time/token configuration and run with cache disabled. Authorization in this phase is limited to this one-repeat suite. Repeat-10, collateral, production-capacity, arm selection, rollout, and teardown remain `not_run` and are not authorized by this phase. + +After the preflight, replace the reservation with reconstructed cohort spend, set the repeat-10 projection to exactly `10 * preflightCostUSD`, and record cumulative actual plus projected spend. The later production phase continues to reserve `$95` only when reached and separately authorized; real-model collateral selected-side cost remains deferred until its A-side measurement, as required by this plan. + +### Phase 3 one-repeat invalid cohort, diagnosis, and deterministic repair (2026-07-25) + +The private suite was created at `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/recall/` with materializable `dilution-control` and `cross-atom-consistency` Go fixtures plus `dilution-{a,b,c}.yml` and `consistency-{a,b,c}.yml`. All six cases remain at `repeat: 1`. Within each family the current definitions are identical except for the arm name suffix and the two permitted review settings: A is packing off/base, B is packing on/base, and C is packing on/atom-scaled. They use Anthropic `claude-opus-4-8`, reasoning `high`, normal depth, concurrency/provider concurrency 1, a 20-minute case limit, a 1,000,000-token budget, `expect.maxCostUSD: 10`, and cache disabled. + +The first paid cohort used the original three-lens configuration (`core/code-review`, `core/tests`, and `lang/go`). Its cache-off invocation was `ace65769-3a68-4bd1-bd36-662fa6827dc6`, started `2026-07-25T12:02:33.511Z` and completed `2026-07-25T12:23:15.259Z`. Its exact log root is `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/recall/logs`; the same ID is the selected cohort. It completed six live executions with zero eval/provider/configuration errors and 84 model calls: + +| Run | Case | Review run ID | Eval score | Actual cost USD | +| ---: | --- | --- | --- | ---: | +| 1 | `consistency-a` | `20260725-120233-6c490c44` | fail, 1/2 expectations | 1.270413 | +| 2 | `consistency-b` | `20260725-120706-36842f4b` | fail, 1/2 expectations | 0.901080 | +| 3 | `consistency-c` | `20260725-121006-87a8ed77` | fail, 0/2 expectations | 0.519151 | +| 4 | `dilution-a` | `20260725-121205-5a020015` | pass, 2/2 expectations | 1.102087 | +| 5 | `dilution-b` | `20260725-121737-6249976f` | fail, 0/2 expectations | 0.539328 | +| 6 | `dilution-c` | `20260725-121917-4c7e8289` | fail, 1/2 expectations | 0.875598 | + +The exact required report command selected that cohort and failed closed. The original invalid report is preserved immutably at `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/reports/plan102-eval-preflight-invalid-ace65769.json`, SHA-256 `3368178d57d6c4c46865fccb52b8aa160d1f9acf3536be9e1cde4d05d52c50ab`. Its authoritative reconstructed `preflightCostUSD` and cumulative `actualValidationCostUSD` are `$5.207657`; the unrounded sum of the 84 model-call records is `$5.207655`, with the `$0.000002` difference coming from summing the report's per-run six-decimal values. + +The invalid cohort's exact additional repeat-10 planning projection is `10 * preflightCostUSD = $52.076570`. Because a corrected repeat-one retry is now pending, reserve at most another `$60` before that retry and replace the repeat-10 estimate with `10 * correctedPreflightCostUSD` only after a valid retry. The currently known accounting projection is therefore `$60 + $52.076570 + $95 = $207.076570` (retry cap, placeholder repeat-10 estimate, and production-pair reservation), with fake-provider collateral `$0` and real-model collateral still deferred until its A-side measurement. Cumulative actual plus this known projection is `$212.284227 <= approvedValidationCostUSD: $500`, leaving `$287.715773` before deferred collateral. This is accounting only: Phase 3 does not authorize or start the retry. + +Paid target treatment was invalid in every packed arm/case: consistency B `0/1`, consistency C `0/1`, dilution B `0/1`, and dilution C `0/1`. Each consistency target remained a one-atom deep packet with the same requested/routed lenses as A while non-target atoms packed, reducing file packets from 5 to 3. Each dilution target likewise remained a one-atom deep packet; B emitted only one-atom events and stayed at 3 packets, while C packed two non-target light hunks and also stayed at 3. The target comparisons themselves did not change path, coverage, or requested lenses. + +The failed report had three independent artifact-contract defects, now fixed with focused regressions: + +- `paid_summary_reconciliation` failed six times because attention records were compared to filename-sorted packet artifacts by array index. Reconciliation now requires unique IDs and joins by `packetId`, so producer order is irrelevant but duplicates/missing packets still fail. +- `paid_evidence_relations` failed twice because the verifier admitted only Stage 7/8 producers even though real uncertainty promotions are intentionally produced at Stage 9. Stage 9 is now admitted only when `provenance.source === "uncertainty_promotion"`; an arbitrary Stage 9 candidate still fails. +- `a_lens_route_join` failed twice because it treated intentional Stage-6 pruning from the A planner request as packing loss. The actual packing contract remains strict: event requested signatures must join the Stage-5 decisions, and B/C routed lenses must exactly equal the union of their A source atoms' already-routed lenses. + +Re-running the report locally over the immutable cohort after those fixes removes all ten artifact-contract failures and leaves 17 treatment failures: four `requested_lens_join`, three `treatment_invariant`, two `effective_profile_downgrade`, one `routed_lens_join`, four `insufficient_treatment`, and one each of `hunk_bijection`, `unknown_source_atom`, and `atom_bijection`. These are not waived. The consistency planners varied the non-target coverage, requested lenses, and relationship hints across A/B/C, changing an A atom from normal to light and A standalone profiles from investigate to simple in packed arms. Dilution C changed an A-skipped hunk to light and changed the normal hunk's requested lenses, so its atom universe could not join A. Those are real independent Stage-5 samples. The four target-treatment misses also exposed a fixture/config defect: an obvious target regression was predictably assigned deep/tests attention while safe mechanical siblings were assigned normal/light attention, but packing correctly requires identical coverage and requested-lens signatures. + +That 17-failure diagnostic rerun occurred before changing the declared case files. After the deterministic fixture repair below, the historical CLI now stops earlier with `declared_case_join`, as designed, because the current YAML hashes no longer match invocation `ace65769...` and its persisted snapshots. Do not edit the old manifest or logs to bypass that provenance check; the immutable original report and case snapshots remain the authority for the invalid cohort, and the pending retry will create a new invocation with the repaired declarations. + +The deterministic repair keeps both required failure classes while reducing those avoidable compatibility axes. All six current cases enable only `lang/go`. In `cross-atom-consistency`, every separated sibling now contains the same safe `Amount == 0` to `Amount <= 0` boundary hardening while four also add the shared validation veto and `HandleWire` still omits it. In `dilution-control`, all four separated hunks are comparator-boundary changes: the target retains the inclusive-limit regression, while the attempt normalization, integer-equivalent retry guard, and equality-equivalent shard cap remain safe sibling changes. The original paid case snapshots/logs are untouched. + +The repaired suite passes strict six-case parsing and arm-structure comparison. Both fixture bases and both materialized main/feature repositories pass `go test ./...`. A no-model scripted normal-coverage validation proves: + +| Case | A packets / target atoms | B packets / target atoms | C packets / target atoms | Routed lens | Profile | B base budget | C effective budget | +| --- | --- | --- | --- | --- | --- | --- | --- | +| `cross-atom-consistency` | 5 / 1 | 1 / 5 | 1 / 5 | `lang/go` | `standard` | 4 calls, 2 rounds, 10,000 chars | 7 calls, 2 rounds, 17,500 chars | +| `dilution-control` | 4 / 1 | 1 / 4 | 1 / 4 | `lang/go` | `standard` | 4 calls, 2 rounds, 10,000 chars | 7 calls, 2 rounds, 16,000 chars | + +That validator observed zero model calls and passed exact hunk/atom order and bijection, five-hunk/12K caps, normal coverage, requested/routed lens, standalone/effective profile floor, base/effective budget, target membership, and dispatch-rank checks. It proves the repaired deterministic preconditions, not real-planner treatment. A second paid repeat-one cohort is still required and remains pending outside Phase 3. + +One-repeat recall from the invalid cohort is retained only as measurement: consistency A/B each hit the final expectation while C did not; dilution A hit candidate/final, B missed before candidate generation despite reviewing the target packet, and C generated a candidate that was rejected at verification. These samples do not select B or C and do not decide rollout. Plan 102 steps 8–12 are `not_run` because the cohort is treatment-invalid, not because packing has received a rollout verdict. No case was changed to repeat 10, no later paid suite ran, no arm was selected, and no experiment flag or product path was torn down. + +### Phase 4 repaired one-repeat retry and failed gate (2026-07-25) + +Phase 4 retried Plan 102 step 7 exactly once and stopped at the failed report gate. The runtime was exact clean commit `bb96fd3439c715130756a93efd8e679772f81a9b` on branch `next`, with `git status --porcelain=v1` empty, Node `v26.5.0`, pnpm `11.15.1`, and launch preflight timestamp `2026-07-25T13:06:52Z`. The draft phase note was stashed before launch and restored only after the paid process and report completed. The documented `pnpm dev -- eval ...` spelling passed a literal `--` through pnpm 11 and was rejected by Commander before suite allocation; it created no invocation, run directory, model call, or spend. The one actual invocation used the equivalent pnpm-11 spelling `pnpm dev eval --eval-dir /home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/recall --no-cache`. + +Immediately before the paid call, the repaired suite strictly parsed as six cases with `repeat: 1`, cache disabled, `lang/go` as the only lens, and the exact Phase 3 provider/model/reasoning/concurrency/depth/time/token/cost settings. Within each family, structural comparison found only the case-name suffix and the two permitted A/B/C review-field differences. The declaration SHA-256 values were: + +| Case | SHA-256 | +| --- | --- | +| `consistency-a.yml` | `15702c3e5c8c2d97845b39ff04618d26966174934ad1afe9765bdb7710bde590` | +| `consistency-b.yml` | `5b0e093db08b9cee8837d04d5236ceabd30727903ef18e13c74208022bca05bb` | +| `consistency-c.yml` | `69130fa21479fe25acd881bff1fc9e914ff470cb5efb70e89c1e70bb5f4e01c3` | +| `dilution-a.yml` | `fe2802d21333f6650fe4c4fc1a5685574638fe6036f19b6d8bfbcad088fbb871` | +| `dilution-b.yml` | `00a8d51ea6aee14c9ef39105c3bd7514ba6167ac642b35dc9b45114f67bdd757` | +| `dilution-c.yml` | `87883f0210d92551da43b860f47b98a169609ff902102b21b28f853039be12ef` | + +The six fixture source files had aggregate SHA-256 `d486e2601a1ea05f0872794be71f409b5296b78c2998ef39e973eedb57061da4`. Both materialized base and feature revisions passed `go test ./...`. The no-model validator observed exactly zero model calls and no failures. With normal coverage and the declared `lang/go` route, it reproduced consistency A `5` packets/one target atom versus B/C `1` packet/five target atoms, and dilution A `4`/one versus B/C `1`/four. Exact hunk/atom order and bijection, five-hunk/12K caps, coverage, requested/routed lenses, standalone/effective profile floors, base/effective budgets, target membership, and dispatch ranks all passed. This reconfirmed only deterministic fixture eligibility; it did not predict independently sampled Stage-5 plans. + +The sole live retry invocation was `5bd80f2c-865e-40f0-a605-07387138b904`, started `2026-07-25T13:10:14.113Z` and completed `2026-07-25T13:26:22.430Z`. Its exact log root is `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/recall/logs`. It completed all six cache-off executions with zero provider/configuration errors, zero incomplete reviews, zero budget violations, and 71 model calls: + +| Run | Case | Review run ID | Eval score | Model calls | Actual cost USD | +| ---: | --- | --- | --- | ---: | ---: | +| 7 | `consistency-a` | `20260725-131014-ff7643a9` | fail, 0/2 expectations | 13 | 0.669039 | +| 8 | `consistency-b` | `20260725-131217-78051528` | fail, 0/2 expectations | 11 | 0.564235 | +| 9 | `consistency-c` | `20260725-131407-46ee2073` | fail, 0/2 expectations | 11 | 0.618305 | +| 10 | `dilution-a` | `20260725-131627-fd970500` | fail, 1/2 expectations | 12 | 0.614135 | +| 11 | `dilution-b` | `20260725-131904-9623b816` | pass, 2/2 expectations | 13 | 0.713651 | +| 12 | `dilution-c` | `20260725-132212-896d115e` | fail, 1/2 expectations | 11 | 0.806856 | + +The exact report command selected that invocation UUID, not `latest`: `pnpm exec tsx scripts/packet-packing-report.ts eval --logs /home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/recall/logs --cohort 5bd80f2c-865e-40f0-a605-07387138b904 --expected-repeats 1 --output /tmp/plan102-eval-preflight.json`. It exited `1` with exactly six failures: consistency B and C were each treated `0/1`, and one B plus one C packet in each family failed the composite `treatment_invariant`. There were no other report failure codes. + +The treatment diagnosis is exact: + +- Consistency A produced five packets: one normal/investigate atom, three light/simple atoms, and the target `HandleWire` deep/investigate atom. B and C packed only the three non-target light atoms, reducing five packets to three, while the target stayed a one-atom deep packet. Both target treatments are therefore invalid at `0/1`. On the packed non-target packet `e33a8a1d...`, the independent B/C planner hints made all three standalone profiles `investigate`; the corresponding A atoms were `simple`. The event's profile-floor fields consequently could not exactly reconcile to the A atoms, producing one composite invariant failure in each packed arm. +- Dilution A produced deep/investigate, normal/investigate, light/simple, and deep/investigate atoms. B and C validly combined the target first deep atom with the last deep atom, reduced four packets to three, and treated the target at `1/1` with two source atoms. A separate one-atom light packet `3989cd85...` nevertheless changed from A `simple`/zero-tool to a B/C `investigate` standalone profile due independently sampled planner hints. Its event profile-floor fields likewise could not exactly reconcile to A, producing one composite invariant failure per packed arm. + +Thus caps, hunk/atom joins, coverage, requested/routed lenses, effective profiles/budgets, dispatch ranks, artifact lineage, summary reconciliation, and spend evidence produced no separate failures; the gate failed specifically on the two missing consistency target treatments and cross-arm standalone-profile reconciliation. No report check was waived. The failure is not a rollout verdict from recall, and there will be no third paid one-repeat invocation. + +One-repeat recall remains measurement only. All three consistency executions generated and published the missing-wire-validation finding as category `security`, while the declared expectation required `correctness`, so each recorded candidate/final partial matches rather than a hit. Dilution A and C hit the candidate expectation but were rejected at verification; dilution B hit both candidate and final expectations. These six samples do not select B or C and do not establish packing safety or harm. + +The report's authoritative reconstructed `retryPreflightCostUSD` is `$3.986221`; the unrounded sum of its 71 model-call records is `$3.986220`, with the `$0.000001` difference coming from the report's per-run six-decimal reconciliation. Cumulative `actualValidationCostUSD` is `$5.207657 + $3.986221 = $9.193878`. Because the retry is not valid, `validPreflightCostUSD` and therefore the required `10 * validPreflightCostUSD` repeat-10 projection are **not established** and cannot authorize step 8. For comparison only, `10 * retryPreflightCostUSD = $39.862210`; including the known later `$95` production-pair reservation would give `$9.193878 + $39.862210 + $95 = $144.056088`. The conservative Phase 3 placeholder remains `$52.076570`, yielding known projected remaining spend of `$147.076570` and cumulative actual plus projection of `$156.270448 <= approvedValidationCostUSD: $500`, leaving `$343.729552` before deferred real-model collateral. Fake-provider collateral remains `$0`. None of these accounting projections authorize a later paid phase after the failed gate. + +The failed retry report is preserved byte-for-byte at `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/reports/plan102-eval-preflight-retry-invalid-5bd80f2c.json`, SHA-256 `650113d24d092e6fd712303e1828b297010c76f672d7efb799f5081f79635517`. The new run/manifests aggregate SHA-256 is `dabb5d4700a0fe559490216da73d335bd9f28cec6d3c004d8b41b4cb7c77c3c1`. The original runs 1–6 plus manifest retain aggregate SHA-256 `2a3232651752a4f3652334d5126e2eebca6169da658e854214b6f98a158eb750`, and the original invalid report remains SHA-256 `3368178d57d6c4c46865fccb52b8aa160d1f9acf3536be9e1cde4d05d52c50ab`. No historical log, manifest, or report was edited or deleted. + +Plan 102 steps 8–11 are `not_run` because the repaired retry gate failed. Step 12's failure teardown is also `not_run` in Phase 4 and is reserved for the next separately reviewed phase; no case was changed to repeat 10, no arm was selected, no collateral or production suite ran, no rollout verdict was made, and no experiment code or evidence was torn down in this phase. + +### Phase 5 failed-outcome teardown and final gate (2026-07-25) + +Plan 102 is complete through the explicit failed-outcome branch, not through rollout. The repaired retry never established a valid preflight: consistency B/C each treated the target `0/1`, and independently sampled planner hints prevented exact A-to-B/C standalone-profile reconciliation in both case families. No report condition was waived and no third paid attempt was made. The product decision is therefore to reject this packing design for this iteration and restore the original baseline path. + +Before teardown, every JSON report actually produced by the completed phases was preserved under `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/reports/`: + +| Preserved report | SHA-256 | +| --- | --- | +| `plan102-packet-shape.json` | `77af0c38937bd6957806f05ad201cbad32461414c1d667824096208473c276fa` | +| `plan102-eval-preflight-invalid-ace65769.json` | `3368178d57d6c4c46865fccb52b8aa160d1f9acf3536be9e1cde4d05d52c50ab` | +| `plan102-eval-preflight-retry-invalid-5bd80f2c.json` | `650113d24d092e6fd712303e1828b297010c76f672d7efb799f5081f79635517` | + +`reports/manifest.sha256`, itself SHA-256 `7c6a1dfb923aea7a36986e50c40068eb878688611f22bc31e4c3c20128970667`, covers exactly those three report basenames. `sha256sum -c manifest.sha256` passes for all three. The deterministic replay was copied byte-for-byte from `/tmp/plan102-packet-shape.json`; the two existing invalid-preflight reports were hash-verified and never rewritten. No repeat-10, collateral, or production-capacity report exists because those phases were not reached. + +Both paid invocations use the immutable log root `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/recall/logs` and retain their invocation manifests and run artifacts unchanged: + +| Invocation UUID | Eval runs | Review run IDs | Authoritative cost USD | +| --- | --- | --- | ---: | +| `ace65769-3a68-4bd1-bd36-662fa6827dc6` | 1–6 | `20260725-120233-6c490c44`, `20260725-120706-36842f4b`, `20260725-121006-87a8ed77`, `20260725-121205-5a020015`, `20260725-121737-6249976f`, `20260725-121917-4c7e8289` | 5.207657 | +| `5bd80f2c-865e-40f0-a605-07387138b904` | 7–12 | `20260725-131014-ff7643a9`, `20260725-131217-78051528`, `20260725-131407-46ee2073`, `20260725-131627-fd970500`, `20260725-131904-9623b816`, `20260725-132212-896d115e` | 3.986221 | + +Final cumulative `actualValidationCostUSD` is `$9.193878`, below the owner-approved `approvedValidationCostUSD: $500`. There is no projected remaining validation spend: step 8 (repeat-10 recall), step 9 (deterministic and real-model collateral), step 10 (production-capacity pair), and step 11 (arm selection and rollout documentation) are all `not_run` because the repaired step-7 treatment gate failed. Their absent reports and `$0` later-phase spend are intentional, not missing evidence. No paid model call occurred during teardown or the final repository gate. + +Step 12 completed through the failure branch. The same-file packing pass, atom-scaled and base experiment modes, both temporary config fields and eval/config plumbing, experiment-only atom wrappers/IDs/provenance/profile floor/telemetry, report script and tests, and dead artifact-reconstruction exports/metadata were removed. `scripts/`, `src/`, and `tests/` now match the pre-experiment product baseline at commit `5aca256` for every Plan 102 code path, restoring the original `hunkFirstGroups()` packet behavior and ordinary packet tool budget rather than retaining a dark alternate path. No golden packet-packing fixture was created. + +The private active suite no longer contains A/B/C case YAMLs. B/C declarations were retired with the experiment. `consistency-a` was also removed because its repaired baseline run met `0/2` declared expectations, and `dilution-a` was removed because it met only `1/2`; neither satisfied the suite's existing all-expectations-pass policy. Both fixture repositories under `recall/repos/`, all twelve run directories, both invocation manifests, and every log/report artifact remain unchanged. No production or collateral case was created merely for teardown. + +The exact step-12 evidence, retired-field grep, report-absence, strict-suite no-model parse, and focused baseline tests pass. Step 13's complete `pnpm run check`, `pnpm test`, and `pnpm build` gate plus `git diff --check` also pass. This completed status records a failed experiment with verified baseline restoration; it does not claim rollout, recall safety, throughput gain, or production economics. + +## Phase 2 deterministic reconciliation (2026-07-24) + +Step 6's exact four-run command completed with exit `0`, `noModelCalls: true`, four explicit `modelCallsObserved: 0` row proofs, and no report failures. The frozen output is `/tmp/plan102-packet-shape.json` (SHA-256 `77af0c38937bd6957806f05ad201cbad32461414c1d667824096208473c276fa`). The authoritative motivating count is **75**: the real `combinedPatchChars()` builder produced 96→75, so the bounded 74/76 reconciliation was not used. The report's diagnostic reapplication of the old `contentWithLineNumbers` proxy produces 76 for the current atom stream while the real measurement produces 75; this does not change the golden because the authoritative builder result itself matches the plan's historical 75 target. The frozen hash was refreshed after the verifier stopped trusting serialized cap telemetry and independently rendered the source diff for every packet that actually combines multiple atoms. + +| Retained run | Packets off→on | Reduction | Hunks | Multi-atom packets | Hunk distribution off→on (1/2/3/4/5) | Atoms per on-packet (1/2/3/4) | +| --- | ---: | ---: | ---: | ---: | --- | --- | +| `740d73f2` | 109→85 | 22.018% | 153 | 16 | 86/12/5/2/4 → 54/13/6/5/7 | 69/9/6/1 | +| `fe1548ae` | 109→85 | 22.018% | 153 | 16 | 86/12/5/2/4 → 54/13/6/5/7 | 69/9/6/1 | +| `81f806a6` | 93→68 | 26.882% | 136 | 17 | 71/11/5/2/4 → 38/12/6/4/8 | 51/10/6/1 | +| `dca8d870` | 96→75 | 21.875% | 142 | 14 | 73/11/5/3/4 → 46/12/4/5/8 | 61/8/5/1 | + +All four rows have zero hunk/atom bijection failures, atom splits or reorderings, new coverage promotions, cap violations, planner-lens drops, high/critical focus omissions, deep-context downgrades, derived/effective profile downgrades, effective base-budget downgrades, and invalid dispatch ranks. The largest eligible packet has five hunks; independently rendered raw-diff patch use for every multi-atom packet is at most 11,755 characters in each run. Oversized single-hunk packets are intentionally windowed and are not atom-combination cap candidates. Flag-off and flag-on context truncation counts are identical (13/13, 13/13, 13/13, and 15/15). Each run internalizes two related-context excerpts because their target hunk is now in the same packet. Six standalone note strings per run are absent after the unchanged bounded note merge, but none belongs to a high/critical atom; this remains an explicit paid-recall concern rather than being hidden as deterministic parity. Lens omissions are zero. All treatment packets use the `base` budget and the eight direct file/whole-file bypass packets per run retain their baseline budget. + +Packing changes the complete scheduling position of 129, 129, 129, and 117 hunks respectively, while every recomputed rank satisfies the unchanged `[fileClassRank, -changedLines]` formula. The motivating row's 75 packets at concurrency 6 versus 96 at concurrency 4 gives a capacity multiplier of `(96 / 75) * (6 / 4) = 1.92×`. Its completion projection is `52.7 * (75 / 57) * (4 / 6) = 46.23` minutes. + +The motivating run has byte-identical flag-off artifacts after applying the same credential stripping used by artifact serialization. The three older retained artifacts predate Plan 100 and omit `dispatchRank`; the replay applies an explicit run-ID-pinned compatibility view that removes only that absent serialized field after validating every current rank. Those same three recorded diffs predate persisted `hunkHash` and the current derived hunk ID. Their versioned diff parity requires every hunk to use the old schema, then compares every other parsed field exactly against fresh `parseDiff(rawDiff)` output; partial migration or any semantic diff drift fails closed. The oldest clean artifact also predates the self-test context filter, so 16 exact redundant `path:path` test lines are removed in that compatibility view. Any other field drift still fails closed, and the current packing-off implementation remains covered by the packet-builder golden parity tests. + +Paid validation was not authorized or run in this phase: `actualValidationCostUSD = $0`; steps 7–12 are `not_run` pending an owner-supplied `approvedValidationCostUSD`. No paid model call may begin until that ceiling and the next-phase projection are recorded. + ## Decision Pack more same-file work, but only across boundaries that do not carry a deliberate review signal: @@ -202,7 +345,7 @@ Use staged measurements instead of guessing the final bill: 3. The two production cases run as one paired suite, so approve that phase before launching either case. Run D cost `$25.2852` in total: `$22.8517` in Stage 7 across 57 dispatched packets (`$0.4009` per dispatched packet) plus `$2.4335` outside Stage 7. For frozen packed count `P`, forecast a complete pair as `2 * $2.4335 + (96 + P) * $0.4009`, or `$73.02–$73.82` for `P in {74, 75, 76}`. Applying 25% contingency yields `$91.28–$92.28`; round upward and reserve **`$95`**. This budgets all 96 baseline packets plus all packed packets even if the 60-minute baseline later truncates. Replace the reservation with actual paired cost afterward. 4. Before starting any next phase, require `actualValidationCostUSD + projectedRemainingCostUSD <= approvedValidationCostUSD`. If it does not hold, stop for explicit owner approval rather than dropping recall or deterministic gates to save money. -The report script must aggregate cohort cost from `score.metrics.costUSD`, falling back to the actual value of the `maxCostUSD` budget result when necessary. It emits cohort `actualCostUSD`; the executor records cumulative actual/projected/approved amounts in the reconciliation note using the formulas above. +The report script must reconstruct cohort cost from every recorded model call and require the per-repeat and aggregate score cost/count fields to match that evidence. Candidate, verification, final-selection, and final artifacts must also form an exact lineage before any paid score is accepted. It emits cohort `actualCostUSD`; the executor records cumulative actual/projected/approved amounts in the reconciliation note using the formulas above. Normalize production payback to equivalent reviewed work because the baseline may truncate while the selected arm completes. For the pinned diff, set `equivalentTargetHunks = 142` and compute: @@ -270,7 +413,7 @@ Author six case files (`dilution-{a,b,c}.yml`, `consistency-{a,b,c}.yml`) with ` | B — isolate shape | on | base | | C — shape + capacity | on | atom-scaled | -Run the one-repeat treatment preflight, then use `scripts/packet-packing-report.ts eval` on the suite logs. For both cases it must prove: +Run the one-repeat treatment preflight, then use `scripts/packet-packing-report.ts eval` on the suite logs. Preflight proves treatment and economics only and must not select a rollout arm. Invocation manifests record each run's exact owning log root/path so a suite may span roots without ambiguous numeric run IDs. For both cases it must prove: - A's target packet has one source atom; - B and C put the target hunk in a packet with at least two source atoms; @@ -302,7 +445,7 @@ Define Stage-7 tool-pressure rate per arm as: rejectionRate = rejected repository-tool attempts / reviewed pre-existing source atoms ``` -Report the rejection cause/limit when telemetry supplies it, result characters, used calls, continuations, and model-service time per reviewed atom. C qualifies as a budget fix only when `rejectionRate(C) <= rejectionRate(B)` and it either strictly reduces rejection rate or improves candidate/final recall. If C and B have equal recall and pressure, choose B. If the selected packed arm exceeds A by more than `0.10` rejected attempts per reviewed atom without higher candidate recall, stop and keep packing dark. +Report the rejection cause/limit when telemetry supplies it, result characters, used calls, continuations, and model-service time per reviewed atom. C qualifies as a budget fix only when `rejectionRate(C) <= rejectionRate(B)` and it either strictly reduces rejection rate or improves candidate/final recall. If C and B have equal recall and pressure, choose B. If B fails and C passes, C must retain at least 85% of the model-service saving implied by its packet-count reduction. If the selected packed arm exceeds A by more than `0.10` rejected attempts per reviewed atom without higher candidate recall, stop and keep packing dark. ### C. Existing-suite collateral regression checks @@ -313,7 +456,7 @@ After the repeated packing-sensitive gate selects B or C, run two broader suites Materialize A and selected copies in disposable directories, copying the complete suite including `repos/`, and change only `repeat`, `review.packSameFileHunks`, and `review.packedToolBudgetMode`. The report must verify those are the only YAML differences, that the selected setting reached resolved config, and how many selected executions actually packed multiple atoms. Untreated cases remain valid collateral checks for flag/config parity but are explicitly not evidence about packing recall. -Add a `regression` mode to `scripts/packet-packing-report.ts` that compares the two explicit log roots and reports eval errors, expectation transitions, packet/hunk/cap/profile invariants, treatment counts, tool pressure, and dispatch-order changes. It must not merge these one-repeat outcomes into the repeated packing-sensitive recall gate. +Add a `regression` mode to `scripts/packet-packing-report.ts` that compares the two explicit log roots and reports eval errors, expectation transitions, packet/hunk/cap/profile invariants, treatment counts, tool pressure, and dispatch-order changes. Each baseline and selected cohort must independently pass every required positive/negative expectation even when both fail identically. It must not merge these one-repeat outcomes into the repeated packing-sensitive recall gate. ### D. One production-shaped capacity confirmation @@ -323,7 +466,7 @@ Confirm: - at least the same hunk set is reviewed, with the target being all 142 reviewable hunks; - accepted/candidate findings and known cross-hunk observations are not lost; -- total model-service time, total tokens, cost per reviewed hunk, and wall time improve; +- total model-service time, total/reasoning tokens, cost per reviewed hunk, and wall time improve, with totals covering planner, reviewer, verifier, composer, repair, and every other recorded call rather than Stage 7 alone; - raw arm costs/reviewed-hunk counts and 142-hunk equivalent costs are reported, with truncation extrapolations labeled and positive equivalent-review savings required; - context/tool pressure stays within the offline and eval gates; - profile floors hold, and dispatch-order plus reviewed-hunk-set changes are reported so a Plan-100 ordering interaction cannot masquerade as a packing-quality result. diff --git a/specs/plans/103-issue-103-relationship-driven-packet-packing.md b/specs/plans/103-issue-103-relationship-driven-packet-packing.md new file mode 100644 index 0000000..9286503 --- /dev/null +++ b/specs/plans/103-issue-103-relationship-driven-packet-packing.md @@ -0,0 +1,660 @@ +# Issue 103: Compatible-Atom Packet Packing and the Packet-Size Recall Curve + +Status: PENDING +Related: Plan 102 (`102-issue-102-same-file-packet-packing.md`) — COMPLETE as a failed treatment gate; its deterministic packing mechanism passed every invariant and is adopted here unchanged. Plan 92 (coverage calibration), Plan 100 (dispatch rank), Plan 79 (repeat/recall harness), Plan 32 (adaptive Stage-6 symbol context). +Planned from: Plan 102's preserved evidence, plus artifact-only diagnostics reproducible with `scripts/packing-diagnostics.mjs`, 2026-07-25 +Production replay refs: base/merge-base `d1c49bdf6a8002ec2ec27faac94a932d736532b2`; head `fbb5f8761c2c296e115af17e919a7c35d9de8373` +Planned at: commit `32d7b83` (branch `next`) +Recommended priority: after Plan 101's paid A/B settles. + +## Reconciliation (2026-07-25) + +Status: **implemented and shipped dark; synthetic recall curve abandoned on structural grounds; production A/B authorized in its place.** + +**What passed.** Phases 1–3 are complete and committed. The free replay gate cleared every pre-registered threshold on real production diffs, with zero model calls: `dca8d870` 96→75 packets (21.9%), fixed-slot hunk yield 89→**109** at 56 slots against a ≥102 bar, estimator reconciling at exactly 89, deviation from Plan 102's frozen counts of **0**, and zero coverage changes, profile or budget downgrades, lens drops, cap breaches or hunk loss. `81f806a6` and `740d73f2` reduce 26.9% and 25.8%. Report preserved at `packet-dilution/reports/plan103-replay.json`. + +**What did not.** Step 8's synthetic recall curve never ran. Its realism gate failed on two independent fixture designs, and Plan 102 failed the same way on two more. In all four the planner graded the seeded defects `deep` and their safe siblings `normal`/`light`, partitioning the targets away from the hunks they were meant to pack with. + +**The conclusion is structural, not a fixture-craft failure.** The compatibility predicate separates atoms by coverage; the planner assigns coverage by detected defect. A synthetic fixture therefore cannot manufacture "a defect packed among safe siblings at equal coverage" — any bug findable enough to measure recall against is also visible enough to be graded `deep`. Making the second fixture uniformly money-critical tested and falsified the alternative hypothesis that coverage tracks risk surface rather than detected defects. + +**Owner decision, 2026-07-25: ship behind the flag and A/B on real pull requests instead.** This is a deliberate, recorded exception to this plan's teardown rule that no dark path survives the decision, taken because real PRs contain the population synthetic fixtures cannot: hunks the planner grades ordinarily that nonetheless contain defects. That population is where packing's benefit comes from — on the motivating run the planner issued coverage for 32 of 142 hunks, so the 21.9% reduction is overwhelmingly the packing of default-`normal` atoms the planner never singled out. A production A/B observes the treated population directly rather than by construction. + +**Conditions attached to the exception.** `review.packRelatedHunks` stays `false` by default. It is reachable from exactly two places: an eval case, and the per-run CLI flag `--pack-related-hunks` / `--no-pack-related-hunks` added for this A/B. It remains absent from `rawConfigSchema` and repo-safe filtering, so **no `codegenie.toml` and no user `config.toml` can set it** — a repository cannot enable experimental packing for everyone who reviews it. Adding the CLI flag brings `src/cli/` into scope, which this plan originally excluded; that exclusion was written when the setting had no product consumer, and the approved production A/B is one. It is not a supported product surface, and the plan's Non-Goals still forbid it becoming one. **Review date: the flag is either promoted to unconditional behaviour or deleted once the production A/B reports; it does not persist unreviewed.** If it is deleted, the teardown branch of step 9 applies unchanged. + +**Paid validation: `$5.3850`** of an owner-approved `$300` ceiling. Measured cost was `$1.5154` per execution against `$0.78` projected. Steps 8–11 are `not_run`: step 8 abandoned for the reason above, steps 9–11 never authorized because the gate preceding them never passed. + +**Preserved evidence**, `packet-dilution/reports/`: `plan103-replay.json`, `plan103-treatment-proof.json`, `plan103-affinity-pairs.json`, `plan103-cap-sweep.json`, `plan103-ordering-comparison.json`, each hashed in `manifest.sha256`. Plan 102's manifest is unmodified. Both fixture repositories are retained under `evals/packet-dilution/repos/` as the record of why synthetic measurement was abandoned. + +## Introduction and TL;DR + +### The problem + +Reviews run out of time before they run out of code. On the motivating pull request, Stage 6 produced **96 packets** covering 142 changed hunks. Only 56 packets were dispatched before the deadline, so **53 hunks received no review at all**. + +A packet is one conversation with the model, and most of its cost is fixed — the skill text, the prompt scaffolding, the forced finalization at the end. Measurements from that run show a five-hunk packet costs roughly the same as a one-hunk packet: about 3 model calls and 228 seconds either way. Yet 73 of the 96 packets held a single hunk. + +### What this plan achieves + +Two things. + +**First, it lands Plan 102's packing mechanism.** Group compatible hunks from the same file into fewer, slightly larger packets — about **21% fewer packets**, measured deterministically across four production runs with no model calls. The point is not a shorter review but a **more complete** one: at the same time budget, roughly **108 of 142 hunks reviewed instead of 89**. The win is coverage. + +**Second, and more durable, it produces the recall-versus-packet-size curve.** Nobody has ever measured whether a hunk sharing a packet gets reviewed as carefully as a hunk reviewed alone. Every packing proposal — this one, Plan 102, any future cap increase or cross-file scheme — rests on assuming that answer. This plan measures it once, and the curve becomes a standing asset. + +### What it does *not* do, and why + +An earlier draft proposed packing by tree-sitter relationships instead of by planner compatibility, and then as an ordering preference within compatibility. Both were measured against recorded artifacts and rejected: + +- **As a membership predicate**, relationship-required packing yields 4 combinable atom pairs and a 4.2% packet reduction, against compatibility's 21.9%. Relationships between *distinct* atoms are sparse because today's grouper already absorbs most same-symbol adjacency into atoms. +- **As an ordering preference**, it produces packing byte-identical to plain source order on all three distinct retained diffs — same packet counts, same composition. Related atoms are usually already source-adjacent, so preferring them picks the same atom. + +So there is no affinity view, no container identity, and no relationship ordering in this plan. `scripts/packing-diagnostics.mjs` reproduces both measurements, and the outputs are preserved. + +### How the machinery works + +Five pieces, in the order they run inside Stage 6: + +1. **Atoms.** Today's grouper already merges hunks that share an enclosing symbol or sit within 30 lines. We keep it as-is and treat each group as an unbreakable unit. Nothing here ever splits one. + +2. **Partitions.** Within each file, atoms are bucketed by two things the planner already decided: review depth (`light`/`normal`/`deep`) and the set of lenses requested. Only atoms in the same bucket may combine. This guarantees no hunk is reviewed more shallowly, or with different expertise, than planned. It does **not** guarantee equal cost — see below. + +3. **Filling.** Walk each bucket in source order, filling a packet until it hits 5 hunks or 12,000 characters, then start another. + +4. **Context for every member.** If a packet holds three functions, the reviewer gets all three function bodies under a shared, explicitly budgeted allowance. Today's code reads only the top-ranked one, which would quietly leave the others as bare diffs. This is the one place Plan 102's mechanism is genuinely improved rather than reused. + +5. **Safety checks before committing.** Each candidate packet is built in a scratch space first. If merging would drop a lens, drop an important planner note, or squeeze any member's source below a usable minimum *after final rendering*, the merge is abandoned and those atoms stay separate. + +Packing can make a hunk **more** expensive than it would have been alone: the profile floor may lift a `standard` atom into an `investigate` packet's tool budget, and multi-member context deliberately adds input tokens. That is a deliberate trade for coverage, and both effects are reported rather than assumed away. + +### The risk, and how it gets tested + +**Attention dilution.** A bug in a five-hunk packet might get less scrutiny than the same bug alone. + +One test repository, fifteen separated changes in one Go file, **three** independent bugs placed so that no two share a packet at any tested cap, plus a safe-looking control. The same review runs at packet caps 1, 3, and 5, six repeats each, all against one hand-authored frozen plan so packet size is the only variable. Three bugs times six repeats gives **18 recall opportunities per arm** rather than 6 — the same spend buys three times the resolution. Those opportunities are *clustered*, not independent: repeats share a fixture and an authored plan, so they are treated as three per-bug series rather than 18 free trials, and recall is reported per bug as well as in aggregate. + +The pass bar is derived, not chosen: packing must not cost more per-hunk recall than the coverage it buys. If the replay shows packing reviewing 108 hunks where the baseline reviews 89, per-hunk recall may fall no further than 89/108 ≈ **82.4%** of baseline before the whole thing is a net loss. + +Cost: about **$14** to answer that, and a $50 production-scale confirmation that is **required once the curve passes**, not optional. + +### The outcome, either way + +Either packing becomes permanent unconditional behavior and the temporary switches are deleted, or packing is removed entirely and the code returns to today's baseline. No flag survives, no dark path is left behind. + +> **Revision history.** Draft 1 (`7b61b34`) proposed a relationship membership predicate. Draft 2 (`4935134`) demoted relationships to ordering after diagnostics showed 4.2% versus 21.9%. This draft removes relationships entirely after measuring ordering as a no-op, and rebuilds the paid experiment after review found it could repeat Plan 102's zero-treatment failure and could pass a net-negative outcome. + +> Executor instructions: adopt Plan 102's predicate, caps, and fill order unchanged — its deterministic gate passed on four runs and this plan does not relitigate it. The new engineering is multi-member symbol context and transactional rejection. The new evidence is the packet-size recall curve. Do not add relationship signals, container identity, coverage promotion, tool-budget arms, or cap increases; each was measured or reasoned out and has an entry in Non-Goals. +> +> **Commit references are rebase-unstable.** The Plan 102 series is currently `5551547`, `8fceba9`, `7ebfd2f`, `87a5a10`, `f372f73`. Locate referenced work by commit subject when a hash does not resolve. +> +> Drift check: `git diff --stat 32d7b83..HEAD -- src/pipeline/packet-builder.ts src/pipeline/review-runner.ts src/config/schema.ts src/config/config-loader.ts src/types.ts src/evals/eval-runner.ts scripts/packet-packing-report.ts tests/pipeline-phase5.test.ts tests/evals.test.ts` +> Working-tree check: `git status --short -- src/ evals/ scripts/` + +## Decision + +- Run the existing grouper unchanged; its groups are indivisible **atoms**. +- Partition each file's atoms by `(effectiveCoverage, normalizedPlannerLensSignature)` — Plan 102's predicate. Zero coverage promotion by construction. +- Fill greedily in source order under the unchanged `MAX_HUNKS_PER_PACKET = 5` and `MAX_PATCH_CHARS = 12_000`. +- **Source order** means: hunks are ordered within a packet by file position, and packets are ordered by their earliest member hunk. Because partitions are non-contiguous, the globally flattened hunk sequence is *not* preserved across packets. That is expected and is not a defect. +- Read enclosing-symbol source for **every** member atom under an explicit shared budget. +- Abandon any candidate that would lose a lens, a high-priority planner focus note, or a member's usable symbol source after final rendering. +- Retain Plan 102's monotonic profile floor and the unchanged `packetDispatchRank()` formula. +- Two temporary settings, **eval and internal only** — not in the repo TOML schema, not repo-safe filtered, not user-facing. Both deleted at teardown. +- Ship only if the measured packet-size recall curve clears a break-even bar derived from the measured coverage gain. + +## Evidence + +### Plan 102's mechanism passed its deterministic gate + +Four runs, zero model calls, zero failures: `740d73f2` 109→85, `fe1548ae` 109→85, `81f806a6` 93→68, `dca8d870` 96→75 — 21.9–26.9% fewer packets with zero coverage promotions, zero profile or budget downgrades, zero cap violations, zero lens drops, zero invalid dispatch ranks. Only the paid gate failed, and it failed on fixture treatment, never on recall. + +`740d73f2` and `fe1548ae` are the same commit, and all four runs are the same PR at three head commits. This is one PR, not four samples. Thresholds below count distinct diffs. + +### Relationship signals were measured and rejected + +Reproducible via `scripts/packing-diagnostics.mjs`; outputs preserved under `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-dilution/reports/`: + +| Artifact | SHA-256 | +| --- | --- | +| `plan103-affinity-pairs.json` | `2d0f6f2d14ce907822128e5338c2627e97fc9d495a528f807d71ffec0ac6bc76` | +| `plan103-cap-sweep.json` | `f9915c77210d329c2859531007f6175c90eaca45cb52417a22d8dbc3e08de371` | +| `plan103-ordering-comparison.json` | `e4e6bf2e3cfe585b1c04db1280a2bcb1c1bb5516975352b545fb96211bf24a57` | + +On `dca8d870`, 13 atom pairs are related but currently split: 6 blocked by the caps, 2 by coverage inequality, 2 by lens-signature mismatch, 3 eligible. On `81f806a6`, 11 pairs: 7 caps, 4 eligible. Relationship-required packing gives 96→92 (4.2%). Relationship-preferred *ordering* gives 75/66/83 packets across the three distinct diffs — identical to plain source order, including multi-atom counts. + +Both measurements use documented approximations (patch proxy, owner prefix from display strings, synthetic `DEFAULT` lens signature for planner-undeclared hunks). Neither is close enough to its threshold for the approximation to change the conclusion. + +### The fixed-slot estimator is validated against the real run + +The coverage claim is gated on a fixed-slot estimator rather than packet count, so the estimator itself was checked against `dca8d870`. `worker-runner.ts` sorts by `(priority, coverage, dispatchRank, input order)` and assigns worker IDs in that order, so dispatch order is directly recoverable from telemetry. + +**The estimator is a calibrated counterfactual, not an exact reproduction of the scheduler.** The real dispatched set is a prefix *with a hole*: worker `w7-056` was recorded `packet_review_not_dispatched` while `w7-057` completed. At concurrency 4 a later worker can clear its budget checkpoint before an earlier one fails, so the undispatched set is not a clean suffix and a future run could have more than one hole. + +Applying the sort to the 96 flag-off packets and taking the first 56 nevertheless yields **89 hunks — the same 89 the run actually reviewed** — with 55 of 56 packets in common; the one difference is exactly the `w7-056`/`w7-057` swap, and the two packets happen to carry equal hunk counts. That makes 56 slots a *calibrated capacity proxy*, validated against a known outcome, not a claim that the estimator replays scheduling. + +Counting completed reviews, 56 packets produced review outcomes against 96 built; 57 started. This plan uses **56 slots** as the completed-packet capacity, and the reconciliation gate below re-proves the 89-hunk calibration mechanically before the estimator is used for anything. + +### The 5-hunk cap is near-optimal + +Simulator validated against Plan 102's real implementation to within one packet. + +| Cap | `dca8d870` | `81f806a6` | `740d73f2` | Packets >5 hunks | +| --- | --- | --- | --- | ---: | +| 5h/12K (today) | 96→75 (21.9%) | 93→66 (29.0%) | 109→83 (23.9%) | 0 | +| 6h/12K | 96→75 | 93→65 | 109→81 | 2–4 | +| 8h/12K | 96→73 (24.0%) | 93→64 | 109→80 | 4–5 | +| 10h/16K | 96→71 (26.0%) | 93→61 | 109→77 | 5–6 | +| 12h/24K | saturated | | | | + +The predicate is worth ~21 points; the cap ~5 more before saturating, because partitions run out of compatible atoms rather than hitting the cap. Raising it trades oversized packets for a few points and enlarges the untested recall risk. **The cap stays at 5.** + +### The value is coverage, and the cost is not free + +Plan 102's Stage 7 spent 174 model calls, 5.321M tokens, `$22.85`, and 52.7 concurrency-normalized minutes on 57 dispatched packets, reviewing 89 of 142 hunks. Forced finalization alone was 34 calls and `$6.71` of per-packet overhead. Per-packet cost is roughly flat in size (1 hunk: 3.10 calls / 228s; 4–5 hunks: 3.00 calls / 228s). + +Packing does not reduce per-hunk cost uniformly. The profile floor can lift a member into a larger tool budget, and multi-member symbol context adds input tokens by design. Both are reported off/on rather than assumed neutral. + +## Current State + +- `hunkFirstGroups()`/`canJoinGroup()` join same-file hunks by identical enclosing symbol or `NEARBY_GAP_LINES = 30`. Baseline restored after Plan 102's teardown. +- `buildPacket()` derives `reviewProfile` after related-context filtering, so absorbing a strong edge can lower the derived profile — the reason Plan 102's floor exists. +- `readEnclosingSymbolSource()` reads one symbol per packet; `computeSymbolContextBudget()` selects `multiple_symbols_keep_compact` when `singlePrimarySymbol` is false. `DEFAULT_SYMBOL_CONTEXT_CHARS = 3_000`, `MAX_ADAPTIVE_SYMBOL_CONTEXT_CHARS = 6_000`, `MAX_CONTEXT_CHARS = 8_000`. Three members at the default already exceed the packet cap, and `renderPacketContextText()` truncates the combined text afterward — so any per-member quality computed before final rendering is fiction. +- `MAX_RELATIONSHIP_EDGES_PER_HUNK = 8` drops edges in insertion order; recorded `omittedEdges` is 112/91/89 on the retained runs. Relevant only if a future plan revives relationship signals. +- `evals/skill-semantics/` fixture files are 10–15 lines — one atom each — so those cases cannot exercise packing at all. +- `.gitignore` covers `evals/fixtures/logs/` but not `evals/skill-semantics/logs/`. +- `tsconfig.json` includes only `src/**` and `tests/**`, so `scripts/*.ts` is never typechecked by `pnpm run check`. +- `pnpm test -- ` does not filter; it runs the full suite and drops the argument. +- There is no seam to supply a pre-recorded Stage-5 plan to a review. + +## Goal + +1. Land Plan 102's mechanism unchanged in predicate, caps, and fill order. +2. Close the multi-member symbol-context gap so bigger packets are not quietly worse packets. +3. Prove the coverage gain deterministically as a **fixed-slot hunk yield**, not as a packet-count proxy. +4. Measure the packet-size recall curve with enough resolution to decide, and gate on a break-even bar derived from measurement 3. +5. End in one product path with no surviving flag. + +## Design + +### 1. Atoms, partitions, and fill — Plan 102, unchanged + +Wrap `hunkFirstGroups()` output as atoms carrying ordered hunks, hunk count, `combinedPatchChars()` size, first source position, effective coverage, routed lenses, standalone profile, and standalone per-member context quality. Partition by `(effectiveCoverage, normalizedPlannerLensSignature)`. Direct `whole-file` and content-probed `file-diff` returns bypass packing. + +### 2. Multi-member symbol context, fully specified + +Constants: + +- `PACKET_SYMBOL_CONTEXT_BUDGET = 5_000` — the share of `MAX_CONTEXT_CHARS` available to symbol source in a packed packet. +- `MIN_MEMBER_SYMBOL_CHARS = 800` — the floor below which a member is not meaningfully represented. +- `MIN_SLICED_MEMBER_CHARS = 600` — the minimum surviving emitted characters for a member to count as `sliced`. + +Rules: + +1. At least `MAX_CONTEXT_CHARS - PACKET_SYMBOL_CONTEXT_BUDGET` (3,000 characters) is reserved for outline, likely-tests, and planner-hint context. Symbol source never consumes it. +2. **Only members with a resolvable primary symbol participate in the allocation.** `memberSymbolCount` counts distinct resolvable primary symbol identities, never all members. +3. Each participating member receives `floor(PACKET_SYMBOL_CONTEXT_BUDGET / memberSymbolCount)`, subject to `MIN_MEMBER_SYMBOL_CHARS`. +4. If `memberSymbolCount * MIN_MEMBER_SYMBOL_CHARS > PACKET_SYMBOL_CONTEXT_BUDGET`, the candidate is abandoned — the packet cannot represent all its symbol-bearing members. +5. Members needing less than their share release the remainder; one redistribution pass, in source order, to members that requested more. +6. Existing adaptive/sliced selection applies per member within its share. Members render in source order. +7. **Per-member context quality is computed after `renderPacketContextText()` truncation, not before.** A participating member with fewer than `MIN_SLICED_MEMBER_CHARS` surviving is a candidate abandonment, never a reported `sliced`. + +**Symbol-less members.** Configuration, documentation, generic-adapter, and fallback hunks frequently have no resolvable primary symbol; `readEnclosingSymbolSource()` returns empty for them today and their standalone context quality is `outline_only` or `path_only`. Such members: + +- do not participate in the symbol allocation and consume none of it; +- are **exempt from the `MIN_SLICED_MEMBER_CHARS` rule** — applying a symbol-source floor to a member that never had symbol source would reject every packet containing one, including the config- and docs-heavy files where compatible atoms are most abundant; +- must retain their standalone outline or path context quality after packing. A symbol-less member whose quality degrades below what it held alone is a candidate abandonment. + +A packet whose members are *all* symbol-less allocates no symbol budget and is judged solely on the standalone-quality rule. + +### 3. Transactional candidate evaluation + +Every candidate is dry-built against a cloned relationship accumulator and a suppressed telemetry sink; nothing reaches the real graph, artifacts, or event stream until commit. Abandon when the combined packet would drop a routed lens, newly omit a planner focus note on a `high`/`critical` hunk, breach the context rules in section 2, or lower the effective profile below the maximum standalone member profile. Record each abandonment with a reason code and the atoms involved. + +### 4. Profile floor and dispatch rank + +Plan 102's floor verbatim: `max(derivedPackedProfile, max(standalone member profiles))` over `{ simple: 0, standard: 1, investigate: 2 }`. `dispatchRank` recomputed with the unchanged formula. + +### 5. Configuration — eval and internal only + +```ts +// CodegenieConfig["review"], resolved schema and defaults only +packRelatedHunks: boolean; // default false +packMaxHunks: number; // default 5, never exceeds MAX_HUNKS_PER_PACKET in shipped behavior +``` + +Neither appears in `rawConfigSchema`, neither is repo-safe filtered, and no `codegenie.toml` can set them. They exist in the resolved config, defaults, config-source telemetry, the strict eval-case `review` schema, and `applyCaseReviewConfig()`. Both are deleted in step 9. + +The pinned-plan seam is likewise eval-only. `ReviewPlan` carries no base/head identity, so the pinned artifact is an explicit versioned wrapper rather than a bare plan: + +```ts +type PinnedPlanArtifact = { + schemaVersion: 1; + baseSha: string; // resolved base commit the plan was drawn against + headSha: string; // resolved head commit + planSha256: string; // canonical hash of `plan`, defined below + plan: ReviewPlan; +}; +``` + +`planSha256` is the SHA-256 of the stable JSON serialization of `plan` alone — keys sorted recursively, no whitespace — so the hash is independent of field order and of the wrapper's own fields. The eval-case field `review.pinnedPlanPath` names this artifact, and loading it fails closed on any of: wrapper schema mismatch, `ReviewPlan` schema parse failure, `planSha256` not matching a recomputed canonical hash, `baseSha`/`headSha` not matching the resolved review target, or `hunkId` membership differing from the current diff. A hash match over a plan that no longer parses, or that targets a different diff, is not sufficient. + +The planner-draw mode that writes this artifact is a **dedicated internal script**, not a new top-level CLI command — `pnpm dev plan` does not exist today and adding a user-facing verb would contradict the eval-only scope. Tests cover each failure mode individually: bad wrapper schema, bad plan schema, wrong hash, wrong base, wrong head, wrong path, changed lenses, and changed hunk IDs. + +## Validation Strategy + +### A. Free deterministic replay + +Port `scripts/packet-packing-report.ts` from `7ebfd2f` down to `replay` plus `treatment`. Failure records are structured and templated — closed-set `code`, structured fields, message rendered from a template. No raw exception text, no repository source, no hashing. + +Assert per run: packet counts reproduce Plan 102's frozen result within 2; every hunk appears exactly once in source order; no atom split; no coverage promotion; caps hold; no lens or high-priority note lost without a recorded abandonment; no member below `MIN_SLICED_MEMBER_CHARS`; no profile or budget below the standalone maximum; dispatch ranks match the formula; flag-off is artifact-identical including `hunk-relationships.json`. + +**Fixed-slot hunk yield is the primary gate.** Sort packets by the full Stage-7 scheduling tuple `(priority, coverage, dispatchRank, input order)`, take the first 56 — the motivating run's observed dispatch capacity — and count reviewable hunks covered. + +**The estimator must first reconcile against the actual run.** Apply it to the *flag-off* packets and require it to reproduce the 89 hunks the run actually reviewed. If it does not, the estimator is not comparable to the historical baseline and must not define `B` until reconciled. This has been validated once by hand (89 = 89, 55/56 packets in common); the report re-proves it mechanically because it also feeds the paid gate. + +| Gate | Threshold | +| --- | --- | +| Flag-off estimator reconciliation | exactly `89` hunks at 56 slots | +| Fixed-slot hunk yield at 56 packets, flag on | `>= 102` hunks (≥15% over baseline 89) | +| Packet reduction | `>= 20%` per distinct diff | +| Deviation from Plan 102's frozen counts | `<= 2` packets per run | +| Participating members below `MIN_SLICED_MEMBER_CHARS` | `0` | +| Symbol-less members losing standalone quality | `0` | +| New coverage promotions | `0` | + +Also report, off versus on: per-member profile upgrades, total tool-call allowance, total context characters, and projected cost and service time per reviewed hunk. The measured yield ratio `baselineYield / packedYield` becomes the break-even bar for phase B. + +### B. The packet-size recall curve (paid) + +**Location.** `evals/packet-dilution/` in this repository, not the private evals repo. The fixture is synthetic Go with nothing proprietary, and `evals/skill-semantics/` is precedent for a real-model suite living here. This makes the curve a standing asset re-runnable whenever anyone proposes changing caps. Add `evals/packet-dilution/logs/` — and the missing `evals/skill-semantics/logs/` — to `.gitignore`. + +**Fixture.** One Go file, fifteen separated single-hunk changes, far enough apart that today's grouper yields fifteen atoms. It carries: + +- **three independent bugs** at atom positions 1, 10, and 13. Under cap 5 the packets are `[1-5][6-10][11-15]`, placing the bugs in three *different* packets at positions 1, 5, and 3 — first, last, and middle. Under cap 3 the packets are `[1-3]…[10-12][13-15]`, again three different packets. **No two bugs ever share a packet at any tested cap**, so no arm reviews an unnaturally bug-dense packet and no two opportunities collapse into one conversation; +- **one safe-but-suspicious change** as a `should_not_find` control, so a rise in false positives is visible; +- eleven ordinary safe changes. + +Fixture-quality rules, each learned from a Plan 102 failure: + +1. **Transplant bugs already proven detectable.** Take them from `evals/skill-semantics/`, where the existing suite already demonstrates the harness finds them. Do not invent new bugs whose detectability is unknown. +2. **Validate expectations with the fake provider first**, for `$0`. `fake-runner.ts` emits a finding from a trigger line; confirm each `should_find` matcher fires on the expected finding shape before any real call. +3. **Each bug must be one the planner grades ordinarily.** If a bug is obvious enough to draw a `deep` grade on its own, it lands in a different partition and never packs. +4. **Constrain path, line range, and failure mode — never category.** Plan 102 lost three executions that found its bug because the expectation demanded `correctness` and the reviewer said `security`. +5. **Prove treatment model-free before paying.** Build Stage 6 against the frozen plan at caps 1, 3, and 5 and require exactly 15, 5, and 3 packets, with each bug's atom in a packet of exactly 1, 3, and 5 source atoms. Any miss is a fixture defect: fix the fixture, never the sample. + +**The frozen plan is hand-authored, not drawn.** Authoring it makes coverage and lens assignment deterministic, so all fifteen atoms are guaranteed into one partition; removes planner variance so packet size is genuinely the only variable; eliminates any temptation to redraw unfavourable plans; and costs nothing. + +**But an authored plan must be shown to be realistic, not merely convenient — and realism is measured in hunks, not atoms.** Plan 102's replay gives the production shape after packing at cap 5: + +| Run | Packed packets by hunk count (1/2/3/4/5) | Packed packets by atom count (1/2/3/4) | +| --- | --- | --- | +| `dca8d870` | 46/12/4/5/**8** | 61/8/5/1 | +| `81f806a6` | 38/12/6/4/**8** | 48/10/7/1 | +| `740d73f2` | 54/13/6/5/**7** | 66/9/7/1 | + +Five-**hunk** packets are ordinary production output — 7 to 8 per run, roughly a tenth of all packed packets. Five-**atom** packets never occur; the maximum observed is four, once per run. Since the fixture uses fifteen single-hunk atoms, its cap-5 arm produces a five-hunk packet, which is the production-real shape. The cap-5 arm is therefore a genuine production case, not a stress bound — but only because atoms there are one hunk each, and the plan claims nothing about five-*atom* packets, which production does not build. + +Draw **three** reference plans from the real planner against the same fixture and require that in **at least 2 of 3**, every target hunk lands in a packet of at least five hunks at cap 5 and at least three at cap 3 — the shapes the arms actually test. Fewer than 2 of 3 means the fixture exercises a shape the planner would rarely produce: **the fixture is invalid and must be redesigned before any reviewer call**, not merely documented. Draws run with the local cache disabled so they are independent. A machine-enforced report parses all three, rebuilds their Stage-6 packet shapes, emits each target's actual packet hunk count and compatibility signature, and exits non-zero below the threshold. Record all three draws, their diffs against the authored plan, the emitted shapes, and the pass count. + +**Arms.** Three cases differing only in `packMaxHunks` — 1, 3, 5 — at `repeat: 6`, cache off, all pinned to the same authored plan. Three bugs × six repeats = **18 recall opportunities per arm**. + +Those opportunities are **clustered, not independent**: all repeats share one fixture and one authored plan, and each repeat's three bugs are reviewed by the same run. Treat the data as three per-bug series of six, report `R` per bug alongside the aggregate, and never describe it as 18 independent trials. + +**Decision rule.** Let `N` be opportunities per arm (18 initially, 36 after an extension), `K` be repeats per arm (6 initially, 12 after), `R1`, `R3`, `R5` aggregate hits out of `N`, `R{n}[i]` per-bug hits out of `K`, and `B` the break-even ratio measured in phase A (`baselineYield / packedYield`; ≈ 0.824 at 89/108). + +**All thresholds are proportions, so they scale correctly when repeats are extended.** Applying the initial absolute counts to a 36-opportunity extension would silently halve the baseline bar and redefine collapse. + +| Condition | Outcome | +| --- | --- | +| `R1 / N < 5/6` (15 of 18; 30 of 36) | **Void.** Baseline too unreliable to measure a ratio against. | +| any bug with `R1[i] / K >= 5/6` and `R5[i] / K <= 1/6` (5-of-6 and 1-of-6; 10-of-12 and 2-of-12) | **Fail (per-bug collapse).** One target falling from reliable detection to near-zero is a real regression regardless of the aggregate. | +| `R5 / R1 >= B`, no per-bug collapse, and no monotone decline `R1 > R3 > R5` | **Pass.** | +| `R5 / R1 >= B` but `R1 > R3 > R5` | **Extend once** (see below). | +| `R5 / R1 < B` | **Fail.** Packing costs more recall than the coverage it buys. | + +`R3` therefore has a real role — it distinguishes a cliff from a gradient — and there is no unreachable branch. + +**Void and extension are bounded and preregistered, not discretionary:** + +- **Void permits exactly one fixture redesign and one rerun.** A second Void is a **Fail** and takes the teardown branch. Void never means "keep trying until the numbers work." +- **Monotone decline permits exactly one extension:** six additional repeats on **all three arms**, budgeted below at ~$14. Re-apply the same decision rule to the combined 12-repeat totals (36 opportunities per arm). Its outcome is final — a second extension is not available, and a still-monotone result after extension is a **Fail**. +- Every Void or extension is recorded in the reconciliation note with its trigger, cost, and result. + +**Honest limits.** Eighteen clustered opportunities per arm can detect a large regression; they cannot resolve, say, 100% versus 85% with confidence. The gate is a screen against material harm, not a certificate of equivalence, and the reconciliation note must say so. Report per-arm cost, tokens, and model-service time per reviewed hunk alongside recall. + +### C. Collateral + +1. `evals/fixtures/` flag-off and flag-on at `repeat: 1`, fake provider. Every expectation, packet invariant, and config parse must match. +2. **Treated cross-language packet-shape cases** under `evals/packet-dilution/`: small TypeScript, Python, and Solidity fixtures each with at least three compatible same-file atoms, run under the fake provider, asserting packed packet shape and per-member context presence. Existing `evals/skill-semantics/` files are 10–15 lines and yield one atom each, so they cannot exercise packing; they remain a flag-off/on regression check only. +3. One production-shaped capacity pair on the pinned `trails-api` diff at concurrency 6, flag-off and flag-on. **Required before shipping**, not optional. + + Both sides consume **the same pinned Stage-5 plan** and identical provider, model, reasoning, budget, concurrency, time limit, and cache settings; independently drawn plans would confound the pair exactly as they confounded Plan 102. Its gate is numeric, not narrative: + + - the flag-on run must review **at least 10% more reviewable hunks** than flag-off within the same wall-clock and token budget; + - the flag-on reviewed-hunk set must be a **superset of the flag-off set minus at most 2 hunks**, so the gain is not a reshuffle; + - measured flag-on yield must fall within **±10% of the phase-A fixed-slot prediction**; a larger miss means the estimator does not transfer to a live run and the prediction, not the run, is what failed. + + Any of the three failing is a stop-for-investigation before shipping. + +### Cost discipline + +**`approvedValidationCostUSD: $300`** — recorded 2026-07-25 on owner approval. The owner approved the plan's `$119` reservation and raised the ceiling to `$300`, and separately authorized running the production capacity pair inside Phase 4 rather than deferring it. Spend to date: `$0.00`. + +No paid call before `approvedValidationCostUSD` is recorded here. + +| Phase | Executions | Projection | +| --- | ---: | ---: | +| Fake-provider expectation validation | — | $0 | +| Three reference planner draws | 3 | ~$3 | +| Recall curve, 6 repeats × 3 arms | 18 | ~$14 | +| Reserved: one bounded extension, 6 more repeats × 3 arms | 18 | ~$14 | +| Reserved: one fixture redesign rerun after a single Void | 18 | ~$14 | +| `evals/fixtures/` and cross-language shape cases | — | $0 | +| Production capacity pair | 2 | ~$50 | +| Contingency 25% | — | ~$24 | +| **Reservation** | | **~$119** | + +The two reserved contingencies are the *only* reruns this plan authorizes; a second Void or a second extension is a Fail, not another draw on the budget. Phases through the first curve cost ~$17 and decide whether packing ships; the capacity pair runs last and only after a pass. + +## In-Scope Files + +- `src/pipeline/packet-builder.ts` — atoms, partitions, fill, multi-member symbol context, transactional candidates, profile floor, telemetry. +- `src/pipeline/review-runner.ts` — eval-only pinned-plan seam. +- `src/types.ts` — per-member context-quality telemetry, both temporary settings. +- `src/config/schema.ts`, `src/config/config-loader.ts`, `src/evals/eval-runner.ts` — resolved-config and eval plumbing only. +- `scripts/packet-packing-report.ts`, `tests/packet-packing-report.test.ts` — `replay`, `treatment`, and `realism`. +- `scripts/draw-pinned-plan.ts` — internal planner-draw writing the versioned `PinnedPlanArtifact`. No user-facing CLI verb is added; `src/cli/` stays out of scope. +- `tests/pipeline-phase5.test.ts`, `tests/evals.test.ts`, `tests/config-loader.test.ts`. +- `evals/packet-dilution/` — Go recall fixture, cross-language shape cases, authored plan. +- `.gitignore` — `evals/packet-dilution/logs/` and the missing `evals/skill-semantics/logs/`. +- `specs/plans/README.md` and affected `specs/project/` docs. + +## Non-Goals + +- **Relationship-based packing, in any form.** Measured at 4.2% as a predicate and as a no-op as an ordering preference. No affinity view, no `ownerKey`, no `same_container`. +- **Raising `MAX_HUNKS_PER_PACKET` or `MAX_PATCH_CHARS`.** The sweep shows 5 captures ~80% of achievable compression. `packMaxHunks` exists only to measure the curve and never ships above 5. +- **Coverage promotion.** 2 of 13 candidate pairs, zero of them structural. +- **Reordering the stage flow.** The planner grades related siblings consistently in 132 of 134 measured pairs. +- Cross-file packing; the packet schema is file-scoped. +- Tool-budget changes of any kind. +- Changing prompt templates, attention-note ordering, `canJoinGroup()`, Plan 100's dispatch policy, or Plan 92's escalation rules. +- Any user-facing configuration surface, or leaving either setting dark after step 9. + +## Implementation Steps + +1. Reconcile drift and working-tree checks. Add both settings to the resolved config, defaults, source telemetry, the strict eval-case schema, and `applyCaseReviewConfig()` — **not** to `rawConfigSchema` or repo-safe filtering. Add both `.gitignore` log entries. + + **Verify:** `pnpm exec vitest run tests/config-loader.test.ts tests/evals.test.ts` → exit 0; defaults `false`/`5`; eval overrides apply; `packMaxHunks > 5` rejected; a `codegenie.toml` setting either key fails strict parsing. +2. Wrap `hunkFirstGroups()` output as atoms, capturing standalone coverage, lenses, profile, and per-member context quality; add flag-off golden parity. + + **Verify:** `pnpm exec vitest run tests/pipeline-phase5.test.ts -t "packet atom|flag-off parity|whole-file bypass"` → exit 0. +3. Implement partitions and source-order greedy fill; reproduce Plan 102's frozen counts. + + **Verify:** `pnpm exec vitest run tests/pipeline-phase5.test.ts -t "compatible partition"` → exit 0; hunk bijection, within-packet source ordering, packet ordering by earliest member, cap splits, interleaved partitions, degradation merge, exact dispatch rank. +4. Implement multi-member symbol context per section 2. + + **Verify:** `pnpm exec vitest run tests/pipeline-phase5.test.ts -t "member symbol context"` → exit 0; the 3,000-character non-symbol reserve is never consumed; equal shares with redistribution; a candidate needing more than the budget floor for all members is abandoned; quality is computed after final truncation and a member surviving under 600 characters aborts rather than reporting `sliced`. +5. Implement transactional candidate evaluation and the profile floor. + + **Verify:** `pnpm exec vitest run tests/pipeline-phase5.test.ts -t "candidate rejection|profile floor"` → exit 0; abandoned candidates leave no telemetry, graph, or artifact residue. +6. Add the pinned-plan seam and planner-draw mode. + + **Verify:** `pnpm exec vitest run tests/evals.test.ts -t "pinned plan"` → exit 0; a pinned plan reproduces identical Stage-6 inputs across runs; hash and `hunkId` mismatches fail closed; no user-facing path reaches it. +7. Port the report script and run the four-run replay. + + **Verify:** `pnpm exec vitest run tests/packet-packing-report.test.ts` → exit 0; the replay exits 0 with `modelCallsObserved: 0` and every section-A gate satisfied, including fixed-slot hunk yield `>= 102`. Record the measured break-even ratio `B`. +8. Build the fixture, validate expectations under the fake provider, author and hash the plan, prove treatment model-free, then record the ceiling and run: reference planner draw, the 6×3 curve, `evals/fixtures/`, cross-language shape cases, and the capacity pair. Apply the decision table verbatim. + + **Verify:** the realism report exits 0 with at least 2 of 3 draws placing every target in a five-hunk packet at cap 5; treatment proof shows 15/5/3 packets with each bug at 1/3/5 hunks and no two bugs sharing a packet; all three arms consumed the same verified plan artifact; the decision table yields Pass, Fail, Void, or Extend-once with no discretion; the capacity pair meets all three numeric gates; spend stays within the ceiling. +9. Record the decision and tear down in a dedicated commit. + + **Preserve evidence first:** copy into `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-dilution/reports/` every produced JSON report, the authored frozen plan, all three reference draws, the realism report and the reference diffs it emits, the treatment proof, the capacity-pair reports, and a `not_run` ledger naming every unreached phase with its stopping reason — then regenerate `manifest.sha256` over the complete set. Do not modify Plan 102's manifest, which its reconciliation note describes as covering exactly three files. + + **If Pass:** make packing unconditional at cap 5; delete both settings and the unpacked path. Keep atoms, partitions, the profile floor, multi-member symbol context, and transactional rejection. Reduce the report script to a golden check. Keep `evals/packet-dilution/` as a standing suite with the shipped cap only, and retain the curve in the reconciliation note. + + **If Fail — including a second Void, a post-extension monotone decline, or any per-bug collapse:** delete the packing pass, both settings, the atom wrapper, and the report script with its tests. Multi-member symbol context survives only if the replay showed it improving context quality with packing off. Keep the fixture and the measured curve — the curve is the durable asset regardless of outcome. Do not leave the feature dark. + + A *first* Void or a *first* monotone decline does not reach this step: each authorizes exactly one bounded rerun under phase B's preregistered limits, and only its result reaches teardown. + + **Verify:** `rg -n "packRelatedHunks|packMaxHunks" src scripts tests evals` → exit 1; both manifests verify; the note records the decision, actual spend, and the resolution limit of 18 observations per arm. +10. Run the complete repository gate. + + **Verify:** `pnpm run check && pnpm test && pnpm build` → exit 0. + +## Tests and Commands + +```bash +pnpm run check +pnpm exec vitest run tests/pipeline-phase5.test.ts tests/config-loader.test.ts tests/evals.test.ts tests/packet-packing-report.test.ts +``` + +Artifact diagnostics (reproduces this plan's Evidence section, no model calls): + +```bash +RUNS=/home/peter/Dev/0xsequence/trails-api/.codegenie/runs +D="$RUNS/20260724-184952-dca8d870 $RUNS/20260724-162739-81f806a6 $RUNS/20260724-135818-740d73f2" +node scripts/packing-diagnostics.mjs pairs $D +node scripts/packing-diagnostics.mjs sweep $D +for p in source compatibility related; do + node scripts/packing-diagnostics.mjs simulate $D --predicate $p +done +``` + +Deterministic four-run replay: + +```bash +pnpm exec tsx scripts/packet-packing-report.ts replay \ + --repo /home/peter/Dev/0xsequence/trails-api \ + --run $RUNS/20260724-135818-740d73f2 \ + --run $RUNS/20260724-150405-fe1548ae \ + --run $RUNS/20260724-162739-81f806a6 \ + --run $RUNS/20260724-184952-dca8d870 \ + --dispatch-slots 56 \ + --distinct-diffs \ + --output /tmp/plan103-packing-shape.json +``` + +Fixture validation, planner draw, and the paid curve: + +```bash +# 1. expectation wiring, fake provider, $0 +pnpm dev eval --eval-dir evals/packet-dilution/shape --no-cache + +# 2. three independent reference draws (cache off), then a machine-enforced +# realism report: >=2 of 3 must place every target in a five-hunk packet at +# cap 5 and a three-hunk packet at cap 3, else the fixture is invalid +for i in 1 2 3; do + pnpm exec tsx scripts/draw-pinned-plan.ts \ + --repo evals/packet-dilution/repos/dilution \ + --base main --branch feature --no-cache \ + --output evals/packet-dilution/plans/reference-draw-$i.json +done + +pnpm exec tsx scripts/packet-packing-report.ts realism \ + --authored evals/packet-dilution/plans/frozen.json \ + --draw evals/packet-dilution/plans/reference-draw-1.json \ + --draw evals/packet-dilution/plans/reference-draw-2.json \ + --draw evals/packet-dilution/plans/reference-draw-3.json \ + --require-target-hunks 5 --at-cap 5 --min-passing-draws 2 \ + --emit-diffs evals/packet-dilution/plans/reference-diff-{1,2,3}.json \ + --output /tmp/plan103-realism.json + +# 3. model-free treatment proof at each cap +pnpm exec tsx scripts/packet-packing-report.ts treatment \ + --pinned-plan evals/packet-dilution/plans/frozen.json \ + --expect-packets 15,5,3 \ + --expect-target-atoms 1,3,5 \ + --output /tmp/plan103-treatment-proof.json + +# 4. the paid curve +pnpm dev eval --eval-dir evals/packet-dilution/recall --no-cache + +pnpm exec tsx scripts/packet-packing-report.ts treatment \ + --logs evals/packet-dilution/recall/logs \ + --cohort \ + --expected-repeats 6 \ + --require-pinned-plan \ + --break-even \ + --output /tmp/plan103-recall-curve.json +``` + +Collateral and capacity: + +```bash +pnpm dev eval --eval-dir /tmp/plan103-collateral/fixtures-off --no-cache +pnpm dev eval --eval-dir /tmp/plan103-collateral/fixtures-on --no-cache +pnpm dev eval --eval-dir /home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-dilution/capacity --no-cache +``` + +Evidence preservation and verification: + +```bash +D=/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-dilution/reports +cp /tmp/plan103-*.json "$D"/ +cp evals/packet-dilution/plans/frozen.json "$D"/ +cp evals/packet-dilution/plans/reference-draw-*.json "$D"/ +cp evals/packet-dilution/plans/reference-diff-*.json "$D"/ # emitted by the realism report +cp /tmp/plan103-realism.json "$D"/ +cp /tmp/plan103-capacity-*.json "$D"/ 2>/dev/null || true # absent if not reached +# not_run ledger: every unreached phase and why +"$EDITOR" "$D/not-run-ledger.md" +(cd "$D" && sha256sum plan103-*.json frozen.json reference-draw-*.json reference-diff-*.json not-run-ledger.md > manifest.sha256 && sha256sum -c manifest.sha256) +(cd "$D/../../packet-packing/reports" && sha256sum -c manifest.sha256) # Plan 102 evidence unchanged +``` + +Note the `pnpm dev eval` spelling: under pnpm 11 a literal `--` reaches Commander and is rejected before suite allocation. + +## Acceptance Criteria + +- Flag off produces byte-identical packet artifacts, IDs, order, profiles, context, budgets, and `hunk-relationships.json`. +- Flag on reproduces Plan 102's packet counts within 2 per run, every hunk appearing exactly once, hunks ordered within packets by file position and packets by earliest member, under unchanged caps with zero coverage promotion. +- Every member atom with a resolvable primary symbol has its source present after final rendering at or above `MIN_SLICED_MEMBER_CHARS`, or its candidate was abandoned and recorded. Symbol-less members consume no symbol budget, are exempt from that floor, and retain their standalone outline or path quality. +- The 3,000-character non-symbol context reserve is never consumed by symbol source. +- No lens dropped and no `high`/`critical` focus note newly omitted without a recorded abandonment; every effective profile at least the standalone maximum; dispatch ranks unchanged in formula. +- The fixed-slot estimator reproduces exactly 89 hunks on the flag-off packets at 56 slots before it is used for anything; flag-on yield at 56 slots is at least 102; the measured break-even ratio is recorded and used as phase B's bar. +- Per-member profile upgrades, tool-call allowance, context characters, and cost and service time per reviewed hunk are reported off versus on. +- All three curve arms consume one authored plan verified by hash, schema, hunk-ID membership, and diff identity; at least 2 of 3 reference draws confirm the tested partition is one production would plausibly create; treatment is proven model-free at 15/5/3 packets with no two bugs sharing a packet at any cap; the decision table, including per-bug collapse and the bounded Void and extension allowances, is applied without discretion. +- Cross-language shape cases prove packed context on TypeScript, Python, and Solidity; `evals/fixtures/` shows no regression; the capacity pair confirms the predicted yield. +- Paid validation never begins without a recorded ceiling; every phase records actual and projected spend. +- Report failures carry templated structured messages with no raw exception text, repository source, or hashing. +- Teardown leaves one product path with no surviving flag; both evidence manifests verify; Plan 102's manifest is unmodified. +- Checks, tests, and build pass; the decision and the 18-observation resolution limit are documented. + +## Stop Conditions + +- The flag-off estimator does not reproduce 89 hunks at 56 slots: the proxy is not comparable to the historical baseline and must not define `B` until reconciled. +- Fixed-slot hunk yield below 102, packet reduction below 20%, or deviation from Plan 102's frozen counts above 2: the port is wrong. Fix before spending anything. +- Any atom split or reordered, hunk lost or duplicated, cap exceeded, coverage promoted, profile or budget downgraded, or member context below the minimum without abandonment: fix the deterministic design first. +- Flag-off artifacts change in any respect: stop; parity is the basis of every later comparison. +- The model-free treatment proof does not yield 15/5/3 packets with the target atoms at 1/3/5: fix the fixture and re-prove. Never adjust the sample to fit. +- Any arm consumed a different plan hash: discard and rerun that repeat. +- Fewer than 2 of 3 reference draws place every target in a five-hunk packet at cap 5 and a three-hunk packet at cap 3: the fixture tests a shape production would rarely create. Redesign before any reviewer call. +- `R1 / N < 5/6`: the baseline is too unreliable to measure against. Void; fix the fixture rather than lowering the bar. A second Void is a Fail. +- Any bug with `R1[i] / K >= 5/6` and `R5[i] / K <= 1/6`: per-bug collapse. Fail regardless of the aggregate ratio. +- The capacity pair misses any of its three numeric gates: stop for investigation before shipping. +- Any proposal to apply the initial absolute thresholds to an extended cohort rather than the proportional ones: reject; it halves the baseline bar. +- A monotone decline surviving the single authorized six-repeat extension: Fail. No second extension exists. +- `R5 / R1 < B`: packing costs more recall than the coverage it buys. Fail and take the teardown branch — do not renegotiate `B` after seeing the result. +- A monotone decline `R1 > R3 > R5` that survives extended repeats: treat as a dilution signature and fail, even if the ratio clears. +- Any proposal to raise `MAX_HUNKS_PER_PACKET`, revive relationship signals, or add coverage promotion to recover margin: reject here. Each has a measured Non-Goal entry. +- Actual plus projected spend exceeds the approved ceiling: stop for explicit approval. +- "Keep packing dark" always means *do not ship it in this iteration*; every such outcome terminates in step 9's failure branch in the same change. + +## Maintenance Notes + +- Any change to `hunkFirstGroups()`, `canJoinGroup()`, `readEnclosingSymbolSource()`, `computeSymbolContextBudget()`, `packetReviewProfile()`, packet caps, `packetDispatchRank()`, or `toolBudget()` invalidates the recorded shape, context, and yield baselines; rerun the replay first. +- The packet-size recall curve is the durable asset from this plan, independent of whether packing ships. Any future proposal to raise caps, pack cross-file, or pack cross-coverage should extend the curve rather than assume it. +- `scripts/packing-diagnostics.mjs` reproduces the relationship and cap-sweep analyses that shaped this plan. Its approximations are documented in its header; the authoritative measurement is the report script's replay. +- Plan 102's preserved reports and paid logs under `codegenie-private-evals/trails-api/packet-packing/` record its failed fixture design and must not be edited. This plan's evidence lives under `packet-dilution/reports/` with its own manifest. +- Reviewers should scrutinize per-member context after final rendering, abandonment reasons, fixed-slot yield, and treatment proof — not packet count, which is Plan 102's already-validated result. +- Commit hashes here are rebase-unstable; locate referenced work by commit subject when a hash does not resolve. + +## Production A/B Post-Mortem — Runs 67 and 68 (2026-07-26) + +### Decision + +**Skip Plan 103 for now. Do not promote `packRelatedHunks` as a product feature on the evidence from this pair.** Packing worked mechanically and showed a real constrained-capacity benefit, but it did not achieve the combined product goal: materially lower end-to-end cost or elapsed time while maintaining equivalent finding and report quality. + +This is a negative product decision, not a claim that the implementation did nothing. The experiment established useful properties of the treatment, recorded below, but the observed whole-review outcome is not strong enough to justify retaining or shipping the current policy. No further paid validation or promotion is authorized by this report; any retry should be proposed as a new, bounded experiment with fresh gates. + +### Compared runs and control quality + +Evidence is under `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/0c4d5213/logs/`: + +- **Run 67:** `packRelatedHunks: true` +- **Run 68:** `packRelatedHunks: false` + +Both runs reviewed the same base/head diff, used the same model and budgets, and consumed the same pinned Stage-5 plan. The canonical hashes of their `review-plan.json` artifacts match. Both reviewed all 131 hunks with identical coverage totals: 13 deep, 112 normal, 6 light, and 0 skipped. This makes the pair substantially better controlled than earlier planner-variable comparisons, although Stage 7 and verification remain nondeterministic model executions. + +### End-to-end results + +| Metric | Run 67: packed | Run 68: unpacked | Packed delta | +| --- | ---: | ---: | ---: | +| Generated/reviewed packets | 62 | 73 | -11 (-15.1%) | +| Hunks reviewed | 131 | 131 | no change | +| Hunks reached in the first 56 packet slots | 121 | 106 | +15 (+14.2%) | +| Elapsed time | 2,710.767 s (45m 10.8s) | 2,885.696 s (48m 05.7s) | -174.929 s (-6.1%) | +| Model calls | 211 | 239 | -28 (-11.7%) | +| Tokens | 4,861,061 | 5,074,177 | -213,116 (-4.2%) | +| Cost | $21.7162 | $22.0228 | -$0.3066 (-1.4%) | +| Candidate findings | 12 | 15 | -3 | +| Published findings | 3 | 4 | -1 | + +The capacity result is real: if both reviews had stopped after 56 packet conversations, the packed run would have covered 121 rather than 106 hunks (92.4% rather than 80.9% of the diff). That supports packing as a possible capacity mechanism for reviews that would otherwise time out. + +It does **not** establish the desired full-review product win. Both arms completed, and the user-visible improvement was approximately three minutes and thirty-one cents on a roughly 48-minute, $22 review. Packet reduction was 15.1%, below this plan's 20% gate; the fixed-slot improvement was 14.2%, slightly below the stated 15% target. The end-to-end cost and time deltas are too small and too exposed to normal model variance to justify the quality risk. + +### What packing actually treated + +Run 67 accepted nine merges, combining 20 source atoms containing 29 hunks into nine packets. This removed 11 conversations. There were no abandoned candidates, coverage changes, lens losses, hunk losses, duplicate hunks, or cap breaches. All treated packets were normal coverage and remained under the 5-hunk / 12,000-character caps. + +The treatment-local economics were materially stronger than the whole-run totals: + +| Treated work only | Packed | Corresponding unpacked packets | Reduction | +| --- | ---: | ---: | ---: | +| Stage-7 calls | 29 | 52 | 44.2% | +| Tokens | 699,190 | 965,424 | 27.6% | +| Cost | $3.2833 | $4.4453 | 26.1% | +| Cumulative model-call time | 1,424.6 s | 1,924.6 s | 26.0% | +| Agent repository-tool calls | 35 | 60 | 41.7% | +| Initial prompt characters | 579,165 | 859,791 | 32.6% | +| Packet context characters | 35,311 | 43,613 | 19.0% | + +However, a packed treatment conversation was not approximately equal in cost to a standalone one. Per conversation it used about 61% more tokens, cost about 64% more, and consumed about 64% more cumulative call time. Packing saved resources only because one larger conversation replaced an average of 2.22 smaller conversations. Across the complete review, unchanged work and model/verification variance absorbed most of that local saving. + +The run also did not demonstrate relationship-driven selection. Only two of the nine merges joined source atoms connected by strong edges in the retained relationship graph; those accounted for three of the eleven removed packets. The other seven merges, accounting for eight removed packets, came from same-file compatibility and source-order filling. Manual inspection found that many were reasonably related refactor changes (for example, an added helper import beside the implementation using it), but that relationship was not what authorized the merge. The current treatment therefore remains opportunistic compatible-atom batching rather than a proven tree-sitter/relationship-driven policy. + +### Finding and report quality + +Run 67 formally failed its eval because `minFindings` required four and it published three; run 68 published four and passed. Both runs nevertheless passed every required semantic expectation: + +- explicit-preference routing fallback behavior was found by both; +- the zero-decimal `AmountFromUSD` behavior change was found by both; +- the ERC20 balance test-coverage candidate was generated by both; and +- the LiFi false-positive control passed in both. + +Run 67 additionally published the optional relay-decimals narrowing issue. Run 68 generated the same candidate but rejected it during verification. Conversely, run 68 published two findings absent from run 67: + +- the ZeroEx transaction-value parsing finding came from an unchanged packet and therefore cannot be attributed to packing; and +- the `directEdgeOriginTokenAmount` parsing finding came from an atom included in a three-atom packed packet and is the one plausible treatment-side miss. + +The latter describes a real parser-domain change, but its defect value is weak and conditional: the removed dependency helper permissively extracted decimal digits from malformed strings, while the new path rejects them and returns `nil`, which may be safer for malformed provider data. Even so, the burden is on packing to demonstrate non-inferiority. One pair in which the final findings and report differ does not meet that burden. Required-target parity is encouraging, but it is not equivalent to maintaining the same overall quality. + +The pair also demonstrates why exact single-run finding equality cannot by itself isolate packing: an unchanged packet produced the ZeroEx finding in only one arm, and the same relay candidate received opposite verification outcomes. Repeated unpacked controls would be required to quantify this background variance. That uncertainty is a reason not to ship, not a reason to waive the quality requirement. + +### Telemetry observations + +- Run 67 recorded 91 `build_packet_context` operations while producing 62 final packets. The count reconciles as 73 standalone builds plus nine dry candidate builds plus nine committed packed builds. The dry builds were intended to use a suppressed telemetry sink, so they remain visible when they should not be. +- This local Stage-6 accounting makes total tool calls misleading: the packed run reports 584 total tool calls versus 555 unpacked, while actual Stage-7 plus Stage-9 agent tool use fell from 275 to 244. +- `packet_member_symbol_context` events did not carry a packet ID, complicating per-packet auditing. +- The core relationship graph remained stable across arms at 131 nodes and 298 edges. Related-context attachment records changed because relationships internal to a packed packet no longer needed separate excerpts. +- Run 67 had one invalid Stage-7 schema response that was repaired successfully; it did not lose a packet or required finding. + +These issues do not explain the product decision, but they should be fixed before any future packing experiment relies on telemetry totals as gates. + +### Product conclusion + +Plan 103 demonstrated that compatible-atom packing can reduce conversation count and can review more hunks before a fixed packet cutoff. It did not demonstrate a material end-to-end cost or latency improvement after the whole pipeline completed, and it did not establish equivalent finding/report quality. The observed 1.4% cost reduction and 6.1% elapsed-time reduction are insufficient compensation for an unresolved quality difference. + +The product goal was not "some packets were combined." It was lower cost and/or meaningfully lower completion time **while maintaining review quality**. Runs 67 and 68 do not satisfy that conjunction. Plan 103 is therefore skipped for now and must not be presented as a successful production optimization. + +If revisited, it should be a new plan rather than an extension that renegotiates these results. At minimum, a successor must pre-register meaningful end-to-end cost/time thresholds, adjudicated material-finding non-inferiority, repeated pinned-plan pairs plus unpacked controls, explicit per-pack relationship provenance, per-member assessments, and automatic teardown on failure. diff --git a/specs/plans/README.md b/specs/plans/README.md index cd876cc..8d35400 100644 --- a/specs/plans/README.md +++ b/specs/plans/README.md @@ -105,4 +105,5 @@ This directory tracks implementation plans for confirmed improvements. Status va | 99 | COMPLETE | Issue 99: Plan 98 Correctness Follow-Up — Unicode-Safe Tree-sitter Extraction and Likely-Test Compatibility | | 100 | COMPLETE | Issue 100: Short Hunk IDs — Planner Coverage Survival and Dispatch Resilience | | 101 | IN PROGRESS (paid semantic A/B pending) | Issue 101: Exact Skill Provenance and Evidence-Gated Bundled-Skill Revision | -| 102 | PENDING | Issue 102: Same-File Packet Packing | +| 102 | COMPLETE (failed gate; baseline restored) | Issue 102: Same-File Packet Packing | +| 103 | IMPLEMENTED (dark; production A/B pending) | Issue 103: Compatible-Atom Packet Packing and the Packet-Size Recall Curve | diff --git a/specs/plans/phase-plans/102/phase_1.md b/specs/plans/phase-plans/102/phase_1.md new file mode 100644 index 0000000..a941963 --- /dev/null +++ b/specs/plans/phase-plans/102/phase_1.md @@ -0,0 +1,28 @@ +--- +status: complete +--- + +# Phase 1: Dark Same-File Packet Packing + +## Overview + +Implement Plan 102 steps 1–4 only. This phase adds temporary experiment configuration, preserves the current hunk-first groups as explicit indivisible atoms, packs only compatible same-file atoms behind the dark flag, preserves each atom's exact standalone review profile and routed-lens/focus safeguards, emits Stage-6 treatment provenance, and keeps base versus atom-scaled tool budgets experimentally separable. The report script, retained-run replay, paid evals, rollout decision, and teardown remain out of scope. + +## Steps + +1. Extend `src/types.ts`, `src/config/schema.ts`, `src/config/config-loader.ts`, and `src/evals/eval-runner.ts` with temporary `review.packSameFileHunks: boolean` and `review.packedToolBudgetMode: "base" | "atom-scaled"` fields. Default them to `false` and `base`, apply user/repository/eval overrides through strict schemas, mark both repository-safe, record config-source winners, and expose the effective values in eval run metadata. +2. Refactor only `hunkFirstGroups()` results in `src/pipeline/packet-builder.ts` into internal hunk-first groups and explicit packet atoms. Give atoms stable ordered-hunk IDs, exact rendered patch size, source position, effective coverage, normalized sorted/deduplicated planner-lens signature, and the exact standalone packet profile derived by the existing Stage-6 build path. Preserve the direct whole-file and content-probed file-diff returns, and bypass all atom work when packing is disabled so packet artifacts, IDs, order, profiles, context, lenses, and budgets remain unchanged. +3. Add a stable compatible-atom pass for hunk-first atoms only. Partition by effective coverage and normalized planner-lens signature, greedily pack in partition source order under five-hunk/12,000-character caps, restore packet order by first source position, preserve atom and hunk ordering, merge degradation reasons deterministically, and rebuild packet kind/context/routing/dispatch rank through existing helpers. Dry-build candidates against isolated relationship telemetry so a newly lost standalone routed lens or high/critical focus note leaves atoms separate. Apply an explicit `simple < standard < investigate` profile floor from exact standalone profiles and emit `same_file_atoms_packed` Stage-6 provenance without changing prompts or packet IDs. +4. Add the isolated budget policy in `src/pipeline/packet-builder.ts`. `base` uses the existing budget from the effective profile. `atom-scaled` applies only to non-simple packets that combine multiple pre-existing atoms, adds at most one call and 2,000 result characters per additional atom subject to 1.75x ceilings, leaves investigation rounds and source-extension policy unchanged, and applies `budgetBoost` last. +5. Add focused coverage in `tests/config-loader.test.ts`, `tests/evals.test.ts`, and `tests/pipeline-phase5.test.ts` for strict/default/source config behavior, eval propagation, flag-off parity, atom identity/profile capture, whole-file bypass, compatible packing, atom/cap/source-order/coverage/lens/focus/degradation/dispatch invariants, relationship-driven profile floors (including `symbol_mention`, `planner_hint`, ordinary context, `primarySymbols`, and `same_symbol` controls), Stage-6 provenance, and base/atom-scaled budgets. +6. Run the focused Plan 102 commands, then the full repository workflow: `pnpm run check`, `pnpm test`, and `pnpm build`. Fix all failures without implementing Plan 102 step 5 or later. + +## Tests + +- `config loader defaults and precedence for packet-packing experiment fields`: proves `false`/`base` defaults, repository-safe overrides, source attribution, and strict enum rejection. +- `eval packet-packing review overrides`: proves strict YAML parsing, both overrides reaching effective run config, and invalid budget modes failing closed. +- `packet atom flag-off parity and whole-file bypass`: proves dark mode preserves packet/artifact shape and direct whole-file/file-diff construction never enters the packer. +- `same-file packing compatibility and invariants`: proves atoms remain indivisible, coverage and requested-lens boundaries hold, interleaved partitions restore stable order, all hunks appear once in source order, caps split correctly, degradation reasons merge deterministically, and dispatch rank uses the existing combined changed-line formula. +- `same-file packing focus and lens safeguards`: proves a candidate that would newly omit a standalone routed lens or high/critical focus is left unpacked. +- `same-file packing profile floor`: proves absorbed strong `symbol_mention`/`planner_hint`, ordinary related context, and the same-name `primarySymbols` case cannot reduce effective profile or base budget, while `same_symbol` alone does not invent an investigate profile. +- `packed tool budget policies`: proves base parity, simple zero budget, one-atom/five-hunk parity, per-additional-atom scaling and 1.75x caps across standard/investigate/deep packets, unchanged rounds/source extension, and boost-last behavior. diff --git a/specs/plans/phase-plans/102/phase_2.md b/specs/plans/phase-plans/102/phase_2.md new file mode 100644 index 0000000..9d6c574 --- /dev/null +++ b/specs/plans/phase-plans/102/phase_2.md @@ -0,0 +1,33 @@ +--- +status: complete +--- + +# Phase 2: Packet-Packing Reports and Deterministic Replay + +## Overview + +Implement Plan 102 steps 5–6 only. This phase adds a fail-closed report tool for retained-run Stage-6 replay, repeated A/B/C evaluation analysis, collateral regression comparison, treatment/economics/tool-pressure accounting, and production-normalized payback. It then runs the exact four retained inputs without model calls, freezes `/tmp/plan102-packet-shape.json`, and records the deterministic packet/cap/context/lens/note reconciliation in Plan 102. Paid eval execution, private recall fixtures, rollout selection, and teardown remain out of scope. + +## Steps + +1. Add `scripts/packet-packing-report.ts` with an import-safe CLI and exported pure report builders/parsers. Define strict JSON boundary validation for retained-run artifacts, eval `info.json`/repeat execution artifacts, packets, events, model/tool calls, and cost sources. Make every missing/corrupt/ambiguous input or invariant violation a named report failure and non-zero CLI exit. +2. Implement `replay` mode. Parse `--repo`, repeated `--run`, and `--output`; verify each run directory and recorded base/head/merge-base refs; materialize the recorded head in a temporary detached Git worktree; rebuild the repository index and Stage 6 twice from the recorded diff, filter decisions, file facts, planner plan/dossier, and enabled lenses with packing off/base and on/base; capture all Stage-6 telemetry without any model runner; and remove the worktree in `finally`. +3. Compare replay outputs against recorded and off/on packet shapes. Enforce recorded flag-off artifact parity, reviewable-hunk bijection, atom preservation/order, file/language/source order, five-hunk and 12K caps, zero new coverage promotion, lens preservation, high/critical note preservation, profile/base-budget monotonicity, and the unchanged dispatch-rank formula. Emit stable per-run packet/atom counts, packet membership, changed scheduling position by hunk, derived-floor/profile/budget evidence, and cap/context/related-context/note/lens omissions. Apply the one-time 74/75/76 motivating-run rule only to real `combinedPatchChars()` measurement; reject all other target/count/ref failures. +4. Implement `eval` mode with explicit `--logs`, `--cohort latest|`, `--expected-repeats`, and `--output`. Persist each invocation run's exact owning log root/path so multi-root suites and repeated numeric run IDs resolve unambiguously. Select one complete latest cohort explicitly rather than pooling history, reject an interrupted newest invocation, validate A/B/C arm identity and effective config, and join executions bidirectionally by case, arm, repeat, and expectation. Resolve every B/C treatment atom to the matching A-repeat packet, require exact atom/hunk bijections, and independently recompute cap use, coverage/profile floors, base/effective budgets, and dispatch ranks from the artifacts. Require at least 8/10 treated executions when ten repeats are expected; repeat-1 preflight cannot select a rollout arm. +5. Compute evaluation views and economics. Strictly load candidate, verification, final-selection, and final artifacts and reconstruct their packet/hunk/changed-line/provenance/lineage joins, matched score IDs, per-repeat finding/count/cost metrics, and repeat aggregates. Use every execution for the primary intent-to-treat candidate/final rates and atom-count-bucketed loss histograms; emit a separately labeled treated-only view whose numerator and denominator both exclude untreated B/C executions. Aggregate actual cohort spend from all recorded model calls; aggregate reviewed atoms, repository-tool requested/used/rejected calls, rejection causes, result characters, continuations, service time, input/output tokens, and cost per reviewed atom. Enforce aggregate A-relative non-inferiority, zero-recall, treatment, per-atom pressure, strict C-improvement, and B-fail/C-pass 85%-saving-retention gates. +6. Add production economics and capacity gates to `eval` output when a baseline/selected production pair is present: exact external repo/refs, cache-off, concurrency-6, declared/effective 60-minute shape, exact bidirectional reviewed-hunk sets with 142/142 required, raw cost and cost per reviewed hunk, normalized 142-hunk equivalent costs, per-arm extrapolation labels, positive equivalent-review savings, cumulative-validation-cost break-even inputs/count, wall throughput/time, all-stage model-service seconds and total/reasoning tokens per hunk, Stage-7 continuations per atom, and normalized tool pressure. Fail closed on truncated work, missing evidence, non-improving throughput, regressions in any capacity metric, or a non-positive payback denominator. +7. Implement `regression` mode with explicit `--baseline-logs` and `--selected-logs`, independent explicit cohort selection, `--expected-repeats`, and `--output`. Verify only permitted YAML differences and selected effective config, independently require both cohorts to pass every required positive/negative expectation, then report eval errors/expectation transitions, deterministic negative/positive regressions, relational artifacts, packet/hunk/cap/profile/budget/rank invariants, treatment counts, dispatch-order movement, per-atom pressure, and actual cohort cost. Label the result as one-repeat collateral evidence and never combine it with repeated recall gates. +8. Add `tests/packet-packing-report.test.ts` using disposable synthetic retained runs and eval cohorts. Cover successful replay/eval/regression/production summaries plus fail-closed cases for strict packet/event/model/tool/coverage/info schemas, corrupt/missing or cross-artifact refs, interrupted cohorts, flag-off mismatch without source leakage, replay model calls and cleanup failure, hunk/atom loss or mismatch, cap/profile/base-budget/rank defects, bidirectional expectation mismatch, missing treatment, incorrect denominators, recall regression, valid untreated collateral, equal B/C tie selection, exact production work/capacity, missing spend data, and non-positive payback. +9. Run `pnpm test -- tests/packet-packing-report.test.ts`, the Plan 102 focused suite, `pnpm run check`, `pnpm test`, and `pnpm build`, iterating until clean. +10. Run the exact four-run no-LLM replay command against `/home/peter/Dev/0xsequence/trails-api` and the retained directories from Plan 102, writing `/tmp/plan102-packet-shape.json`. If and only if all deterministic gates pass, add a Plan 102 reconciliation note containing the JSON table, frozen motivating count (74/75/76 only), proxy-versus-real patch explanation when needed, per-run distribution, cap/context/related-context/lens/note/profile/budget/rank comparison, capacity multiplier, and completion projection. Stop with a roadblock on any unpermitted count/ref/invariant failure. + +## Tests + +- `replay rebuilds recorded Stage 6 without model calls`: verifies explicit refs/worktree cleanup, recorded inputs, stable off/on summaries, packet membership, cap/context/lens/note/profile/budget/rank reporting, and no LLM path. +- `replay fails closed on stale or corrupt evidence`: covers missing/corrupt artifacts and refs, recorded/off parity mismatch, hunk loss/duplication, atom split/reorder, cap/coverage/lens/focus/profile/budget/rank violations, and an unpermitted motivating packet count. +- `eval selects one explicit A/B/C cohort`: proves latest/id selection, arm/config identity, exact repeat joins, no mixed cohorts, and actual cost aggregation with the documented fallback. +- `eval reports intent-to-treat and treated-only recall separately`: proves all executions remain in the primary denominator while untreated B/C executions are removed from both treated-only numerator and denominator. +- `eval proves treatment and per-atom pressure`: proves Stage-6 provenance joins, target packet/coverage/lens/profile/budget/cap invariants, 8/10 thresholds, atom-normalized calls/rejections/results/continuations/service/tokens/cost, and rejection-cause reporting. +- `eval fails closed on recall, treatment, telemetry, and spend defects`: covers missing/mismatched telemetry, under-treatment, zero or out-of-band recall, missing cost, and invalid denominators. +- `production economics normalize equal reviewed work`: proves raw versus 142-hunk-equivalent cost, extrapolation labels, positive savings, validation break-even count, and failure on non-positive normalized savings. +- `regression compares explicit roots as collateral evidence`: proves separate cohort selection, permitted YAML/config deltas, expectation/eval transition detection, treatment/invariant/pressure/order/cost reporting, and no accidental recall-cohort pooling. diff --git a/specs/plans/phase-plans/102/phase_3.md b/specs/plans/phase-plans/102/phase_3.md new file mode 100644 index 0000000..77c5569 --- /dev/null +++ b/specs/plans/phase-plans/102/phase_3.md @@ -0,0 +1,30 @@ +--- +status: complete +--- + +# Phase 3: One-Repeat Packet-Packing Treatment Preflight + +## Overview + +Implement Plan 102 step 7 only. This phase creates two private, materializable Go fixtures and six repeat-one A/B/C recall cases, records the owner-approved paid-validation ceiling before model use, executes the cache-off one-repeat suite, and runs the exact cohort report preflight. The first cohort is retained as treatment-invalid evidence: it exposed real independent-planner variance, a fixture/config incompatibility, and three report artifact-contract bugs. This phase fixes the report bugs, repairs and revalidates the fixtures without model calls, and stops with a second paid repeat-one cohort pending. One-sample recall is measurement only; retry execution, repeat-10 execution, arm selection, collateral checks, production validation, rollout, and teardown remain out of scope. + +## Steps + +1. Reconcile current drift and working-tree state for Plan 102, inspect the private-eval conventions and existing eval schema, and record `approvedValidationCostUSD: $500` (superseding the earlier $200 authorization), current actual spend, preflight projection assumptions, and authorized scope in Plan 102 before any paid model call. +2. Create `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/recall/repos/dilution-control/{base,feature}` as a compact materializable Go repository. Put a locally detectable boundary regression in one separated function and safe unrelated edits in separated sibling functions so A keeps the target in one source atom while B/C combine it with unrelated atoms. +3. Create `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/recall/repos/cross-atom-consistency/{base,feature}` as a compact materializable Go repository. Change five separated sibling functions so four add a shared validation/veto while the fifth omits it, and keep baseline atom boundaries separate while allowing B/C to combine the inconsistent siblings. +4. Add `dilution-{a,b,c}.yml` and `consistency-{a,b,c}.yml` with `repeat: 1`, cache disabled, precise candidate/final expectations, and identical model/provider/reasoning/concurrency/depth/lens/time/token settings. Limit arm differences to A `false`/`base`, B `true`/`base`, and C `true`/`atom-scaled`. +5. Parse/materialize the private suite without model calls and compare all six YAMLs structurally to prove that only the permitted arm fields and case-specific identity/fixture/expectation fields differ. +6. Run `pnpm dev -- eval --eval-dir /home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/recall --no-cache` once, then run the exact `pnpm exec tsx scripts/packet-packing-report.ts eval --logs .../recall/logs --cohort latest --expected-repeats 1 --output /tmp/plan102-eval-preflight.json` command. +7. Inspect and preserve the invalid cohort evidence. Record invocation/cohort/run IDs, the immutable failed-report path/hash, actual preflight and cumulative costs, `10 * preflightCostUSD`, projected spend assumptions, the active $500 ceiling, target telemetry, root-cause classification, and one-repeat recall limitations. Stop paid work because all four B/C target treatments are invalid. +8. Fix only the three demonstrated report artifact-contract defects: packet-ID attention reconciliation, Stage-9 uncertainty-promotion provenance, and the distinction between valid A-side Stage-6 lens pruning and B/C routed-lens loss. Add focused positive/negative regressions and rerun the report locally over the immutable cohort to expose the remaining real treatment failures. +9. Repair the private deterministic preconditions without model calls. Restrict all arms to `lang/go`, make every sibling hunk a plausibly comparable boundary/validation-risk change while retaining the dilution boundary bug and consistency omission, rerun strict YAML/arm parsing, materialized Go tests, and the local atom/packing/cap/profile/budget/rank validator. Record the retry as pending; do not start it. +10. Run the focused Plan 102 test command, `pnpm run check`, the complete test suite, and the build. Do not change any case to repeat ten, run a later paid phase, select an arm, remove experiment flags, or commit. + +## Tests + +- `private packet-packing suite parses and materializes`: verifies all six strict YAML cases load with `lang/go` only, both Go repos materialize, and main/feature compile and pass `go test ./...` without model use. +- `A/B/C structural comparison`: verifies repeat/cache/model/provider/reasoning/concurrency/depth/lenses/time/token/fixture/expectation inputs match within each case and only the two permitted review fields differ across arms. +- `report artifact-contract regressions`: verifies attention joins by unique packet ID regardless of producer order, admits Stage 9 only for relational uncertainty promotion, and preserves the distinction between requested and validly routed lenses while still detecting B/C routed-lens loss. +- `local repaired treatment validator`: assigns identical normal coverage and `lang/go` to every hunk, then verifies A target packets contain one source atom, B/C targets combine all source atoms and reduce target-file packets, and all hunk/atom/cap/coverage/lens/profile/budget/rank invariants pass with zero model calls. +- `repository focused and complete gates`: verifies packet packing, configuration, eval loading, reporting, type/lint/format checks, all tests, and the production build remain clean after evidence is recorded. diff --git a/specs/plans/phase-plans/102/phase_4.md b/specs/plans/phase-plans/102/phase_4.md new file mode 100644 index 0000000..c4ea9e8 --- /dev/null +++ b/specs/plans/phase-plans/102/phase_4.md @@ -0,0 +1,32 @@ +--- +status: complete +--- + +# Phase 4: One-Repeat Treatment Retry + +## Overview + +Complete only the repaired retry of Plan 102 step 7. Re-verify the six current strict private cases, both materializable Go fixtures, the no-model treatment proof, budget authorization, and exact clean runtime provenance at commit `bb96fd3`. Then execute one cache-off repeat-one A/B/C invocation, analyze that exact invocation UUID with the packet-packing report, preserve the resulting JSON report immutably, and reconcile actual/projected validation spend. The retry gate failed: consistency B/C did not treat the target, and independently sampled B/C planner hints changed standalone profiles relative to A in both families. The phase stops without a third paid invocation. One-sample recall remains measurement only. Repeat ten, arm selection, collateral, production validation, rollout, and teardown remain out of scope. + +## Steps + +1. Confirm `HEAD` is exactly `bb96fd3439c715130756a93efd8e679772f81a9b`, the repository and Plan 102 in-scope paths are clean, all six private YAMLs remain `repeat: 1`, and the invalid cohort/logs/report retain their recorded hashes and contents. +2. Hash the six current YAMLs and both fixture trees, strictly load the suite, compare A/B/C definitions within each family, and prove that arm differences remain limited to name plus `review.packSameFileHunks` and `review.packedToolBudgetMode`. Verify cache disabled and the exact provider/model/reasoning/concurrency/depth/lens/time/token/cost settings. +3. Materialize both fixture base/feature repositories without model calls and run `go test ./...` in every materialized revision. Re-run the deterministic local treatment validator to prove exact hunk/atom order and bijection, five-hunk/12K caps, coverage/lens/profile/budget/rank invariants, A one-atom targets, B/C multi-atom targets, and reduced packed packet counts. +4. Reconcile the paid gate immediately before launch: cumulative actual spend is `$5.207657`; the retry reserves at most `$60`; the placeholder repeat-10 projection is `$52.076570`; the known later production reservation is `$95`; and `$212.284227 <= approvedValidationCostUSD: $500`. Record exact clean runtime provenance and launch exactly one complete `pnpm dev -- eval --eval-dir .../recall --no-cache` invocation. +5. Resolve the new invocation UUID and its six review run IDs from the invocation manifest. Run exactly `packet-packing-report.ts eval --cohort --expected-repeats 1`, never `latest`, and inspect the JSON for zero provider/configuration/evidence failures, valid B/C treatment in both families, and every deterministic/artifact invariant. +6. Copy the new JSON report without altering it into `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/reports/`, record its SHA-256, and leave the invalid cohort, all logs, and all reports untouched. If treatment or report validation fails, stop with the exact diagnosis and do not launch a third paid invocation. +7. Update Plan 102 reconciliation with invocation/cohort/run/review IDs, per-run and exact cohort cost, cumulative actual cost including `$5.207657`, `10 * validPreflightCostUSD`, the known future `$95` production reservation, total projected spend, and comparison with the `$500` ceiling. State that one-repeat recall is measurement only and that steps 8–12 remain `not_run`. +8. Run the focused Plan 102 tests, repository checks, complete test suite, and build. Do not change any private case to repeat ten, select an arm, start collateral or production validation, or tear down experiment scaffolding. + +## Tests + +- `clean runtime provenance and immutable evidence`: proves the paid command ran from exact commit `bb96fd3`, with a clean worktree, and that the prior invalid cohort/logs/report remained byte-identical. +- `strict repaired suite preflight`: proves all six repeat-one YAMLs parse, use only `lang/go`, differ only by the two permitted arm settings, disable cache, and retain identical paid configuration within each family. +- `fixture materialization and no-model treatment proof`: proves both fixture revisions pass `go test ./...` and the deterministic A/B/C treatment validator passes all atom, packet, cap, coverage, lens, profile, budget, and rank invariants without model calls. +- `exact paid cohort report`: proves the explicit new invocation contains six complete executions, both B/C arms receive the target treatment for both cases, paid evidence and costs reconcile, and all deterministic/artifact failure lists are empty. +- `repository focused and complete gates`: verifies packet packing, configuration, eval loading, reporting, lint/format/type checks, all tests, and the production build remain clean after reconciliation. + +## Outcome + +Invocation `5bd80f2c-865e-40f0-a605-07387138b904` completed six cache-off executions with zero errored cases and authoritative reconstructed cost `$3.986221`. The exact explicit-cohort report exited `1` with two consistency `insufficient_treatment` failures and four cross-arm standalone-profile `treatment_invariant` failures. It is preserved at `/home/peter/Dev/0xPolygon/codegenie-private-evals/trails-api/packet-packing/reports/plan102-eval-preflight-retry-invalid-5bd80f2c.json`, SHA-256 `650113d24d092e6fd712303e1828b297010c76f672d7efb799f5081f79635517`. Cumulative actual validation spend is `$9.193878`, within the `$500` ceiling. Steps 8–12 remain `not_run`; the next separately reviewed phase owns the required step-12 failure teardown. diff --git a/specs/plans/phase-plans/102/phase_5.md b/specs/plans/phase-plans/102/phase_5.md new file mode 100644 index 0000000..d1b0f3b --- /dev/null +++ b/specs/plans/phase-plans/102/phase_5.md @@ -0,0 +1,33 @@ +--- +status: complete +--- + +# Phase 5: Failed-Gate Teardown and Final Repository Gate + +## Overview + +Complete Plan 102 step 12 through its failed-outcome branch, then run step 13. Preserve every JSON report actually produced by completed phases and prove its immutable checksum manifest before deleting experiment scaffolding. Restore the original single packet-builder path, remove both temporary configuration fields and all experiment-only reporting, atom/profile-floor/provenance/budget/telemetry surfaces, and keep only generally useful hardening that remains independently used and tested. Retire private B/C declarations and retain an A declaration only when its baseline result satisfied the suite's existing expectation policy; keep all private fixture repositories, logs, reports, and invocation manifests unchanged. Reconcile Plan 102 as a complete failed outcome without claiming rollout, and execute the exact teardown checks plus the full repository gate without model calls. + +## Steps + +1. Reconcile the clean Phase 4 commit against pre-experiment commit `5aca256`, classify every experiment-series change as required teardown, dead report support, or independently used hardening, and verify that no unrelated working-tree changes overlap the phase. +2. Before product deletion, stage `/tmp/plan102-packet-shape.json` in the workspace together with a private-evidence application manifest. Verify the deterministic replay hash `77af0c38937bd6957806f05ad201cbad32461414c1d667824096208473c276fa` and both immutable invalid-preflight hashes already under the approved private reports root. Define `manifest.sha256` over exactly those three preserved JSON report basenames, then ask the primary context to mechanically copy the replay and install the manifest without changing any existing report, log, or invocation artifact. +3. Restore the pre-experiment packet-building product path in `src/pipeline/packet-builder.ts`: remove compatible-atom packing, atom-scaled budgets, atom wrappers/IDs/provenance, member-atom profile floors, treatment telemetry, and experiment-only exported helpers or metadata while preserving the original `hunkFirstGroups()` behavior and packet budgets. +4. Remove `packSameFileHunks` and `packedToolBudgetMode` from `src/types.ts`, `src/config/schema.ts`, `src/config/config-loader.ts`, `src/evals/eval-runner.ts`, and every remaining eval/config/runtime artifact path. Remove dead report-only support from eval artifacts/commands, pipeline composition/review/verification, telemetry artifacts, and tests unless a surface remains generally used and independently covered after the report is gone. +5. Delete `scripts/packet-packing-report.ts` and `tests/packet-packing-report.test.ts`; remove all packet-packing-only cases from `tests/pipeline-phase5.test.ts`, `tests/config-loader.test.ts`, and `tests/evals.test.ts`, preserving focused tests for the restored baseline behavior. Confirm no packet-packing golden fixture exists. +6. Stage private suite mutations in the workspace for primary-context mechanical application. Remove `consistency-{b,c}.yml` and `dilution-{b,c}.yml`. Because `consistency-a` failed both declared expectations and `dilution-a` failed its final expectation, remove both active A declarations under the suite's existing pass policy. Do not touch `repos/`, `logs/`, `reports/`, invocation manifests, or create production/collateral cases. +7. Update Plan 102 with the final evidence inventory, both paid invocation UUIDs and all twelve review run IDs, private log/report paths and hashes, final cumulative cost `$9.193878` against the `$500` ceiling, steps 8–11 as `not_run` for the failed repaired retry gate, and step 12 complete through baseline restoration. Mark the plan and plan index `COMPLETE (failed gate; baseline restored)` according to repository conventions and update any affected normative project documentation only where it currently describes the temporary experiment as shipped behavior. +8. Run the exact evidence-manifest checksum check, both live-code/private-active-YAML retired-field greps, strict private suite parsing with no model calls, report script/test absence, and focused baseline tests. Verify the private report directory contains exactly the three produced JSON reports covered by `manifest.sha256` and that fixture repositories, logs, reports, and invocation manifests remain present and unchanged. +9. Run the complete step-13 gate: `pnpm run check`, `pnpm test`, `pnpm build`, and `git diff --check`. Iterate until every check passes and report the failed-outcome teardown ready for review without committing. + +## Tests + +- `evidence manifest`: `sha256sum -c manifest.sha256` passes for exactly the deterministic replay and two invalid one-repeat preflight reports. +- `live-code teardown`: retired config names have no matches under `src`, `scripts`, `tests`, or `evals`; report script and report tests do not exist. +- `private active declarations`: no retired fields remain in active YAML, B/C and failing A declarations are absent, retained suite directories strictly parse when active YAML exists, and no model call is made. +- `focused baseline behavior`: `pnpm test -- tests/pipeline-phase5.test.ts tests/evals.test.ts` passes with the original `hunkFirstGroups()` packet path and ordinary tool-budget behavior. +- `complete repository gate`: `pnpm run check`, `pnpm test`, `pnpm build`, and `git diff --check` all exit zero. + +## Outcome + +The failed-outcome branch is complete. Three produced JSON reports are preserved under the private reports root and pass their exact three-entry manifest; both fixture repositories, all 12 paid run directories, and both invocation manifests remain unchanged. All six active A/B/C declarations were retired because neither A baseline satisfied the suite's all-expectations-pass policy. The live product, configuration, eval, telemetry, report, and test surfaces are restored to the pre-experiment baseline with no dark packing path. Plan 102 and its index record `COMPLETE` as a failed gate with baseline restoration, steps 8–11 remain `not_run`, cumulative spend is `$9.193878` against the `$500` ceiling, and the focused plus complete repository gates pass without model calls. diff --git a/specs/plans/phase-plans/103/phase_1.md b/specs/plans/phase-plans/103/phase_1.md new file mode 100644 index 0000000..c3a59b7 --- /dev/null +++ b/specs/plans/phase-plans/103/phase_1.md @@ -0,0 +1,49 @@ +--- +status: complete +--- + +# Phase 1: Dark Compatible-Atom Packing + +## Overview + +Implement Plan 103 steps 1–3 only. This phase adds the two eval/internal-only settings, wraps today's `hunkFirstGroups()` output as explicit indivisible atoms, and implements Plan 102's compatibility partition and source-order greedy fill behind the dark flag. It reproduces Plan 102's already-validated packet counts and nothing more. + +Multi-member symbol context, transactional rejection, the profile floor, the pinned-plan seam, the report script, and every paid phase are out of scope. Because this phase deliberately stops short of the context work, packing must remain **off by default** and no packed packet may be dispatched in a real review until Phase 2 lands — a packed packet without multi-member context would silently drop non-primary members' surrounding source. + +## Steps + +1. Add `review.packRelatedHunks: boolean` (default `false`) and `review.packMaxHunks: number` (default `5`, rejected above `MAX_HUNKS_PER_PACKET`) to `CodegenieConfig`, `codegenieConfigSchema`, and `defaultConfig`. Add both to the strict eval-case `review` schema and `applyCaseReviewConfig()`. Do **not** add either to `rawConfigSchema` or `REPO_SAFE_REVIEW_KEYS`: no `codegenie.toml` may set them and no config-source attribution is claimed for a value that cannot come from a config file. Add `evals/packet-dilution/logs/` and the currently missing `evals/skill-semantics/logs/` to `.gitignore`. +2. Refactor `hunkFirstGroups()` results in `src/pipeline/packet-builder.ts` into explicit packet atoms without changing their membership. Each atom carries its ordered `PlannedHunk[]`, hunk count, `combinedPatchChars()` size, first source position, effective coverage, normalized planner lens signature (stable serialization of the sorted deduplicated union of `decision.lenses`), and a stable ID derived from ordered hunk IDs. **Scope correction:** the plan's atom description also lists standalone review profile and standalone per-member context quality. Both require the dry-build machinery that Phase 2 owns, and neither is needed to partition or fill, so they are captured in Phase 2 alongside the transactional evaluation that computes them rather than half-built here. Preserve the direct `whole-file` and content-probed `file-diff` returns from `groupHunks()` so they bypass the packer entirely, and bypass all atom work when the flag is off so packet artifacts, IDs, ordering, profiles, context, lenses, and budgets stay byte-identical. +3. Implement the compatibility partition and fill. Partition each file's atoms by `(effectiveCoverage, normalizedPlannerLensSignature)`; fill greedily in source order under `packMaxHunks` and `MAX_PATCH_CHARS`; order packets by earliest member hunk and render each packet's hunks by file position. Materialize combined groups through the existing `packetGroup()`/`packetKind()` rules, carry non-empty `degradationReason` values through a sorted deduplicated `"; "` join, do not synthesize `wholeFileText` or `fileContext`, and recompute `dispatchRank` with the unchanged `packetDispatchRank(filePath, facts, combinedChangedLines)` formula. +4. Add focused coverage in `tests/config-loader.test.ts`, `tests/evals.test.ts`, and `tests/pipeline-phase5.test.ts`. +5. Run the focused commands below, then `pnpm run check`, `pnpm test`, and `pnpm build`. Fix all failures without starting Phase 2. + +## Tests + +- `config defaults and eval-only surface`: proves `false`/`5` defaults, eval-case overrides reaching resolved config, `packMaxHunks > 5` rejected, and a `codegenie.toml` setting either key failing strict parsing rather than being silently accepted or filtered. +- `packet atom identity and flag-off parity`: proves atoms preserve today's group membership exactly, atom IDs are stable under reordering of equivalent inputs, and flag-off packet artifacts and telemetry are byte-identical to the pre-change baseline. +- `whole-file bypass`: proves direct whole-file and content-probed file-diff groups never enter the packer. +- `compatible partition and fill`: proves atoms are never split, every reviewable hunk appears exactly once, hunks render in file position order within a packet, packets order by earliest member, coverage and lens signature both gate membership, cap splits occur at `packMaxHunks` and `MAX_PATCH_CHARS`, interleaved partitions restore stable order, degradation reasons merge deterministically, and dispatch rank equals the existing formula over combined changed lines. +- `packMaxHunks parameterization`: proves caps of 1, 3, and 5 produce the packet shapes the Phase 4 curve depends on. + +## Verification commands + +```bash +pnpm exec vitest run tests/config-loader.test.ts tests/evals.test.ts +pnpm exec vitest run tests/pipeline-phase5.test.ts +pnpm run check && pnpm test && pnpm build +``` + +## Outcome + +Complete. `pnpm run check`, `pnpm test`, and `pnpm build` all pass; the suite grew from 761 to 773 tests with no existing test modified. + +**Config surface.** `review.packRelatedHunks` (default `false`) and `review.packMaxHunks` (default `5`) exist in `CodegenieConfig`, `codegenieConfigSchema`, `defaultConfig`, the strict eval-case schema, and `applyCaseReviewConfig()`. They are absent from `rawConfigSchema`, `DEFAULT_SOURCE_PATHS`, and `REPO_SAFE_REVIEW_KEYS`, and tests prove a `codegenie.toml` or user `config.toml` setting either key throws `invalid config file` rather than being silently filtered. `MAX_PACK_HUNKS` is duplicated in `schema.ts` rather than imported from `packet-builder.ts` to keep the config schema free of pipeline dependencies; a test asserts it equals 5 and the resolved schema rejects 6. + +**Packer.** `PacketGroup` gained a required `origin` field so the direct whole-file and content-probed file-diff returns are structurally excluded from packing rather than inferred from `kind`, which is ambiguous — `packetGroup()` can also produce `file-diff`. `packRelatedHunks()` partitions a file's atoms by `(effectiveCoverage, normalizedLensSignature)`, fills sequentially in source order under `packMaxHunks` and `MAX_PATCH_CHARS`, orders packets by earliest member, and rebuilds combined groups through the existing `packetGroup()`/`packetKind()` rules with degradation reasons merged by sorted deduplicated join. + +Fill is sequential-flush, matching Plan 102's described algorithm, not the best-fit search used in the planning simulation. Sequential preserves strict source order; best-fit would let a later small atom jump ahead of an earlier larger one. The two differ by about one packet on the retained runs, inside the documented residual, and Phase 3's replay measures the real number. + +**Verified behaviours.** Packing 6 compatible atoms at cap 5 yields `[h1-h5][h6]`; a one-hunk cap is byte-identical to flag-off (asserted by full artifact comparison, which is the strongest parity check available without a pre-change baseline); a `deep` hunk among `normal` siblings and a `core/tests` hunk among `core/code-review` siblings both stay separate; every hunk appears exactly once with in-packet source ordering; a two-hunk atom survives a one-hunk cap intact; whole-file groups never enter the packer; dispatch rank equals the existing formula over combined changed lines; and caps 1/3/5 over 15 atoms produce exactly the 15/5/3 packet shapes Phase 4's recall curve depends on. + +**Not yet safe to enable.** Multi-member symbol context is Phase 2. Until it lands, a packed packet containing atoms with different primary symbols would carry only the top-ranked symbol's source, leaving the rest as bare diffs. The flag stays `false` and no packed packet may reach a real review before Phase 2. diff --git a/specs/plans/phase-plans/103/phase_2.md b/specs/plans/phase-plans/103/phase_2.md new file mode 100644 index 0000000..3342e86 --- /dev/null +++ b/specs/plans/phase-plans/103/phase_2.md @@ -0,0 +1,54 @@ +--- +status: complete +--- + +# Phase 2: Multi-Member Context, Transactional Rejection, and the Profile Floor + +## Overview + +Implement Plan 103 steps 4–5, plus the atom metadata deferred from Phase 1. A packed packet now carries surrounding source for every member symbol under an explicit budget, candidates are evaluated in isolation and abandoned rather than committed when they would lose something, and a packed packet can never review below its strongest standalone member's profile. + +The pinned-plan seam, the report script, the replay, and every paid phase remain out of scope. + +## Steps + +1. Add `PACKET_SYMBOL_CONTEXT_BUDGET = 5_000`, `MIN_MEMBER_SYMBOL_CHARS = 800`, and `MIN_SLICED_MEMBER_CHARS = 600`, and implement `readMemberSymbolSources()`: participants are distinct resolvable primary symbol identities, each gets `floor(budget / count)` subject to the floor, one redistribution pass hands surplus from members needing less to members needing more in source order, and a single-symbol packet delegates unchanged to `readEnclosingSymbolSource()` so flag-off behaviour is untouched. +2. Thread member symbol facts through `buildContext()` and `buildPacket()`, and return per-member emitted characters alongside the packet. +3. Apply the profile floor **before** lens routing, since `routedPacketLenses()` prunes `core/code-review` at a `simple` profile — raising the profile afterwards would leave a lens dropped that the floor exists to preserve. +4. Evaluate every multi-atom candidate transactionally: build each member standalone and the candidate itself against a scratch relationship accumulator and a suppressed telemetry sink, compare, then either rebuild for real or emit the members separately with a recorded reason. +5. Add focused coverage and run the full repository gate. + +## Tests + +- `renders every member's symbol source in a packed packet`: three distinct symbols all appear in the packed context. +- `keeps symbol source inside its budget`: symbol section stays within the 5,000-character allowance, total context within 8,000, and all four members survive final rendering. +- `cannot starve a member at the shipped cap`: pins the arithmetic that makes the budget rejections dormant at cap 5. +- `floors a packed profile to its strongest standalone member`: a `same_symbol` edge absorbed by packing cannot lower the effective profile below what the members held alone. +- `shares one symbol budget when packed members resolve to the same symbol`: identical name *and* range is one participant, not several. + +## Verification commands + +```bash +pnpm exec vitest run tests/pipeline-phase5.test.ts +pnpm run check && pnpm test && pnpm build +``` + +## Outcome + +Complete. `pnpm run check`, `pnpm test`, and `pnpm build` pass; 773 → 778 tests with no existing test modified. + +**Multi-member symbol context.** `readMemberSymbolSources()` reads every distinct primary symbol under a shared 5,000-character budget, leaving at least 3,000 of `MAX_CONTEXT_CHARS` for outline, tests, and hints. Symbol source renders at the head of the packet context and `truncateTail()` keeps the head, so the reserve is what guarantees survival — the budget cannot be crowded out by later sections. Per-member survival is nevertheless verified against the final rendered text by locating each member's `Primary symbol:` header, so rule 7 is checked rather than assumed. + +**Transactional evaluation.** Candidates dry-build against `scratchRelationshipGraph()` — a shallow clone with fresh `relatedContextAttached`/`relatedContextOmitted` accumulators — and a quiet telemetry sink, with throwaway symbol-context and build metrics. Only a candidate that passes every check is rebuilt against the real recorder. Single-atom packets build once, unchanged; packed packets cost two builds, which is acceptable given Stage-6 repository-tool runtime was 5.2 seconds across the whole motivating run. + +**Profile floor.** Applied before routing, for the reason in step 3. Verified end-to-end with the case Plan 102 described: `h1` and `h3` share an enclosing symbol but are separated by `h2`, so the grouper yields three atoms with a `same_symbol` edge; packing absorbs the edge target, which would otherwise derive a weaker profile. + +### Two findings that changed the design + +**A short symbol is not a collapsed member.** The first implementation rejected any member emitting fewer than `MIN_SLICED_MEMBER_CHARS`, which fails a packet whose symbol is simply small — a 500-character function is *fully* represented. Members now carry a `complete` flag and only an incomplete member below the floor counts as collapsed. Without this, packing would have been rejected for most real packets; four tests caught it immediately. + +**Both budget rejections are unreachable at the shipped cap, and that is now pinned.** A packet holds at most 5 hunks, so at most 5 distinct primary symbols. `5 × 800 = 4,000 ≤ 5,000`, so oversubscription cannot fire; and the smallest share is `5,000 / 5 = 1,000`, above the 600 sliced minimum, so truncation-collapse cannot fire either. They are retained as defensive invariants because the plan specifies them and because a cap increase past six would make them live. Rather than contrive an unreachable end-to-end scenario, a test pins the arithmetic so any future cap change surfaces here. Two originally-drafted tests were removed for asserting behaviour that cannot occur. + +The same reasoning applies to `routed_lens_lost`: the partition key already forces identical requested lens sets across a candidate's members, so ordinary lens loss cannot occur. It remains reachable only through packet-level routing predicates such as `shouldKeepTestsLens()` reacting to a combined packet's `relevantTests`, which is why the check stays. + +**Still not enabled.** The flag remains `false`. Phase 3 owns the pinned-plan seam, the report script, and the four-run replay whose fixed-slot hunk yield gate decides whether any paid phase is authorized. diff --git a/specs/plans/phase-plans/103/phase_3.md b/specs/plans/phase-plans/103/phase_3.md new file mode 100644 index 0000000..66fc147 --- /dev/null +++ b/specs/plans/phase-plans/103/phase_3.md @@ -0,0 +1,64 @@ +--- +status: complete (step 7); step 6 deferred to phase 4 +--- + +# Phase 3: The Free Replay Gate + +## Overview + +Plan 103 step 7: port the report script and run the four-run replay whose fixed-slot hunk yield decides whether any paid phase is authorized. + +**Step order was inverted deliberately.** The plan lists the pinned-plan seam (step 6) before the replay (step 7), but the seam is paid-phase infrastructure and the replay is the gate that decides whether a paid phase happens at all. Building the seam first risked constructing infrastructure for a phase that might never run. Step 6 moves to the front of Phase 4, where it is used. + +## Steps + +1. Add `scripts/packet-packing-report.ts` with a `replay` mode that rebuilds Stage 6 from recorded run artifacts using the real builder — resolved input, diff parse, file filter, classification, repository index, `buildReviewPackets()` off and on — with zero model calls. +2. Emit structured, templated failure records: a closed-set code plus typed fields, rendered from a template. No raw exception text, no repository source, no hashing. +3. Implement the fixed-slot hunk yield estimator over the Stage-7 scheduling tuple, and gate it on reconciling against the historical run before it may define the break-even ratio. +4. Add focused tests and run the four-run replay. + +## Tests + +- `renders failures from typed fields without raw text` +- `orders packets by the stage 7 scheduling tuple` +- `counts distinct hunks within a fixed dispatch slot budget` +- `passes a clean pack and fails closed on every invariant violation`: hunk loss, hunk duplication, cap breach, coverage change, profile downgrade, budget downgrade, and lens drop each produce their code. + +## Outcome + +**The gate passes.** `pnpm run check`, `pnpm test`, and `pnpm build` pass; 778 → 782 tests. + +| Run | Packets off→on | Reduction | Yield @56 slots | Reviewable hunks | +| --- | --- | ---: | --- | ---: | +| `dca8d870` | 96 → **75** | 21.9% | **89 → 109** (+20) | 142 | +| `81f806a6` | 93 → **68** | 26.9% | 89 → 116 (+27) | 136 | +| `740d73f2` | 93 → 69 | 25.8% | 90 → 116 (+26) | 137 | +| `fe1548ae` | duplicate diff of `740d73f2` | | | | + +Zero failures, `noModelCalls: true`, three distinct diffs. Preserved at `packet-dilution/reports/plan103-replay.json`, SHA-256 `20321c65fefd02167eabe01584e6bf7f0b0728b008c6a5ea0f49dc9e6eb73a63`. + +Against the pre-registered gates: estimator reconciliation exactly 89 ✓; flag-on yield 109 ≥ 102 ✓; reduction ≥20% on every distinct diff ✓; deviation from Plan 102's frozen counts is **0** on both post-Plan-100 runs ✓; zero coverage changes, profile or budget downgrades, lens drops, cap breaches, or hunk loss ✓. + +**Break-even ratio for Phase 4: `B = 89 / 109 = 0.8165`.** + +### The reconciliation gate earned its place immediately + +The first replay produced 114 off-packets against the recorded 96, and 161 reviewable hunks against 142 — and the estimator gate caught it rather than the number being quietly accepted. The diff itself was byte-correct (88 files, 217 hunks, matching the recorded totals exactly), so the divergence was downstream. + +Cause: the reviewed repository carries its own `codegenie.toml` with `classification.pathRules` that skip generated webrpc clients and docs, schema dumps, and e2e snapshots, and raise `workers/`, `lib/intentmachine/protocol/`, and migrations to critical priority. The real run loaded it — 88 files, 61 kept, 27 skipped. Replaying with bare `defaultConfig` kept files the real run never reviewed, silently changing the workload. `applyRepoConfigLayer()` fixed it, after which `dca8d870` reproduces Plan 102's frozen 96 → 75 and its 142 reviewable hunks exactly. + +Had the gate not existed, the replay would have reported a plausible-looking 20.2% reduction on a workload that was not the one being claimed. + +### Two older runs legitimately diverge + +`740d73f2` and `fe1548ae` replay at 93 packets where their artifacts recorded 109. Both predate Plan 100, which changed hunk identity, and both are the runs whose planner output was entirely dropped. Plan 102's own reconciliation note flagged that these three older artifacts need a compatibility view. Their off→on comparison remains internally valid — identical inputs on both sides — so they contribute reduction evidence but not historical-count evidence. The gate is scoped to `dca8d870`, the only post-Plan-100 run with surviving planner coverage. + +### Design note + +`comparePackets()` originally tested coverage in one direction, catching promotion but not demotion — the more dangerous case, since a demoted hunk is reviewed more shallowly than planned. Because the partition key forces identical coverage across a packet's members, any change at all is a violation, so the check is now an inequality and the code is `coverage_changed`. A test covers it. + +The estimator is documented in the script as a calibrated counterfactual capacity proxy, not a scheduler reproduction: the real dispatcher is a prefix-with-holes at concurrency above one. + +## Remaining before Phase 4 + +Step 6 — the versioned `PinnedPlanArtifact`, its validation, and `scripts/draw-pinned-plan.ts` — is now authorized and moves to the head of Phase 4, together with the owner-approved validation ceiling that Phase 4 cannot start without. diff --git a/specs/plans/phase-plans/103/phase_4.md b/specs/plans/phase-plans/103/phase_4.md new file mode 100644 index 0000000..5c60ebd --- /dev/null +++ b/specs/plans/phase-plans/103/phase_4.md @@ -0,0 +1,155 @@ +--- +status: complete (synthetic curve abandoned; see outcome) +--- + +# Phase 4: Paid Validation + +## Overview + +Plan 103 step 8, plus step 6 carried forward from Phase 3. Every free precondition runs before the first paid call, in the order the plan requires: fixture construction, expectation validation under the fake provider, the authored frozen plan, the reference-draw realism gate, and the model-free treatment proof. + +## Authorization + +**`approvedValidationCostUSD: $300`**, recorded in the plan on 2026-07-25. The owner approved the `$119` reservation and raised the ceiling to `$300`, and authorized running the production capacity pair inside this phase rather than deferring it. Provider credentials are present (`anthropic`, default `claude-opus-4-8`). + +Spend to date: **`$0.00`**. + +## Completed + +**Fixture.** `evals/packet-dilution/repos/dilution/{base,feature}` — one Go file, fifteen independent guard functions separated by ~62 lines, comfortably beyond `NEARBY_GAP_LINES = 30`, so today's grouper yields exactly fifteen atoms. Three bugs at atom positions 1, 10 and 13: + +- atom 1 — `WithinTransferLimit` changes an inclusive bound to exclusive, rejecting the limit value itself; +- atom 10 — `ShouldRetry` changes `<` to `<=`, allowing one attempt beyond the maximum; +- atom 13 — `ShardIndex` maps onto `shards + 1`, producing an index outside the configured range. + +Atom 7 is the negative control: `used >= capacity` rewritten as `!(used < capacity)`, which looks like a boundary edit and is provably equivalent. The remaining eleven guards are safe range additions. + +**Treatment proof.** `packet-packing-report.ts treatment` rebuilds Stage 6 at caps 1/3/5 with zero model calls and asserts the exact shape the curve depends on: + +| Cap | Packets | Target packet size | Distinct target packets | +| ---: | ---: | --- | ---: | +| 1 | 15 | 1, 1, 1 | 3 | +| 3 | 5 | 3, 3, 3 | 3 | +| 5 | 3 | 5, 5, 5 | 3 | + +No two targets share a packet at any cap. Preserved at `packet-dilution/reports/plan103-treatment-proof.json`. + +### The proof caught a fixture defect on first run + +The initial invocation targeted the hunk at line 679, which is atom **11**, not atom 10. At cap 5 that places two targets inside `[11-15]`, collapsing two of the three recall opportunities into one packet and one model conversation — exactly the clustering defect the review flagged before implementation. The `targets_share_packet` check failed closed and the target was corrected to line 617. Had the proof not existed, the curve would have run at $14 with two of three observations correlated. + +## Paid validation log + +| Step | Executions | Cost | Result | +| --- | ---: | ---: | --- | +| Matcher-calibration smoke, cap 1, repeat 1 | 1 | `$1.5154` | **pass, 4/4 expectations**, zero losses | +| Realism draws, three unpinned cap-5 runs | 3 | `$2.8480` | **gate failed 0/3** | + +**Total spend `$4.3634`** of the `$300` ceiling. The `$27.36` curve was not run. + +## The realism gate failed — stop condition reached + +All three unpinned draws placed every target in a **three**-hunk packet, not five, and put **all three targets in the same packet**: + +``` +draw 1 packet deep 3h starts: 59, 617, 806 +draw 2 packet deep 3h starts: 59, 617, 806 +draw 3 packet deep 3h starts: 59, 617, 806 +``` + +The real planner grades exactly the three bug hunks `deep` and everything else `normal`/`light`, so the fifteen atoms split across three coverage partitions. The authored plan's uniform `normal` coverage produces one partition and five-hunk packets; production produces three partitions and a three-atom deep packet. The curve would have measured dilution at a packet shape production does not build for these hunks. + +It also collapses the independence the fixture was designed for: three targets in one packet is one model conversation, not three, which is the clustering defect the treatment proof was written to prevent — reappearing through the planner rather than through my choice of line numbers. Draws 1 and 3 are byte-identical, so this is deterministic behaviour, not sampling variance. + +**Cause: the fixture violates this plan's own rule 3** — *each bug must be one the planner grades ordinarily; a bug obvious enough to draw a `deep` grade on its own lands in a different partition and never packs*. Comparator-boundary bugs are exactly that obvious. This is the same structural finding the artifact-only diagnostics produced before implementation, now confirmed live: the planner grades risky hunks differently from their safe siblings. + +Per the pre-registered stop condition, the fixture must be redesigned before any reviewer call. The gate cost `$2.85` and prevented `$27.36` of measurement at the wrong shape. + +### Three ways forward, for the owner to choose + +1. **Subtler bugs.** Rewrite the three defects so the planner grades them `normal`, putting them in the majority partition. Risk: a bug subtle enough to be graded ordinary may also be one the reviewer never finds, collapsing `R1` and voiding the curve from the other direction. +2. **Re-target the curve at the shape production builds.** Measure 1 → 2 → 3 rather than 1 → 3 → 5, since a three-atom deep packet is what these hunks actually produce. Needs more bug-bearing atoms so targets do not share a packet at the upper caps. +3. **Keep the authored plan and downgrade the claim.** Run the curve as a controlled isolation instrument and state explicitly that it measures dilution at a five-hunk shape the planner did not choose for this fixture, so it bounds harm rather than describing production. + +Option 2 is the most faithful to what the plan is trying to establish, and it is the cheapest to reach from here. + +### Side evidence, not a gate result + +The three unpinned draws are real cap-5 recall samples under a live planner: two found all three bugs, one lost the retry bug at verification (`lost-at-verification=1`, zero missed before candidate generation). The pinned cap-1 smoke run found all three. Nine of ten target opportunities were hit across four paid executions. This is not a controlled comparison and decides nothing, but nothing so far suggests packing is harmful. + +## Second fixture, same wall — synthetic measurement abandoned + +The money-path redesign tested a real hypothesis: that the planner's coverage decision tracks **risk surface** rather than **detected defects**, so a file where every function handles money in minor units would be graded uniformly. Fifteen fee/settlement functions, three carrying money-semantic defects (a dropped division remainder in `SplitFee`, a cap applied before the rate in `PlatformCut`, a refund on gross rather than net in `RefundAmount`) at atoms 1, 7 and 13 — spacing that keeps targets in separate packets at every cap, which the previous layout only managed at caps 1 and 3. + +The model-free proof passed: 15/5/3 packets, three distinct target packets at each cap. The realism gate then failed identically to the first fixture: + +``` +draw 1 packet deep 3h starts: 63, 462, 861 <- the three defects, one packet + packet light 2h starts: 130, 995 + packet normal 5h starts: 197,264,331,398,529 + packet normal 5h starts: 596,663,730,797,928 +``` + +Planner entries `deep:3, light:2`. It located precisely the three defective functions. Making every function money-critical did not disguise them; it grouped them more cleanly. + +**The hypothesis is falsified.** Coverage tracks detected defects, not just risk surface. + +### The obstacle is structural + +Four fixture designs across two plans have now failed the same way: + +| Fixture | Design | Outcome | +| --- | --- | --- | +| 102 `cross-atom-consistency` | missing guard among siblings | target `deep`, siblings `light`; 0/1 treated | +| 102 `dilution-control` | boundary bug among safe changes | planner variance invalidated the comparison | +| 103 `limits` | comparator flips among range guards | three bugs, one `deep` packet; 0/3 | +| 103 `settlement` | money-semantic bugs in a uniformly money-critical file | three bugs, one `deep` packet; gate failed | + +A synthetic fixture cannot produce the condition the curve requires — a defect packed alongside safe siblings at equal coverage — because the compatibility predicate separates them by construction. Any bug made findable enough to measure is also visible enough to be graded `deep`. This is the harness working correctly, and it is not solvable by better fixture craft. + +**Step 8's synthetic recall curve is `not_run`, by design rather than by omission.** The four-fixture record is the finding: the curve is the wrong instrument for this question. + +## Outcome + +Preconditions complete, infrastructure shipped and tested, synthetic curve abandoned on structural grounds. + +**Total paid validation: `$5.3850`** of the `$300` ceiling — one calibration smoke run (`$1.5154`), three first-fixture realism draws (`$2.8480`), one second-fixture realism draw (`$1.0216`). Nothing spent on the curve. + +Phases 8 through 11 of the plan are `not_run`. Steps 9 (collateral) and 10 (production capacity pair) were never reached because the gate that authorizes them never passed. + +### What the paid runs did establish + +Four executions produced findings against these fixtures, and nine of ten target opportunities were hit — three of three at cap 1 pinned, three of three and two of three and three of three across the unpinned cap-5 draws — with the single loss occurring at verification rather than before candidate generation, and `partial-match=0` throughout. This is not a controlled comparison and decides nothing about dilution. It does mean nothing observed suggests packing is harmful. + +The pinned-plan seam was also confirmed live: `planner_plan_pinned` fired, Stage 5 made no planner call, and Stage 6 produced exactly the treatment-proof shape. + +**The smoke run replaced the planned fake-provider validation.** The fake runner emits a generic trigger-based finding, so it can only exercise wiring, not matcher semantics — and matcher semantics is exactly what killed three of Plan 102's executions. One real cap-1 execution answered the real question instead: all three bugs found, the `AtCapacity` control correctly not flagged, `missed-before-candidate-generation=0`, `lost-at-verification=0`, `partial-match=0`. No matcher calibration was needed. + +It also confirmed the pinned-plan seam end to end: `planner_plan_pinned` fired once, Stage 5 made no planner call, and Stage 6 produced exactly 15 packets at cap 1 — the treatment-proof shape, now verified in a live review rather than only model-free. + +**Cost is 2× the plan's projection.** Measured `$1.5154` per execution against the `$0.78` estimated from Plan 102's Go fixtures — this fixture is larger (15 hunks, 935 lines) and runs at `reasoning: high` on `claude-opus-4-8`. Revised projections: + +| Phase | Executions | At `$1.52` | +| --- | ---: | ---: | +| Smoke (spent) | 1 | `$1.52` | +| Realism draws | 3 | `$4.56` | +| Curve, 6 repeats × 3 arms | 18 | `$27.360` | +| Reserved extension, if triggered | 18 | `$27.36` | +| Reserved Void rerun, if triggered | 18 | `$27.36` | +| Production capacity pair | 2 | `$50` | +| **Worst case, all contingencies** | | **`$138`** | + +Comfortably inside the `$300` ceiling; the plan's `$119` reservation would have been tight had both contingencies fired. + +### Original remaining-work list, now superseded by the gate failure + +1. **Step 6 — the pinned-plan seam.** Versioned `PinnedPlanArtifact` with `baseSha`, `headSha`, `planSha256` over a canonical sorted-key serialization; `review.pinnedPlanPath` validation failing closed on wrapper schema, plan schema, hash, base, head, and hunk-ID membership; `scripts/draw-pinned-plan.ts` as an internal script rather than a CLI verb; one test per failure mode. +2. **Eval case YAMLs** — `cap1.yml`, `cap3.yml`, `cap5.yml` at `repeat: 6`, cache off, `lang/go` only, identical except `packMaxHunks`, all pinned to one authored plan. +3. **Expectation validation under the fake provider** — confirm each `should_find` matcher fires on the expected finding shape, and the `should_not_find` control does not, for `$0`. +4. **Authored frozen plan plus three reference draws** and the realism report requiring at least 2 of 3 to place every target in a five-hunk packet at cap 5. + +Only after all four does the `6 × 3` curve run. + +## Outcome + +_Preconditions complete; paid validation not started._ diff --git a/src/cli/review-command.ts b/src/cli/review-command.ts index 21a1ea2..f6a5594 100644 --- a/src/cli/review-command.ts +++ b/src/cli/review-command.ts @@ -54,6 +54,7 @@ type CommanderReviewOptions = { cache?: boolean; ci?: boolean; progress?: boolean; + packRelatedHunks?: boolean; }; export function parseReviewCommand( @@ -97,6 +98,10 @@ export function parseReviewCommand( .option("--no-progress", "disable the interactive progress spinner") .option("--cache", "enable local model-call cache for this run; provider prompt caching is reported separately") .option("--no-cache", "disable local model-call cache for this run; provider prompt caching is reported separately") + // Plan 103: per-run toggle for the packing A/B. Deliberately CLI-only — + // no codegenie.toml or user config may set it, so a repository cannot + // enable experimental packing for everyone who reviews it. + .option("--pack-related-hunks", "experimental: pack related same-file hunks into fewer review packets") .action((commitArgs: string[], options: CommanderReviewOptions) => { commits = commitArgs; commandOptions = options; @@ -285,6 +290,9 @@ function buildCliOverrides(options: CommanderReviewOptions): CliConfigOverrides if (options.depth !== undefined) { cli.depth = parseDepth(options.depth); } + if (options.packRelatedHunks !== undefined) { + cli.packRelatedHunks = options.packRelatedHunks; + } if (options.budgetBoost !== undefined) { cli.budgetBoost = parseBudgetBoost(options.budgetBoost); } diff --git a/src/config/config-loader.ts b/src/config/config-loader.ts index bb8d2de..53eda08 100644 --- a/src/config/config-loader.ts +++ b/src/config/config-loader.ts @@ -32,6 +32,7 @@ export type CliConfigOverrides = { model?: string; reasoning?: ReasoningLevel | "auto"; cacheEnabled?: boolean; + packRelatedHunks?: boolean; }; export type LoadConfigOptions = { @@ -64,6 +65,7 @@ const DEFAULT_SOURCE_PATHS = [ "review.maxTime", "review.perPassTimeoutMs", "review.budgetBoost", + "review.packRelatedHunks", "review.maxBudgetTokens", "github.summaryWhenNoFindings", "classification.pathRules", @@ -469,6 +471,10 @@ function applyCliOverrides( config.review.depth = cli.depth; sources["review.depth"] = "cli"; } + if (cli.packRelatedHunks !== undefined) { + config.review.packRelatedHunks = cli.packRelatedHunks; + sources["review.packRelatedHunks"] = "cli"; + } if (cli.budgetBoost !== undefined) { config.review.budgetBoost = cli.budgetBoost; sources["review.budgetBoost"] = "cli"; diff --git a/src/config/schema.ts b/src/config/schema.ts index d000099..82f5bbc 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -17,6 +17,12 @@ export const reviewMaxTimeMinutesSchema = positiveFiniteNumberSchema.max(MAX_REV // packets; beyond 3 the marginal recall of another draw is negligible while // cost keeps climbing. Hard cap, not a default. export const MAX_DEEP_ENSEMBLE_PASSES = 3; +// Plan 103 (experiment-only): upper bound for review.packMaxHunks. Must equal +// MAX_HUNKS_PER_PACKET in packet-builder.ts — duplicated rather than imported +// so the config schema stays free of pipeline dependencies; a focused test +// asserts the two agree. The recall curve varies packMaxHunks below this +// bound; nothing may raise packing above today's shipped cap. +export const MAX_PACK_HUNKS = 5; export const pathRuleSchema = z .object({ @@ -141,7 +147,10 @@ export const codegenieConfigSchema = z maxBudgetTokens: positiveIntSchema.optional(), maxModelCalls: positiveIntSchema.optional(), deepEnsemblePasses: positiveIntSchema.max(MAX_DEEP_ENSEMBLE_PASSES).optional(), - adaptiveSecondPass: z.boolean().optional() + adaptiveSecondPass: z.boolean().optional(), + packRelatedHunks: z.boolean(), + packMaxHunks: positiveIntSchema.max(MAX_PACK_HUNKS), + pinnedPlanPath: z.string().min(1).optional() }) .strict(), github: z @@ -216,7 +225,10 @@ export const defaultConfig: CodegenieConfig = { // soft-stop and went partial); 8M puts the soft-stop at 6.8M, ~15% above // the largest observed legitimate run. A protective ceiling, not a // target. - maxBudgetTokens: 8_000_000 + maxBudgetTokens: 8_000_000, + // Plan 103: dark until the packet-size recall curve clears its gate. + packRelatedHunks: false, + packMaxHunks: MAX_PACK_HUNKS }, github: { summaryWhenNoFindings: false diff --git a/src/evals/eval-runner.ts b/src/evals/eval-runner.ts index 8470145..c8e2933 100644 --- a/src/evals/eval-runner.ts +++ b/src/evals/eval-runner.ts @@ -6,6 +6,7 @@ import { z } from "zod"; import { applyRepoConfigLayer } from "../config/config-loader.js"; import { MAX_DEEP_ENSEMBLE_PASSES, + MAX_PACK_HUNKS, reasoningLevelSchema, reviewDepthSchema, reviewMaxTimeMinutesSchema, @@ -138,6 +139,11 @@ const caseSchema = z maxBudgetTokens: positiveIntSchema.optional(), deepEnsemblePasses: positiveIntSchema.max(MAX_DEEP_ENSEMBLE_PASSES).optional(), adaptiveSecondPass: z.boolean().optional(), + // Plan 103 (experiment-only): eval cases are the only surface that can + // set these. No codegenie.toml may. + packRelatedHunks: z.boolean().optional(), + packMaxHunks: positiveIntSchema.max(MAX_PACK_HUNKS).optional(), + pinnedPlanPath: z.string().min(1).optional(), verify: z.boolean().optional(), cache: z.boolean().optional(), cacheDir: z.string().min(1).optional(), @@ -825,6 +831,15 @@ function applyCaseReviewConfig( if (review?.adaptiveSecondPass !== undefined) { config.review.adaptiveSecondPass = review.adaptiveSecondPass; } + if (review?.packRelatedHunks !== undefined) { + config.review.packRelatedHunks = review.packRelatedHunks; + } + if (review?.packMaxHunks !== undefined) { + config.review.packMaxHunks = review.packMaxHunks; + } + if (review?.pinnedPlanPath !== undefined) { + config.review.pinnedPlanPath = review.pinnedPlanPath; + } if (review?.verify !== undefined) { config.review.verify = review.verify; } diff --git a/src/pipeline/packet-builder.ts b/src/pipeline/packet-builder.ts index f39b77f..6a871b4 100644 --- a/src/pipeline/packet-builder.ts +++ b/src/pipeline/packet-builder.ts @@ -59,11 +59,28 @@ type PlannedHunk = { type PacketGroup = { hunks: PlannedHunk[]; kind: ReviewPacket["kind"]; + // Plan 103: only `hunk-first` groups are eligible for compatible-atom + // packing. The direct whole-file and content-probed file-diff returns from + // groupHunks() carry their own file context and must bypass the packer. + origin: "hunk-first" | "direct"; fileContext?: ReviewPacket["fileContext"]; wholeFileText?: string; degradationReason?: string; }; +// Plan 103: a semantic atom is one group returned by hunkFirstGroups(). Atoms +// are indivisible — packing combines them but never splits or reorders one. +type PacketAtom = { + id: string; + group: PacketGroup; + hunks: PlannedHunk[]; + hunkCount: number; + patchChars: number; + sourcePos: number; + coverage: Exclude; + lensSignature: string; +}; + type HunkRelationshipSource = "same_symbol" | "symbol_mention" | "planner_hint"; type HunkRelationshipStrength = "strong" | "medium" | "weak"; @@ -140,6 +157,15 @@ const MAX_RELATED_CONTEXT_PATCH_CHARS = 1_500; const MAX_RELATIONSHIP_EDGES_PER_HUNK = 8; const MAX_RELATIONSHIP_SYMBOL_LOOKUPS = 20; const MAX_RELATIONSHIP_MENTION_RESULTS = 40; +// Plan 103 multi-member symbol context. A packed packet holding several +// distinct primary symbols must show every member's surrounding source, not +// just the top-ranked one. Symbol source is rendered at the head of the packet +// context, and truncateTail() keeps the head, so a symbol budget below +// MAX_CONTEXT_CHARS survives final rendering by construction — the reserve +// below is what guarantees outline/tests/hints cannot crowd it out. +const PACKET_SYMBOL_CONTEXT_BUDGET = 5_000; +const MIN_MEMBER_SYMBOL_CHARS = 800; +const MIN_SLICED_MEMBER_CHARS = 600; export async function buildReviewPackets( plan: ReviewPlan, @@ -190,15 +216,42 @@ export async function buildReviewPackets( } const allowWholeFileContext = includedPlanned.length === planned.length; - for (const group of await groupHunks(includedPlanned, repoIndex, telemetry, { allowWholeFileContext })) { - const first = group.hunks[0]; - if (!first) { + const groups = await groupHunks(includedPlanned, repoIndex, telemetry, { allowWholeFileContext }); + const positionByHunk = new Map(includedPlanned.map((entry, index) => [entry.hunk.id, index])); + const build = async ( + group: PacketGroup, + sink: TelemetryRecorder, + metrics: SymbolContextMetrics, + buildMetrics: PacketBuildMetrics, + packing?: { members: PlannedHunk[][]; profileFloor?: ReviewProfile } + ): Promise => { + const decisions = group.hunks + .map((entry) => effectiveByHunk.get(entry.hunk.id)) + .filter((decision): decision is EffectiveDecision => decision !== undefined) + .filter(isNonSkipDecision); + return buildPacket( + group.hunks, + decisions, + group, + packing === undefined ? relationshipGraph : scratchRelationshipGraph(relationshipGraph), + repoIndex, + opts.config, + sink, + opts.reviewContext, + metrics, + buildMetrics, + packing + ); + }; + + for (const candidate of packRelatedHunks(groups, effectiveByHunk, positionByHunk, opts.config)) { + if (candidate.atoms.length <= 1) { + const built = await build(candidate.group, telemetry, symbolContextMetrics, packetBuildMetrics); + packets.push(built.packet); continue; } - const groupDecisions = group.hunks.map((entry) => effectiveByHunk.get(entry.hunk.id)).filter((decision): decision is EffectiveDecision => decision !== undefined); - const includedDecisions = groupDecisions.filter(isNonSkipDecision); - const packet = await buildPacket(group.hunks, includedDecisions, group, relationshipGraph, repoIndex, opts.config, telemetry, opts.reviewContext, symbolContextMetrics, packetBuildMetrics); - packets.push(packet); + const committed = await commitPackedCandidate(candidate, build, telemetry, symbolContextMetrics, packetBuildMetrics, opts.config); + packets.push(...committed); } } @@ -248,6 +301,7 @@ async function groupHunks( return [{ hunks: planned, kind: "whole-file", + origin: "direct", fileContext: { mode: "whole-file", reason: wholeFileReason }, wholeFileText: content.text }]; @@ -255,6 +309,7 @@ async function groupHunks( return [{ hunks: planned, kind: "file-diff", + origin: "direct", fileContext: { mode: "file-diff", reason: content.reason } }]; } @@ -281,8 +336,9 @@ async function buildPacket( telemetry: TelemetryRecorder, reviewContext: PacketReviewContext | undefined, symbolContextMetrics: SymbolContextMetrics, - packetBuildMetrics: PacketBuildMetrics -): Promise { + packetBuildMetrics: PacketBuildMetrics, + packing: { members: PlannedHunk[][]; profileFloor?: ReviewProfile } | undefined = undefined +): Promise { const first = planned[0]; if (!first) { throw new Error("cannot build empty packet"); @@ -306,6 +362,7 @@ async function buildPacket( const attentionNoteSelection = mergeAttentionNotes(plannerAttentionNotes, relatedChangedContext); const attentionNotes = attentionNoteSelection.notes; emitRelationshipAttentionTelemetry(telemetry, first.file.path, attentionNoteSelection); + const memberSymbolFacts = (packing?.members ?? [planned]).map((member) => member.flatMap((entry) => entry.symbolFacts)); const context = await buildContext(repoIndex, first.file, planned.map((entry) => entry.hunk), symbolFacts, telemetry, { coverage, reviewPriority, @@ -314,7 +371,7 @@ async function buildPacket( lenses: decisions.flatMap((decision) => decision.lenses), attentionNotes, labels: first.facts.labels - }, symbolContextMetrics); + }, symbolContextMetrics, memberSymbolFacts); const hintContext = await resolvePacketContextHints(repoIndex, first.file, decisions.flatMap((decision) => decision.surroundingContextHints), telemetry); const truncationReason = truncationReasons(packetHunks).join("; "); const auxiliaryContextText = [context.text, hintContext.text].filter((text) => text.trim().length > 0).join("\n\n"); @@ -382,6 +439,10 @@ async function buildPacket( } }); } + // Plan 103: the floor is applied before routing, because routedPacketLenses() + // prunes core/code-review on a `simple` profile — raising the profile after + // routing would leave a lens dropped that the floor was meant to preserve. + const effectiveReviewProfile = maxReviewProfile([reviewProfile, ...(packing?.profileFloor !== undefined ? [packing.profileFloor] : [])]); const lenses = routedPacketLenses({ lenses: decisions.flatMap((decision) => decision.lenses), language: first.facts.language, @@ -392,7 +453,7 @@ async function buildPacket( attentionNotes, coverage, reviewPriority, - reviewProfile, + reviewProfile: effectiveReviewProfile, telemetry }); emitPacketContextQuality(telemetry, first.file.path, coverage, reviewPriority, contextQuality, contextDegradationReasons); @@ -408,7 +469,7 @@ async function buildPacket( language: first.facts.language, reviewPriority, coverage, - reviewProfile, + reviewProfile: effectiveReviewProfile, lenses, hunks: packetHunks, symbolFacts, @@ -423,7 +484,7 @@ async function buildPacket( labels: first.facts.labels, attentionNotes, relatedChangedContext, - toolBudget: scaleToolBudget(toolBudget(coverage, config.review.depth, reviewProfile), config.review.budgetBoost), + toolBudget: scaleToolBudget(toolBudget(coverage, config.review.depth, effectiveReviewProfile), config.review.budgetBoost), ...(reviewContext?.intentText !== undefined ? { intentText: reviewContext.intentText } : {}), ...(reviewContext?.intentSignals !== undefined ? { intentSignals: reviewContext.intentSignals } : {}), ...(context.degradation !== undefined || truncationReason.length > 0 || contextDropReason !== undefined || contextTruncationReason !== undefined || group.degradationReason !== undefined @@ -435,7 +496,44 @@ async function buildPacket( ? { fileContext: { mode: "file-diff", reason: "grouped file hunks" } } : {}) }; - return packet; + // Rule 7: a member counts only for the source that survives final rendering. + const survivingMemberChars = context.memberChars.map((member) => + member.header.length === 0 || renderedContext.text.includes(member.header) + ? member + : { ...member, chars: 0, complete: false } + ); + return { + packet, + routedLenses: lenses, + attentionNotes, + reviewProfile: effectiveReviewProfile, + derivedReviewProfile: reviewProfile, + contextQuality, + memberChars: survivingMemberChars, + symbolBudgetOverSubscribed: context.symbolBudgetOverSubscribed + }; +} + +// Plan 103: what a candidate build has to expose so the packing pass can +// compare it against its members' standalone builds before committing. +type PacketBuildResult = { + packet: ReviewPacket; + routedLenses: string[]; + attentionNotes: string[]; + reviewProfile: ReviewProfile; + derivedReviewProfile: ReviewProfile; + contextQuality: PacketContextQuality; + memberChars: Array<{ identity: string; header: string; chars: number; complete: boolean }>; + symbolBudgetOverSubscribed: boolean; +}; + +const REVIEW_PROFILE_RANK: Record = { simple: 0, standard: 1, investigate: 2 }; + +function maxReviewProfile(profiles: ReviewProfile[]): ReviewProfile { + return profiles.reduce( + (best, profile) => (REVIEW_PROFILE_RANK[profile] > REVIEW_PROFILE_RANK[best] ? profile : best), + "simple" + ); } const DOCS_CONFIG_EXTENSIONS = new Set([".md", ".yml", ".yaml", ".toml", ".conf", ".sample", ".txt"]); @@ -1274,7 +1372,252 @@ function packetGroup(hunks: PlannedHunk[], degradationReason?: string): PacketGr : hunks.length > 1 ? "coalesced-hunks" : "hunk"; - return { hunks, kind, ...(degradationReason !== undefined ? { degradationReason } : {}) }; + return { hunks, kind, origin: "hunk-first", ...(degradationReason !== undefined ? { degradationReason } : {}) }; +} + +// Plan 103: Plan 102's compatibility predicate. Atoms combine only inside one +// file when they share the planner's effective coverage and its requested lens +// set, so packing introduces no coverage promotion and cannot silently reroute +// a hunk to different expertise. Returns the original groups untouched when +// packing is off or when any group bypassed hunkFirstGroups(). +function packRelatedHunks( + groups: PacketGroup[], + effectiveByHunk: Map, + positionByHunk: Map, + config: CodegenieConfig +): PackCandidate[] { + if (!config.review.packRelatedHunks || !groups.every((group) => group.origin === "hunk-first")) { + return groups.map((group) => ({ group, atoms: [] })); + } + + const partitions = new Map(); + for (const group of groups) { + const atom = packetAtom(group, effectiveByHunk, positionByHunk); + const key = `${atom.coverage}\u0000${atom.lensSignature}`; + partitions.set(key, [...(partitions.get(key) ?? []), atom]); + } + + const packed: PacketAtom[][] = []; + for (const partition of partitions.values()) { + let current: PacketAtom[] = []; + let hunkCount = 0; + let patchChars = 0; + for (const atom of partition) { + const exceedsCaps = + hunkCount + atom.hunkCount > config.review.packMaxHunks || + patchChars + atom.patchChars > MAX_PATCH_CHARS; + // An atom that alone exceeds a cap still becomes its own packet: atoms + // are indivisible, so a cap can never split one. + if (current.length > 0 && exceedsCaps) { + packed.push(current); + current = []; + hunkCount = 0; + patchChars = 0; + } + current.push(atom); + hunkCount += atom.hunkCount; + patchChars += atom.patchChars; + } + if (current.length > 0) { + packed.push(current); + } + } + + return packed + .sort((a, b) => (a[0]?.sourcePos ?? 0) - (b[0]?.sourcePos ?? 0)) + .map((atoms) => ({ group: combinePacketAtoms(atoms), atoms })); +} + +type PackCandidate = { group: PacketGroup; atoms: PacketAtom[] }; + +// Plan 103: a candidate is dry-built against an isolated relationship +// accumulator and a suppressed telemetry sink. Nothing reaches the real graph, +// artifacts, or event stream until the candidate passes every check. +function scratchRelationshipGraph(graph: HunkRelationshipGraph): HunkRelationshipGraph { + return { ...graph, relatedContextAttached: [], relatedContextOmitted: [] }; +} + +const QUIET_TELEMETRY = { event: () => undefined } as unknown as TelemetryRecorder; + +const CONTEXT_QUALITY_RANK: Record = { + path_only: 0, + outline_only: 1, + sliced: 2, + full: 3 +}; + +type BuildFn = ( + group: PacketGroup, + sink: TelemetryRecorder, + metrics: SymbolContextMetrics, + buildMetrics: PacketBuildMetrics, + packing?: { members: PlannedHunk[][]; profileFloor?: ReviewProfile } +) => Promise; + +async function commitPackedCandidate( + candidate: PackCandidate, + build: BuildFn, + telemetry: TelemetryRecorder, + symbolContextMetrics: SymbolContextMetrics, + packetBuildMetrics: PacketBuildMetrics, + config: CodegenieConfig +): Promise { + const scratchMetrics = emptySymbolContextMetrics(); + const scratchBuildMetrics: PacketBuildMetrics = { + relatedContextBudgetNudges: 0, + relatedContextBudgetNudgeSources: new Set() + }; + + const standalone = []; + for (const atom of candidate.atoms) { + standalone.push(await build(atom.group, QUIET_TELEMETRY, scratchMetrics, scratchBuildMetrics, { members: [atom.hunks] })); + } + + const members = candidate.atoms.map((atom) => atom.hunks); + const profileFloor = maxReviewProfile(standalone.map((result) => result.reviewProfile)); + const packed = await build(candidate.group, QUIET_TELEMETRY, scratchMetrics, scratchBuildMetrics, { members, profileFloor }); + const rejection = packedCandidateRejection(packed, standalone, candidate); + + if (rejection === undefined) { + const committed = await build(candidate.group, telemetry, symbolContextMetrics, packetBuildMetrics, { members, profileFloor }); + // Artifact-only provenance: without this a multi-hunk packet is + // indistinguishable from one today's grouper produced on its own, so a + // packing A/B cannot tell treated packets from untreated ones. Kept out of + // the reviewer prompt and out of the packet ID. + const patchChars = candidate.atoms.reduce((sum, atom) => sum + atom.patchChars, 0); + telemetry.event({ + stage: 6, + level: "info", + message: "same_file_atoms_packed", + packetId: committed.packet.id, + file: committed.packet.path, + data: { + sourceAtomIds: candidate.atoms.map((atom) => atom.id), + sourceAtomCount: candidate.atoms.length, + sourceAtomHunkCounts: candidate.atoms.map((atom) => atom.hunkCount), + hunkCount: committed.packet.hunks.length, + hunkIds: committed.packet.hunks.map((hunk) => hunk.hunkId), + coverage: committed.packet.coverage, + lensSignature: candidate.atoms[0]?.lensSignature ?? "", + routedLenses: committed.routedLenses, + standaloneProfiles: standalone.map((member) => member.reviewProfile), + derivedProfile: committed.derivedReviewProfile, + effectiveProfile: committed.reviewProfile, + profileFloorApplied: + REVIEW_PROFILE_RANK[committed.reviewProfile] > REVIEW_PROFILE_RANK[committed.derivedReviewProfile], + capUsage: { + hunks: committed.packet.hunks.length, + maxHunks: config.review.packMaxHunks, + patchChars, + maxPatchChars: MAX_PATCH_CHARS + } + } + }); + return [committed.packet]; + } + + telemetry.event({ + stage: 6, + level: "info", + message: "packed_candidate_abandoned", + file: candidate.group.hunks[0]?.file.path ?? "", + data: { + reason: rejection, + atomIds: candidate.atoms.map((atom) => atom.id), + hunkIds: candidate.group.hunks.map((entry) => entry.hunk.id) + } + }); + + const separate: ReviewPacket[] = []; + for (const atom of candidate.atoms) { + const built = await build(atom.group, telemetry, symbolContextMetrics, packetBuildMetrics); + separate.push(built.packet); + } + return separate; +} + +// The abandonment contract: a packed packet may never lose a lens a member +// routed alone, never drop a high-priority planner focus note, never leave a +// symbol-bearing member without usable source, and never fall below the +// maximum standalone member profile. +function packedCandidateRejection( + packed: PacketBuildResult, + standalone: PacketBuildResult[], + candidate: PackCandidate +): string | undefined { + const routed = new Set(packed.routedLenses); + if (standalone.some((member) => member.routedLenses.some((lens) => !routed.has(lens)))) { + return "routed_lens_lost"; + } + + const priority = candidate.group.hunks[0]?.facts.reviewPriority; + if (priority === "high" || priority === "critical") { + const notes = new Set(packed.attentionNotes); + if (standalone.some((member) => member.attentionNotes.some((note) => !notes.has(note)))) { + return "high_priority_focus_note_lost"; + } + } + + if (packed.symbolBudgetOverSubscribed) { + return "member_symbol_budget_oversubscribed"; + } + // A short symbol is fully represented even below the floor; only a member + // whose source was cut below the usable minimum has actually lost context. + if (packed.memberChars.some((member) => !member.complete && member.chars < MIN_SLICED_MEMBER_CHARS)) { + return "member_symbol_context_collapsed"; + } + + const worstStandalone = Math.min(...standalone.map((member) => CONTEXT_QUALITY_RANK[member.contextQuality])); + if (CONTEXT_QUALITY_RANK[packed.contextQuality] < worstStandalone) { + return "member_context_quality_degraded"; + } + + if (REVIEW_PROFILE_RANK[packed.reviewProfile] < REVIEW_PROFILE_RANK[maxReviewProfile(standalone.map((member) => member.reviewProfile))]) { + return "effective_profile_downgraded"; + } + return undefined; +} + +function packetAtom( + group: PacketGroup, + effectiveByHunk: Map, + positionByHunk: Map +): PacketAtom { + const hunkIds = group.hunks.map((entry) => entry.hunk.id); + const decisions = group.hunks + .flatMap((entry) => effectiveByHunk.get(entry.hunk.id) ?? []) + .filter(isNonSkipDecision); + return { + id: sha256Hex(hunkIds.join("\n")), + group, + hunks: group.hunks, + hunkCount: group.hunks.length, + patchChars: combinedPatchChars(group.hunks), + sourcePos: Math.min(...hunkIds.map((id) => positionByHunk.get(id) ?? Number.MAX_SAFE_INTEGER)), + coverage: maxCoverage(decisions.map((decision) => decision.coverage)), + lensSignature: normalizedLensSignature(decisions) + }; +} + +// The compatibility key: the stable serialization of the sorted, deduplicated +// union of the atom's planner-requested lenses. +function normalizedLensSignature(decisions: NonSkipDecision[]): string { + return cleanStrings(decisions.flatMap((decision) => decision.lenses)).join(","); +} + +function combinePacketAtoms(atoms: PacketAtom[]): PacketGroup { + const first = atoms[0]; + if (first === undefined) { + throw new Error("cannot combine zero atoms"); + } + if (atoms.length === 1) { + return first.group; + } + const reasons = cleanStrings(atoms.flatMap((atom) => atom.group.degradationReason ?? [])); + return packetGroup( + atoms.flatMap((atom) => atom.hunks), + reasons.length > 0 ? reasons.join("; ") : undefined + ); } function packetKind(group: PacketGroup, planned: PlannedHunk[], file: DiffFile): ReviewPacket["kind"] { @@ -1582,6 +1925,8 @@ type PacketContextBuildResult = { contextQuality: PacketContextQuality; contextDegradationReasons: string[]; packetSymbols: SymbolInfo[]; + memberChars: Array<{ identity: string; header: string; chars: number; complete: boolean }>; + symbolBudgetOverSubscribed: boolean; degradation?: string; }; @@ -1658,7 +2003,8 @@ async function buildContext( symbolFacts: HunkSymbolFacts[], telemetry: TelemetryRecorder, symbolContextInput: PacketSymbolContextInput, - symbolContextMetrics: SymbolContextMetrics + symbolContextMetrics: SymbolContextMetrics, + memberSymbolFacts: HunkSymbolFacts[][] = [symbolFacts] ): Promise { if (!isToolsHost(repoIndex.tools)) { symbolContextMetrics.outlineOnly += 1; @@ -1668,12 +2014,14 @@ async function buildContext( relevantTests: [], contextQuality: "path_only", contextDegradationReasons: ["repository tools do not provide packet context"], - packetSymbols: [] + packetSymbols: [], + memberChars: [], + symbolBudgetOverSubscribed: false }; } try { const result = await repoIndex.tools.buildPacketContext(file, hunks, symbolFacts); - const symbolSource = await readEnclosingSymbolSource(repoIndex, file, symbolFacts, telemetry, symbolContextInput, symbolContextMetrics); + const symbolSource = await readMemberSymbolSources(repoIndex, file, memberSymbolFacts, telemetry, symbolContextInput, symbolContextMetrics); const contextText = renderContext(result, symbolSource.text); const reasons = [ ...(result.degradation !== undefined ? [result.degradation] : []), @@ -1688,6 +2036,8 @@ async function buildContext( contextQuality: contextQualityFor(result, symbolSource, contextText), contextDegradationReasons: reasons, packetSymbols: result.packetSymbols ?? (result.primarySymbol !== undefined ? [result.primarySymbol] : []), + memberChars: symbolSource.memberChars, + symbolBudgetOverSubscribed: symbolSource.overSubscribed, ...(degradation.length > 0 ? { degradation } : {}) }; } catch (error) { @@ -1701,11 +2051,160 @@ async function buildContext( contextQuality: "path_only", contextDegradationReasons: [message], packetSymbols: [], + memberChars: [], + symbolBudgetOverSubscribed: false, degradation: message }; } } +type MemberSymbolSource = SymbolSourceContext & { + memberChars: Array<{ identity: string; header: string; chars: number; complete: boolean }>; + overSubscribed: boolean; +}; + +// Reads enclosing-symbol source for every distinct primary symbol in a packed +// packet. A single-symbol packet delegates to the unchanged single-symbol path, +// so flag-off behaviour and artifacts are untouched. +async function readMemberSymbolSources( + repoIndex: RepositoryIndex, + file: DiffFile, + memberSymbolFacts: HunkSymbolFacts[][], + telemetry: TelemetryRecorder, + symbolContextInput: PacketSymbolContextInput, + symbolContextMetrics: SymbolContextMetrics +): Promise { + const participants: Array<{ identity: string; fact: HunkSymbolFacts }> = []; + const seen = new Set(); + for (const facts of memberSymbolFacts) { + const fact = primarySymbolFactWithMergedChanges(facts); + if (fact === undefined || symbolSourceSelector(fact) === undefined) { + continue; + } + const identity = symbolFactIdentity(fact); + if (seen.has(identity)) { + continue; + } + seen.add(identity); + participants.push({ identity, fact }); + } + + if (participants.length <= 1) { + const single = await readEnclosingSymbolSource( + repoIndex, + file, + memberSymbolFacts.flat(), + telemetry, + symbolContextInput, + symbolContextMetrics + ); + return { + ...single, + memberChars: participants.length === 1 && participants[0] !== undefined + ? [{ identity: participants[0].identity, header: "", chars: single.text.length, complete: true }] + : [], + overSubscribed: false + }; + } + + // Rule 4: a packet that cannot give every symbol-bearing member its floor + // cannot represent its members, so the candidate is abandoned upstream. + if (participants.length * MIN_MEMBER_SYMBOL_CHARS > PACKET_SYMBOL_CONTEXT_BUDGET) { + return { text: "", reasons: ["member_symbol_budget_oversubscribed"], memberChars: [], overSubscribed: true }; + } + + const blocks: Array<{ identity: string; header: string; text: string; wanted: number }> = []; + const reasons: string[] = []; + let anySliced = false; + for (const participant of participants) { + const selector = symbolSourceSelector(participant.fact); + if (selector === undefined) { + continue; + } + const source = participant.fact.changedLinesSide === "old" ? { kind: "base" as const } : { kind: "head" as const }; + const readPath = participant.fact.changedLinesSide === "old" ? file.oldPath ?? file.path : file.path; + try { + const result = await withRepositoryToolCallContext( + repoIndex.tools, + { stage: 6, initiator: "harness" }, + () => repoIndex.tools.readSymbol(readPath, selector, source) + ); + if (result.text === undefined || result.text.trim().length === 0) { + reasons.push(`member_symbol_source_empty: ${participant.identity}`); + continue; + } + const label = result.symbol?.name ?? participant.fact.enclosingSymbol ?? `line ${String(selector.line ?? "")}`.trim(); + const rendered = renderFullSymbolContext(readPath, label, participant.fact, result.text); + blocks.push({ identity: participant.identity, header: `Primary symbol: ${readPath}:${label}`, text: rendered, wanted: rendered.length }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + telemetry.event({ + stage: 6, + level: "warn", + message: "packet_member_symbol_source_unavailable", + file: file.path, + data: { identity: participant.identity, path: readPath, error: message } + }); + reasons.push(`member_symbol_source_unavailable: ${participant.identity}`); + } + } + + if (blocks.length === 0) { + return { text: "", reasons: reasons.length > 0 ? reasons : ["no_primary_symbol"], memberChars: [], overSubscribed: false }; + } + + // Rules 3 and 5: equal shares, then one redistribution pass in source order + // handing surplus from members that need less to members that need more. + const share = Math.floor(PACKET_SYMBOL_CONTEXT_BUDGET / blocks.length); + const allowances = blocks.map((block) => Math.min(block.wanted, share)); + let surplus = allowances.reduce((sum, allowance) => sum + (share - allowance), 0); + for (const [index, block] of blocks.entries()) { + if (surplus <= 0) { + break; + } + const current = allowances[index] ?? 0; + if (block.wanted <= current) { + continue; + } + const extra = Math.min(surplus, block.wanted - current); + allowances[index] = current + extra; + surplus -= extra; + } + + const emitted = blocks.map((block, index) => { + const allowance = Math.max(allowances[index] ?? 0, 0); + if (block.wanted <= allowance) { + return { ...block, complete: true }; + } + anySliced = true; + return { ...block, text: truncateToBudget(block.text, allowance), complete: false }; + }); + + symbolContextMetrics.materialOmission += anySliced ? 1 : 0; + telemetry.event({ + stage: 6, + level: "debug", + message: "packet_member_symbol_context", + file: file.path, + data: { + members: emitted.length, + share, + budget: PACKET_SYMBOL_CONTEXT_BUDGET, + emittedChars: emitted.map((block) => block.text.length), + sliced: anySliced + } + }); + + return { + text: emitted.map((block) => block.text).join("\n\n"), + quality: anySliced ? "sliced" : "full", + reasons, + ...(anySliced ? { degradation: "member symbol source sliced to share packet context budget" } : {}), + memberChars: emitted.map((block) => ({ identity: block.identity, header: block.header, chars: block.text.length, complete: block.complete })), + overSubscribed: false + }; +} + async function readEnclosingSymbolSource( repoIndex: RepositoryIndex, file: DiffFile, diff --git a/src/pipeline/pinned-plan.ts b/src/pipeline/pinned-plan.ts new file mode 100644 index 0000000..8dc6dcb --- /dev/null +++ b/src/pipeline/pinned-plan.ts @@ -0,0 +1,112 @@ +// Plan 103 (experiment-only, removed at teardown): an eval-only seam that +// replays one recorded Stage-5 plan across several arms so packet size is the +// only difference between them. +// +// ReviewPlan carries no base/head identity and no self-describing hash, so a +// bare plan file cannot prove it belongs to the diff under review. The pinned +// artifact is an explicit versioned wrapper that can. +import { readFileSync } from "node:fs"; +import { z } from "zod"; +import { sha256Hex } from "../util/hashing.js"; +import { CodegenieError } from "../util/errors.js"; +import type { ReviewPlan, UnifiedDiff } from "../types.js"; + +export const PINNED_PLAN_SCHEMA_VERSION = 1; + +const pinnedPlanArtifactSchema = z + .object({ + schemaVersion: z.literal(PINNED_PLAN_SCHEMA_VERSION), + baseSha: z.string().min(1), + headSha: z.string().min(1), + planSha256: z.string().length(64), + plan: z.looseObject({ coverage: z.array(z.looseObject({ hunkId: z.string().min(1) })) }) + }) + .strict(); + +export type PinnedPlanArtifact = { + schemaVersion: typeof PINNED_PLAN_SCHEMA_VERSION; + baseSha: string; + headSha: string; + planSha256: string; + plan: ReviewPlan; +}; + +// Canonical form: keys sorted recursively, no whitespace, over `plan` alone — +// so the hash is independent of field order and of the wrapper's own fields. +export function canonicalPlanJson(plan: unknown): string { + const canonical = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.map(canonical); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.entries(value as Record) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, entry]) => [key, canonical(entry)]) + ); + } + return value; + }; + return JSON.stringify(canonical(plan)); +} + +export function planSha256(plan: unknown): string { + return sha256Hex(canonicalPlanJson(plan)); +} + +export function buildPinnedPlanArtifact(input: { baseSha: string; headSha: string; plan: ReviewPlan }): PinnedPlanArtifact { + return { + schemaVersion: PINNED_PLAN_SCHEMA_VERSION, + baseSha: input.baseSha, + headSha: input.headSha, + planSha256: planSha256(input.plan), + plan: input.plan + }; +} + +function reject(reason: string, context: Record): never { + throw new CodegenieError("config_error", `pinned plan rejected: ${reason}`, { context }); +} + +// Fails closed on every mismatch. A hash match over a plan that no longer +// parses, or that targets a different diff, is not sufficient. +export function loadPinnedPlan( + filePath: string, + expected: { baseSha?: string; headSha?: string; diff?: UnifiedDiff } +): ReviewPlan { + let raw: unknown; + try { + raw = JSON.parse(readFileSync(filePath, "utf8")); + } catch (cause) { + reject("artifact is not readable JSON", { path: filePath, cause: cause instanceof Error ? cause.message : "unknown" }); + } + + const parsed = pinnedPlanArtifactSchema.safeParse(raw); + if (!parsed.success) { + reject("artifact does not match the pinned plan schema", { path: filePath, issues: parsed.error.issues.length }); + } + const artifact = parsed.data as unknown as PinnedPlanArtifact; + + const recomputed = planSha256(artifact.plan); + if (recomputed !== artifact.planSha256) { + reject("planSha256 does not match the canonical hash of the plan", { path: filePath, recorded: artifact.planSha256, recomputed }); + } + // Fixture-backed eval repos are materialized fresh per run, so their commit + // SHAs vary while content-derived hunk IDs do not. "*" records that the + // artifact is content-anchored rather than ref-anchored; hash, schema, and + // hunk-membership validation still apply in full. + if (artifact.baseSha !== "*" && expected.baseSha !== undefined && expected.baseSha !== artifact.baseSha) { + reject("baseSha does not match the resolved review target", { recorded: artifact.baseSha, resolved: expected.baseSha }); + } + if (artifact.headSha !== "*" && expected.headSha !== undefined && expected.headSha !== artifact.headSha) { + reject("headSha does not match the resolved review target", { recorded: artifact.headSha, resolved: expected.headSha }); + } + if (expected.diff !== undefined) { + const available = new Set(expected.diff.files.flatMap((file) => file.hunks.map((hunk) => hunk.id))); + const unknown = artifact.plan.coverage.filter((entry) => !available.has(entry.hunkId)).map((entry) => entry.hunkId); + if (unknown.length > 0) { + reject("plan references hunk ids absent from the current diff", { unknownHunkIds: unknown.slice(0, 5), unknownCount: unknown.length }); + } + } + return artifact.plan; +} diff --git a/src/pipeline/review-runner.ts b/src/pipeline/review-runner.ts index 38b07c8..7cbc277 100644 --- a/src/pipeline/review-runner.ts +++ b/src/pipeline/review-runner.ts @@ -48,6 +48,8 @@ import type { import { CodegenieError, errorExitCode, isCodegenieError } from "../util/errors.js"; import { buildPlannerDossier, runPlanner } from "./planner.js"; import { buildReviewPackets, packetReviewContextFromDossier } from "./packet-builder.js"; +import { loadPinnedPlan, planSha256 } from "./pinned-plan.js"; +import type { PlannerRunResult } from "./planner.js"; import { ensemblePassesForPacket, runLensPackets } from "./lens-runner.js"; import { aggregateAttentionEfficiency, buildAttentionRecords } from "./attention.js"; import { applyCoverageEscalations } from "./coverage-escalation.js"; @@ -202,12 +204,58 @@ export async function runReview( lenses: services.lenses, allFiles: diff.files }); - const plannerResult = await runPlanner(dossier, config, run.telemetry, { - runner: services.runner, - promptBuilder: services.promptBuilder, - lenses: services.lenses, - skills: services.skills - }); + // Plan 103 eval-only seam: a pinned plan replaces the Stage-5 draw so + // several arms can consume byte-identical planner output. Validation fails + // closed; there is no user-facing path that reaches this. + const pinnedPlanPath = config.review.pinnedPlanPath; + let plannerResult: PlannerRunResult; + if (pinnedPlanPath !== undefined) { + const baseSha = resolved.mergeBase ?? resolved.baseRef; + const pinnedPlan = loadPinnedPlan(pinnedPlanPath, { + ...(baseSha !== undefined ? { baseSha } : {}), + ...(resolved.headSha !== undefined ? { headSha: resolved.headSha } : {}), + diff + }); + // Stage 5 never runs, so nothing else writes review-plan.json. Without + // this the run cannot describe which plan produced its packets, and the + // artifacts stop being self-contained the moment the external file moves. + await run.telemetry.writeArtifact("review-plan.json", pinnedPlan); + await run.telemetry.writeArtifact("pinned-plan-source.json", { + path: pinnedPlanPath, + planSha256: planSha256(pinnedPlan), + coverageEntries: pinnedPlan.coverage.length, + ...(baseSha !== undefined ? { baseSha } : {}), + ...(resolved.headSha !== undefined ? { headSha: resolved.headSha } : {}) + }); + run.telemetry.event({ + stage: 5, + level: "info", + message: "planner_plan_pinned", + data: { + path: pinnedPlanPath, + planSha256: planSha256(pinnedPlan), + coverageEntries: pinnedPlan.coverage.length + } + }); + plannerResult = { + plan: pinnedPlan, + plannerCoverage: { + submittedEntries: pinnedPlan.coverage.length, + acceptedEntries: pinnedPlan.coverage.length, + acceptedUniqueHunks: new Set(pinnedPlan.coverage.map((entry) => entry.hunkId)).size, + rejectedUnknownHunk: 0 + }, + degradedPlanning: false, + chunked: false + }; + } else { + plannerResult = await runPlanner(dossier, config, run.telemetry, { + runner: services.runner, + promptBuilder: services.promptBuilder, + lenses: services.lenses, + skills: services.skills + }); + } throwIfHardAborted(run); const packets = applyCoverageEscalations( await buildReviewPackets(plannerResult.plan, kept, fileFacts, repoIndex, run.telemetry, { diff --git a/src/telemetry/run-artifacts.ts b/src/telemetry/run-artifacts.ts index 313f36e..0c6777d 100644 --- a/src/telemetry/run-artifacts.ts +++ b/src/telemetry/run-artifacts.ts @@ -63,6 +63,7 @@ export const ARTIFACT_LOCATION = { "planner-dossier.json": "stages/05-planner/planner-dossier.json", "planner-dossier-chunks.json": "stages/05-planner/planner-dossier-chunks.json", "review-plan.json": "stages/05-planner/review-plan.json", + "pinned-plan-source.json": "stages/05-planner/pinned-plan-source.json", "hunk-relationships.json": "stages/06-packets/hunk-relationships.json", "system-review-raw-tasks.json": "stages/08-followups/system-review-raw-tasks.json", "system-review-tasks.json": "stages/08-followups/system-review-tasks.json", diff --git a/src/types.ts b/src/types.ts index 2666377..0c4f25b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,6 +54,16 @@ export type CodegenieConfig = { // evidence (concrete hint/uncertainty, silent-with-signal, low-confidence // only) earn one additional independent review pass. Off by default. adaptiveSecondPass?: boolean; + // Plan 103 (experiment-only, removed at teardown): pack compatible + // same-file atoms into fewer packets, and the hunk cap the recall curve + // varies. Deliberately absent from rawConfigSchema and repo-safe + // filtering — no codegenie.toml may set either, and neither is a + // supported user surface. Eval cases set them; nothing else can. + packRelatedHunks: boolean; + packMaxHunks: number; + // Plan 103 eval-only: replay one recorded Stage-5 plan across arms so + // packet size is the only difference between them. + pinnedPlanPath?: string; }; github: { summaryWhenNoFindings: boolean; @@ -1012,6 +1022,11 @@ export type EvalCase = { maxBudgetTokens?: number; deepEnsemblePasses?: number; adaptiveSecondPass?: boolean; + // Plan 103 (experiment-only): eval cases are the only surface that may set + // these; removed at teardown. + packRelatedHunks?: boolean; + packMaxHunks?: number; + pinnedPlanPath?: string; verify?: boolean; cache?: boolean; cacheDir?: string; diff --git a/tests/config-loader.test.ts b/tests/config-loader.test.ts index 89e20b6..741f520 100644 --- a/tests/config-loader.test.ts +++ b/tests/config-loader.test.ts @@ -5,9 +5,13 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { MAX_DEEP_ENSEMBLE_PASSES, + MAX_PACK_HUNKS, MAX_REVIEW_TIME_MINUTES, + codegenieConfigSchema, + defaultConfig, rawConfigSchema } from "../src/config/schema.js"; +import type { CodegenieConfig } from "../src/types.js"; import { ensureCodegenieHome, getCodegeniePaths } from "../src/config/paths.js"; import { loadConfig } from "../src/config/config-loader.js"; import { loadProviderSettings, saveProviderSettings } from "../src/provider/provider-settings.js"; @@ -325,3 +329,59 @@ describe("deepEnsemblePasses cap (plan 84)", () => { function tempDir(): string { return mkdtempSync(path.join(tmpdir(), "codegenie-")); } + +describe("plan 103 packing settings", () => { + it("defaults to dark packing at the shipped cap", () => { + const loaded = loadConfig({ repoRoot: tempDir(), homeOverride: tempDir() }); + expect(loaded.config.review.packRelatedHunks).toBe(false); + expect(loaded.config.review.packMaxHunks).toBe(MAX_PACK_HUNKS); + expect(MAX_PACK_HUNKS).toBe(5); + }); + + it("refuses both settings from every config file surface", () => { + // Plan 103 keeps these eval-only: no codegenie.toml and no user config may + // reach them, so strict parsing must reject rather than silently filter. + expect(rawConfigSchema.safeParse({ review: { packRelatedHunks: true } }).success).toBe(false); + expect(rawConfigSchema.safeParse({ review: { packMaxHunks: 3 } }).success).toBe(false); + + const repoRoot = tempDir(); + const home = tempDir(); + writeFileSync(path.join(repoRoot, "codegenie.toml"), "[review]\npackRelatedHunks = true\n"); + expect(() => loadConfig({ repoRoot, homeOverride: home })).toThrow(/invalid config file/); + + const userHome = tempDir(); + writeFileSync(path.join(userHome, "config.toml"), "[review]\npackMaxHunks = 2\n"); + expect(() => loadConfig({ repoRoot: tempDir(), homeOverride: userHome })).toThrow(/invalid config file/); + }); + + it("toggles packing per run from the CLI only", () => { + const repoRoot = tempDir(); + const home = tempDir(); + expect(loadConfig({ repoRoot, homeOverride: home }).config.review.packRelatedHunks).toBe(false); + + const on = loadConfig({ repoRoot, homeOverride: home, cli: { packRelatedHunks: true } }); + expect(on.config.review.packRelatedHunks).toBe(true); + expect(on.sources["review.packRelatedHunks"]).toBe("cli"); + + // A repository must not be able to enable experimental packing for + // everyone who reviews it; only an explicit per-run CLI flag can. + writeFileSync(path.join(repoRoot, "codegenie.toml"), "[review]\npackRelatedHunks = true\n"); + expect(() => loadConfig({ repoRoot, homeOverride: home })).toThrow(/invalid config file/); + + // Omitting the flag leaves the default in place; there is no --no- form + // because off is already the default. + const omitted = loadConfig({ repoRoot: tempDir(), homeOverride: tempDir(), cli: {} }); + expect(omitted.config.review.packRelatedHunks).toBe(false); + expect(omitted.sources["review.packRelatedHunks"]).toBe("defaults"); + }); + + it("bounds packMaxHunks by the shipped packet cap in the resolved schema", () => { + const base = structuredClone(defaultConfig) as CodegenieConfig; + for (const value of [1, 3, MAX_PACK_HUNKS]) { + expect(codegenieConfigSchema.safeParse({ ...base, review: { ...base.review, packMaxHunks: value } }).success).toBe(true); + } + for (const value of [0, -1, MAX_PACK_HUNKS + 1]) { + expect(codegenieConfigSchema.safeParse({ ...base, review: { ...base.review, packMaxHunks: value } }).success).toBe(false); + } + }); +}); diff --git a/tests/evals.test.ts b/tests/evals.test.ts index 64cdd97..6c47c6f 100644 --- a/tests/evals.test.ts +++ b/tests/evals.test.ts @@ -8,6 +8,7 @@ import { loadEvalArtifacts } from "../src/evals/eval-artifacts.js"; import { compareToPrevious, renderEvalCompareText } from "../src/evals/eval-compare.js"; import { executeEvalCommand, renderCaseResult, runEvalCommand } from "../src/evals/eval-command.js"; import { loadEvalSuite, replayFromArtifacts, runEvalCase } from "../src/evals/eval-runner.js"; +import { MAX_PACK_HUNKS } from "../src/config/schema.js"; import { aggregateRepeatScores, assignExpectations, matchExpectation, scoreEvalRun } from "../src/evals/eval-scoring.js"; import type { CandidateFinding, @@ -2295,3 +2296,36 @@ function findNestedGitDirs(root: string): string[] { } return found; } + +describe("plan 103 packing eval surface", () => { + it("applies both packing settings from an eval case and bounds the cap", async () => { + const suiteDir = mkdtempSync(path.join(tmpdir(), "codegenie-eval-packing-")); + writeFileSync(path.join(suiteDir, "packing.yml"), [ + "name: packing-case", + "repo:", + " fixture: repo", + "review:", + " packRelatedHunks: true", + " packMaxHunks: 3", + "should_find:", + " - id: expected", + " path: src/app.ts" + ].join("\n")); + + const suite = await loadEvalSuite(suiteDir); + expect(suite.cases[0]?.evalCase.review).toMatchObject({ packRelatedHunks: true, packMaxHunks: 3 }); + + const overCap = mkdtempSync(path.join(tmpdir(), "codegenie-eval-packing-cap-")); + writeFileSync(path.join(overCap, "packing.yml"), [ + "name: packing-over-cap", + "repo:", + " fixture: repo", + "review:", + ` packMaxHunks: ${MAX_PACK_HUNKS + 1}`, + "should_find:", + " - id: expected", + " path: src/app.ts" + ].join("\n")); + await expect(loadEvalSuite(overCap)).rejects.toMatchObject({ code: "config_error" }); + }); +}); diff --git a/tests/packet-packing-report.test.ts b/tests/packet-packing-report.test.ts new file mode 100644 index 0000000..a4fb39e --- /dev/null +++ b/tests/packet-packing-report.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; +import { comparePackets, dispatchOrder, fail, hunksWithinSlots } from "../scripts/packet-packing-report.js"; +import type { ReviewPacket } from "../src/types.js"; + +function packet(overrides: Partial & { id: string; hunkIds: string[] }): ReviewPacket { + const { hunkIds, id, ...rest } = overrides; + return { + id, + dispatchRank: [0, -hunkIds.length], + kind: hunkIds.length > 1 ? "coalesced-hunks" : "hunk", + prSummary: "", + path: "app.ts", + fileStatus: "modified", + isDeletedContent: false, + language: "typescript", + reviewPriority: "normal", + coverage: "normal", + reviewProfile: "standard", + lenses: ["core/code-review"], + hunks: hunkIds.map((hunkId) => ({ + hunkId, + oldStart: 1, + oldLines: 1, + newStart: 1, + newLines: 1, + header: "@@", + lines: [], + contentWithLineNumbers: "", + changedNewLineNumbers: [], + changedOldLineNumbers: [] + })), + symbolFacts: [], + context: { path: "app.ts" }, + contextText: "", + contextQuality: "full", + relevantTests: [], + surroundingContextHints: [], + labels: [], + attentionNotes: [], + relatedChangedContext: [], + toolBudget: { maxToolCalls: 4, maxInvestigationRounds: 2, maxResultChars: 10_000 }, + ...rest + } as ReviewPacket; +} + +describe("packet packing report", () => { + it("renders failures from typed fields without raw text", () => { + const failure = fail("lens_dropped", "run-1", { hunkId: "h1", lens: "core/tests" }); + expect(failure.message).toBe("lens_dropped: hunkId=h1 lens=core/tests"); + expect(failure.fields).toEqual({ hunkId: "h1", lens: "core/tests" }); + }); + + it("orders packets by the stage 7 scheduling tuple", () => { + const low = packet({ id: "low", hunkIds: ["h1"], reviewPriority: "low" }); + const deep = packet({ id: "deep", hunkIds: ["h2"], coverage: "deep" }); + const normal = packet({ id: "normal", hunkIds: ["h3"] }); + expect(dispatchOrder([low, normal, deep]).map((entry) => entry.id)).toEqual(["deep", "normal", "low"]); + }); + + it("counts distinct hunks within a fixed dispatch slot budget", () => { + const packets = [packet({ id: "a", hunkIds: ["h1", "h2"] }), packet({ id: "b", hunkIds: ["h3"] })]; + expect(hunksWithinSlots(packets, 1)).toBe(2); + expect(hunksWithinSlots(packets, 2)).toBe(3); + }); + + it("passes a clean pack and fails closed on every invariant violation", () => { + const off = [packet({ id: "a", hunkIds: ["h1"] }), packet({ id: "b", hunkIds: ["h2"] })]; + const packed = [packet({ id: "ab", hunkIds: ["h1", "h2"] })]; + expect(comparePackets("run-1", off, packed, 56)).toEqual([]); + + const lost = [packet({ id: "ab", hunkIds: ["h1"] })]; + expect(comparePackets("run-1", off, lost, 56).map((entry) => entry.code)).toContain("hunk_not_unique"); + + const duplicated = [packet({ id: "ab", hunkIds: ["h1", "h1", "h2"] })]; + expect(comparePackets("run-1", off, duplicated, 56).map((entry) => entry.code)).toContain("hunk_not_unique"); + + const overCap = [packet({ id: "ab", hunkIds: ["h1", "h2", "h3", "h4", "h5", "h6"] })]; + expect(comparePackets("run-1", off, overCap, 56).map((entry) => entry.code)).toContain("cap_exceeded"); + + const deepOff = [packet({ id: "a", hunkIds: ["h1"], coverage: "deep" }), packet({ id: "b", hunkIds: ["h2"] })]; + expect(comparePackets("run-1", deepOff, packed, 56).map((entry) => entry.code)).toContain("coverage_changed"); + + const investigateOff = [ + packet({ id: "a", hunkIds: ["h1"], reviewProfile: "investigate" }), + packet({ id: "b", hunkIds: ["h2"] }) + ]; + expect(comparePackets("run-1", investigateOff, packed, 56).map((entry) => entry.code)).toContain("profile_downgraded"); + + const richBudgetOff = [ + packet({ id: "a", hunkIds: ["h1"], toolBudget: { maxToolCalls: 9, maxInvestigationRounds: 2, maxResultChars: 10_000 } }), + packet({ id: "b", hunkIds: ["h2"] }) + ]; + expect(comparePackets("run-1", richBudgetOff, packed, 56).map((entry) => entry.code)).toContain("budget_downgraded"); + + const extraLensOff = [ + packet({ id: "a", hunkIds: ["h1"], lenses: ["core/code-review", "core/tests"] }), + packet({ id: "b", hunkIds: ["h2"] }) + ]; + expect(comparePackets("run-1", extraLensOff, packed, 56).map((entry) => entry.code)).toContain("lens_dropped"); + }); +}); diff --git a/tests/pinned-plan.test.ts b/tests/pinned-plan.test.ts new file mode 100644 index 0000000..fbb8186 --- /dev/null +++ b/tests/pinned-plan.test.ts @@ -0,0 +1,95 @@ +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { buildPinnedPlanArtifact, canonicalPlanJson, loadPinnedPlan, planSha256 } from "../src/pipeline/pinned-plan.js"; +import type { ReviewPlan, UnifiedDiff } from "../src/types.js"; + +const plan = (): ReviewPlan => ({ + diffUnderstanding: { declaredIntent: "intent", inferredBehavior: "behavior" }, + coverage: [ + { hunkId: "h1", path: "app.go", coverage: "normal", lenses: ["lang/go"], surroundingContextHints: [], reason: "test" } + ] +}); + +const diff = (hunkIds: string[]): UnifiedDiff => ({ + files: [ + { + path: "app.go", + status: "modified", + language: "go", + hunks: hunkIds.map((id) => ({ + id, + hunkHash: id.repeat(64).slice(0, 64), + path: "app.go", + oldStart: 1, + oldLines: 1, + newStart: 1, + newLines: 1, + header: "@@", + lines: [] + })) + } + ] +}); + +function write(artifact: unknown): string { + const dir = mkdtempSync(path.join(tmpdir(), "pinned-plan-")); + const file = path.join(dir, "frozen.json"); + writeFileSync(file, JSON.stringify(artifact)); + return file; +} + +describe("pinned plan artifact", () => { + it("hashes the plan independently of key order and wrapper fields", () => { + const a = { b: 1, a: [{ y: 2, x: 1 }] }; + const b = { a: [{ x: 1, y: 2 }], b: 1 }; + expect(canonicalPlanJson(a)).toBe(canonicalPlanJson(b)); + expect(planSha256(a)).toBe(planSha256(b)); + }); + + it("round-trips a valid artifact against its diff", () => { + const file = write(buildPinnedPlanArtifact({ baseSha: "base1", headSha: "head1", plan: plan() })); + const loaded = loadPinnedPlan(file, { baseSha: "base1", headSha: "head1", diff: diff(["h1"]) }); + expect(loaded.coverage[0]?.hunkId).toBe("h1"); + }); + + it("fails closed on every mismatch", () => { + const good = buildPinnedPlanArtifact({ baseSha: "base1", headSha: "head1", plan: plan() }); + + expect(() => loadPinnedPlan(write({ ...good, schemaVersion: 2 }), {})).toThrow(/pinned plan schema/); + expect(() => loadPinnedPlan(write({ ...good, extra: true }), {})).toThrow(/pinned plan schema/); + expect(() => loadPinnedPlan(write({ ...good, plan: { nope: true } }), {})).toThrow(/pinned plan schema/); + expect(() => loadPinnedPlan(write({ ...good, planSha256: "0".repeat(64) }), {})).toThrow(/canonical hash/); + + // A hash that matches a mutated plan is still rejected against its diff and refs. + const mutated = buildPinnedPlanArtifact({ + baseSha: "base1", + headSha: "head1", + plan: { ...plan(), coverage: [{ ...plan().coverage[0]!, hunkId: "ghost" }] } + }); + expect(() => loadPinnedPlan(write(mutated), { diff: diff(["h1"]) })).toThrow(/hunk ids absent/); + + expect(() => loadPinnedPlan(write(good), { baseSha: "other" })).toThrow(/baseSha/); + expect(() => loadPinnedPlan(write(good), { headSha: "other" })).toThrow(/headSha/); + + const badPath = path.join(mkdtempSync(path.join(tmpdir(), "pinned-plan-")), "missing.json"); + expect(() => loadPinnedPlan(badPath, {})).toThrow(/readable JSON/); + }); +}); + +describe("pinned plan run artifacts", () => { + it("keeps the artifact path map and the seam in agreement", async () => { + // The seam writes both names; if either loses its stage mapping the run + // stops being self-describing, which is the failure this guards. + const runArtifacts = await import("../src/telemetry/run-artifacts.js"); + const source = readFileSync(new URL("../src/telemetry/run-artifacts.ts", import.meta.url), "utf8"); + expect(source).toContain('"review-plan.json": "stages/05-planner/review-plan.json"'); + expect(source).toContain('"pinned-plan-source.json": "stages/05-planner/pinned-plan-source.json"'); + expect(runArtifacts).toBeDefined(); + + const runner = readFileSync(new URL("../src/pipeline/review-runner.ts", import.meta.url), "utf8"); + expect(runner).toContain('writeArtifact("review-plan.json", pinnedPlan)'); + expect(runner).toContain('writeArtifact("pinned-plan-source.json"'); + }); +}); diff --git a/tests/pipeline-phase5.test.ts b/tests/pipeline-phase5.test.ts index 51e457e..6024aed 100644 --- a/tests/pipeline-phase5.test.ts +++ b/tests/pipeline-phase5.test.ts @@ -13680,3 +13680,356 @@ function languageProjectionSkill(id: string, languages: string[], marker: string sections: { checks: marker, falsePositives: marker } }; } + +describe("plan 103 compatible-atom packing", () => { + // Hunks spaced far beyond NEARBY_GAP_LINES with no shared enclosing symbol, + // so today's grouper yields exactly one atom per hunk. + function separatedFile(pathName: string, count: number, gap = 100): DiffFile { + return { + path: pathName, + status: "modified", + language: "typescript", + hunks: Array.from({ length: count }, (_, index) => { + const line = 1 + index * gap; + return { + id: `h${index + 1}`, + hunkHash: String(index + 1).repeat(64).slice(0, 64), + path: pathName, + oldStart: line, + oldLines: 1, + newStart: line, + newLines: 1, + header: `@@ -${line} +${line} @@`, + lines: [{ kind: "add" as const, content: `const v${index + 1} = ${index + 1};`, newLineNumber: line }] + }; + }) + }; + } + + function planFor(file: DiffFile, overrides: Record = {}): ReviewPlan { + return { + diffUnderstanding: { declaredIntent: "test intent", inferredBehavior: "test behavior" }, + coverage: file.hunks.map((hunk) => ({ + hunkId: hunk.id, + path: file.path, + coverage: (overrides[hunk.id]?.coverage ?? "normal") as "deep" | "normal" | "light", + lenses: overrides[hunk.id]?.lenses ?? ["core/code-review"], + surroundingContextHints: [], + reason: "test" + })) + }; + } + + async function pack(file: DiffFile, plan: ReviewPlan, review: Partial = {}) { + const base = config(); + return buildReviewPackets( + plan, + [file], + [fakeFacts(file.path, "per-hunk")], + fakeRepositoryIndex(fakeTools()), + nullTelemetry(), + { config: { ...base, review: { ...base.review, ...review } }, enabledLenses: ["core/code-review"] } + ); + } + + const hunkIdsOf = (packets: Awaited>) => packets.map((packet) => packet.hunks.map((hunk) => hunk.hunkId)); + + it("packs compatible same-file atoms into fewer packets", async () => { + const file = separatedFile("app.ts", 6); + const plan = planFor(file); + + const off = await pack(file, plan); + expect(off).toHaveLength(6); + + const on = await pack(file, plan, { packRelatedHunks: true }); + expect(hunkIdsOf(on)).toEqual([["h1", "h2", "h3", "h4", "h5"], ["h6"]]); + }); + + it("is a no-op at a one-hunk cap, matching flag-off artifacts exactly", async () => { + const file = separatedFile("app.ts", 5); + const plan = planFor(file); + const off = await pack(file, plan); + const capOne = await pack(file, plan, { packRelatedHunks: true, packMaxHunks: 1 }); + expect(JSON.stringify(capOne)).toBe(JSON.stringify(off)); + }); + + it("never packs across effective coverage or requested lens boundaries", async () => { + const file = separatedFile("app.ts", 4); + + const mixedCoverage = await pack(file, planFor(file, { h2: { coverage: "deep" } }), { packRelatedHunks: true }); + expect(hunkIdsOf(mixedCoverage)).toEqual([["h1", "h3", "h4"], ["h2"]]); + expect(mixedCoverage.map((packet) => packet.coverage).sort()).toEqual(["deep", "normal"]); + + const mixedLenses = await pack(file, planFor(file, { h3: { lenses: ["core/tests"] } }), { packRelatedHunks: true }); + expect(hunkIdsOf(mixedLenses)).toEqual([["h1", "h2", "h4"], ["h3"]]); + }); + + it("assigns every reviewable hunk exactly once, in source order", async () => { + const file = separatedFile("app.ts", 9); + const packets = await pack(file, planFor(file), { packRelatedHunks: true }); + const flattened = packets.flatMap((packet) => packet.hunks.map((hunk) => hunk.hunkId)); + expect(new Set(flattened).size).toBe(9); + for (const packet of packets) { + const starts = packet.hunks.map((hunk) => hunk.newStart); + expect([...starts].sort((a, b) => a - b)).toEqual(starts); + } + expect(packets[0]?.hunks[0]?.hunkId).toBe("h1"); + }); + + it("never splits an atom, even when the atom alone exceeds the cap", async () => { + // h1 and h2 sit within NEARBY_GAP_LINES, so the grouper makes them one atom. + const file = separatedFile("app.ts", 3, 10); + const packets = await pack(file, planFor(file), { packRelatedHunks: true, packMaxHunks: 1 }); + expect(hunkIdsOf(packets)).toEqual([["h1", "h2", "h3"]]); + }); + + it("leaves whole-file groups out of the packer", async () => { + const file = separatedFile("app.ts", 2); + const packets = await buildReviewPackets( + planFor(file), + [file], + [fakeFacts(file.path, "whole-file")], + fakeRepositoryIndex(fakeTools("export const value = 1;\n")), + nullTelemetry(), + { + config: { ...config(), review: { ...config().review, packRelatedHunks: true } }, + enabledLenses: ["core/code-review"] + } + ); + expect(packets).toHaveLength(1); + expect(packets[0]).toMatchObject({ kind: "whole-file", fileContext: { mode: "whole-file" } }); + }); + + it("recomputes dispatch rank from the combined packet's changed lines", async () => { + const file = separatedFile("app.ts", 5); + const packets = await pack(file, planFor(file), { packRelatedHunks: true }); + expect(packets).toHaveLength(1); + expect(packets[0]?.dispatchRank).toEqual(packetDispatchRank("app.ts", { testStatus: "source" }, 5)); + }); + + it("records packing provenance so treated packets are identifiable", async () => { + const file = separatedFile("app.ts", 6); + const plan = planFor(file); + const events: Array<{ message?: string; packetId?: string; data?: Record }> = []; + const recorder = { + event: (entry: unknown) => events.push(entry as { message?: string }), + writeArtifact: async () => undefined + } as unknown as ReturnType; + const base = config(); + const packets = await buildReviewPackets(plan, [file], [fakeFacts(file.path, "per-hunk")], fakeRepositoryIndex(fakeTools()), recorder, { + config: { ...base, review: { ...base.review, packRelatedHunks: true } }, + enabledLenses: ["core/code-review"] + }); + + const packed = events.filter((entry) => entry.message === "same_file_atoms_packed"); + // Six atoms at a five-hunk cap: one packed packet of five, one lone atom. + expect(packed).toHaveLength(1); + expect(packets).toHaveLength(2); + const data = packed[0]?.data as Record; + expect(data.sourceAtomCount).toBe(5); + expect(data.hunkCount).toBe(5); + expect(data.hunkIds).toEqual(["h1", "h2", "h3", "h4", "h5"]); + expect((data.sourceAtomIds as string[])).toHaveLength(5); + expect(packed[0]?.packetId).toBe(packets[0]?.id); + expect(data.capUsage).toMatchObject({ hunks: 5, maxHunks: 5 }); + expect(data.profileFloorApplied).toBe(false); + }); + + it("records no packing provenance when packing is off", async () => { + const file = separatedFile("app.ts", 6); + const events: Array<{ message?: string }> = []; + const recorder = { + event: (entry: unknown) => events.push(entry as { message?: string }), + writeArtifact: async () => undefined + } as unknown as ReturnType; + const base = config(); + await buildReviewPackets(planFor(file), [file], [fakeFacts(file.path, "per-hunk")], fakeRepositoryIndex(fakeTools()), recorder, { + config: { ...base, review: { ...base.review, packRelatedHunks: false } }, + enabledLenses: ["core/code-review"] + }); + expect(events.filter((entry) => entry.message === "same_file_atoms_packed")).toHaveLength(0); + }); + + it("parameterizes packet shape by packMaxHunks for the recall curve", async () => { + const file = separatedFile("app.ts", 15); + const plan = planFor(file); + for (const [cap, expected] of [[1, 15], [3, 5], [5, 3]] as const) { + const packets = await pack(file, plan, { packRelatedHunks: true, packMaxHunks: cap }); + expect(packets).toHaveLength(expected); + expect(packets.every((packet) => packet.hunks.length <= cap)).toBe(true); + } + }); +}); + +describe("plan 103 multi-member context and transactional packing", () => { + function symbolFactsFor( + pathName: string, + hunkIds: string[], + perHunkSymbol: (id: string) => string, + sameRange = false + ): HunkSymbolFacts[] { + return hunkIds.map((hunkId, index) => ({ + path: pathName, + hunkId, + enclosingSymbol: perHunkSymbol(hunkId), + symbolKind: "function", + // Identity is name plus range: same name at a different range is a + // different symbol, which is what keeps duplicate class names apart. + symbolRange: (sameRange ? [1, 40] : [1 + index * 100, 40 + index * 100]) as [number, number], + changedLines: [1 + index * 100], + changedLinesSide: "new", + source: "tree-sitter", + confidence: "syntactic" + })); + } + + function symbolTools(bodyChars: number) { + const meta = { backend: "text" as const, precision: "exact" as const, degraded: false }; + return { + ...fakeTools(), + readSymbol: async (pathName: string, selector: { symbolName?: string; line?: number }) => ({ + text: `// ${selector.symbolName ?? "sym"}\n${"x".repeat(bodyChars)}\n`, + symbol: { + path: pathName, + name: selector.symbolName ?? "sym", + kind: "function" as const, + lineRange: [1, 40] as [number, number] + }, + meta + }) + }; + } + + function separated(pathName: string, count: number, gap = 100): DiffFile { + return { + path: pathName, + status: "modified", + language: "typescript", + hunks: Array.from({ length: count }, (_, index) => { + const line = 1 + index * gap; + return { + id: `h${index + 1}`, + hunkHash: String(index + 1).repeat(64).slice(0, 64), + path: pathName, + oldStart: line, + oldLines: 1, + newStart: line, + newLines: 1, + header: `@@ -${line} +${line} @@`, + lines: [{ kind: "add" as const, content: `const v${index + 1} = ${index + 1};`, newLineNumber: line }] + }; + }) + }; + } + + function plan(file: DiffFile): ReviewPlan { + return { + diffUnderstanding: { declaredIntent: "test intent", inferredBehavior: "test behavior" }, + coverage: file.hunks.map((hunk) => ({ + hunkId: hunk.id, + path: file.path, + coverage: "normal" as const, + lenses: ["core/code-review"], + surroundingContextHints: [], + reason: "test" + })) + }; + } + + async function run(file: DiffFile, opts: { bodyChars: number; distinctSymbols: boolean; events?: unknown[] }) { + const hunkIds = file.hunks.map((hunk) => hunk.id); + const index: RepositoryIndex = { + facts: [], + symbolFacts: symbolFactsFor(file.path, hunkIds, (id) => (opts.distinctSymbols ? `sym_${id}` : "shared"), !opts.distinctSymbols), + staticSignals: [], + tools: symbolTools(opts.bodyChars) + }; + const recorder = opts.events + ? ({ event: (entry: unknown) => opts.events?.push(entry), writeArtifact: async () => undefined } as unknown as ReturnType) + : nullTelemetry(); + const base = config(); + return buildReviewPackets(plan(file), [file], [fakeFacts(file.path, "per-hunk")], index, recorder, { + config: { ...base, review: { ...base.review, packRelatedHunks: true } }, + enabledLenses: ["core/code-review"] + }); + } + + it("renders every member's symbol source in a packed packet", async () => { + const file = separated("app.ts", 3); + const packets = await run(file, { bodyChars: 400, distinctSymbols: true }); + expect(packets).toHaveLength(1); + const text = packets[0]?.contextText ?? ""; + for (const hunkId of ["h1", "h2", "h3"]) { + expect(text).toContain(`sym_${hunkId}`); + } + }); + + it("keeps symbol source inside its budget and leaves room for other context", async () => { + const file = separated("app.ts", 4); + const packets = await run(file, { bodyChars: 4000, distinctSymbols: true }); + const text = packets[0]?.contextText ?? ""; + const symbolSection = text.split("Outline for")[0] ?? text; + expect(symbolSection.length).toBeLessThanOrEqual(5_000 + 200); + expect(text.length).toBeLessThanOrEqual(8_000); + // every member still present after final rendering + expect((text.match(/Primary symbol:/gu) ?? []).length).toBe(4); + }); + + it("cannot starve a member at the shipped cap, so the budget guards stay dormant", () => { + // At MAX_HUNKS_PER_PACKET = 5 a packet holds at most five distinct primary + // symbols, so the smallest possible share is 5000/5 = 1000 characters — + // above both the 800 floor and the 600 sliced minimum. The oversubscription + // and collapse rejections are therefore defensive invariants that cannot + // fire at today's cap; they become live only if the cap is ever raised past + // six. This test pins that arithmetic so a cap change surfaces here. + const budget = 5_000; + const minMemberChars = 800; + const minSlicedChars = 600; + const maxMembers = 5; + expect(maxMembers * minMemberChars).toBeLessThanOrEqual(budget); + expect(Math.floor(budget / maxMembers)).toBeGreaterThan(minSlicedChars); + expect(Math.floor(budget / 7)).toBeLessThan(minMemberChars); + }); + + it("floors a packed profile to its strongest standalone member", async () => { + // h1 and h3 share an enclosing symbol but are separated by h2, so the + // grouper yields three atoms with a same_symbol edge between h1 and h3. + // Standalone, that edge gives each of them related-changed context; packing + // absorbs the edge target, which would otherwise derive a weaker profile. + const file = separated("app.ts", 3); + const shared = (id: string) => (id === "h2" ? "other" : "shared"); + const facts: HunkSymbolFacts[] = file.hunks.map((hunk) => ({ + path: file.path, + hunkId: hunk.id, + enclosingSymbol: shared(hunk.id), + symbolKind: "function", + symbolRange: shared(hunk.id) === "shared" ? [1, 40] : [200, 240], + changedLines: [hunk.newStart], + changedLinesSide: "new", + source: "tree-sitter", + confidence: "syntactic" + })); + const base = config(); + const index: RepositoryIndex = { facts: [], symbolFacts: facts, staticSignals: [], tools: symbolTools(900) }; + const packed = await buildReviewPackets(plan(file), [file], [fakeFacts(file.path, "per-hunk")], index, nullTelemetry(), { + config: { ...base, review: { ...base.review, packRelatedHunks: true } }, + enabledLenses: ["core/code-review"] + }); + const unpacked = await buildReviewPackets(plan(file), [file], [fakeFacts(file.path, "per-hunk")], index, nullTelemetry(), { + config: { ...base, review: { ...base.review, packRelatedHunks: false } }, + enabledLenses: ["core/code-review"] + }); + + const rank = { simple: 0, standard: 1, investigate: 2 } as const; + const strongestStandalone = Math.max(...unpacked.map((packet) => rank[packet.reviewProfile])); + expect(packed).toHaveLength(1); + expect(rank[packed[0]?.reviewProfile ?? "simple"]).toBeGreaterThanOrEqual(strongestStandalone); + }); + + it("shares one symbol budget when packed members resolve to the same symbol", async () => { + const file = separated("app.ts", 3); + const packets = await run(file, { bodyChars: 400, distinctSymbols: false }); + expect(packets).toHaveLength(1); + expect((packets[0]?.contextText.match(/Primary symbol:/gu) ?? []).length).toBe(1); + }); +});