package billcost import ( "fmt" "sort" ) // Reflect dry-runs the cost table and emits one Violation per rule // failure. The reflector groups bands by Partition and runs the // per-partition rule set on each group, then runs the cross-cutting // coverage rule. // // Scope (alpha.25 fix #11): the reflector validates structure + // simple sanity logic only. Carrier-specific business pricing rules // (per-carrier cost ceilings, cross-band jump tolerance, city // whitelists, return-fee caps) were removed because the reflector // has no carrier context to set them correctly -- a single hard-coded // threshold either false-positives (the alpha.25 cross_band_jump // case, raising 30+ violations on real YTO tables) or fails to catch // per-carrier outliers. PM ruled the reflector should stay // schema-and-shape only; pricing-policy validation belongs to the // downstream platform layer that knows the carrier. // // Per-partition rules (in order): // // segment_order — adjacent bands sorted by LimitBottom // segment_gap — bands[i].LimitTop == bands[i+1].LimitBottom // segment_overlap — same key, but raised when LimitBottom < prev.LimitTop // segment_start_nonzero — bands[0].LimitBottom == 0 // segment_end_short — bands[len-1].LimitTop >= cfg.MaxWeightG // monotonic — cost(w) non-decreasing across band-derived sample weights // first_lt_increment — for any band where BaseWeight == IncrementWeight: BaseAmount/BaseWeight >= IncrementPrice/IncrementWeight (per-gram) // band_incomplete — each band has at least one of (BaseAmount > 0, IncrementPrice > 0) // // Cross-cutting rules: // // partition_missing — every cfg.ExpectedPartitions value appears in Bands // // Sample weights are derived from each band's own boundaries // (LimitBottom+1, BaseWeight, BaseWeight+1, LimitTop) so any band // count and any segment length is covered without a fixed grid. // // Reflect dry-run 跑成本表, 每条规则失败 emit 一条 Violation. 按 // Partition group bands 跑分区内规则集, 再跑跨规则 (分区覆盖). // // 范围 (alpha.25 修 #11): 反射器只做结构 + 简单逻辑校验, 不假定承运商 // 业务定价规则 (单价上限 / 跨段跳价容忍 / 城市白名单 / 退回费上限). // 反射器无承运商上下文, 单一阈值要么误报 (alpha.25 cross_band_jump 在 // 圆通真实表 30+ 误报), 要么漏掉单承运商离群. PM 拍板反射器只看 schema // 和 shape, 定价策略校验属于知道承运商身份的下游 platform 层. // // 分区内规则 (顺序见上方英文注释表). 跨规则同上. // // 采样重量从 band 自身边界派生 (LimitBottom+1 / BaseWeight / // BaseWeight+1 / LimitTop), 任意段数 / 任意段长都覆盖, 不用固定网格. func Reflect(out Output, cfg ReflectConfig) []Violation { var violations []Violation // Group bands by Partition (skip strip-fee bands, those are // handled separately). // 按 Partition group bands (单带费段独立处理). byPartition := map[string][]Band{} for _, b := range out.Bands { if b.IsStripFee { continue } byPartition[b.Partition] = append(byPartition[b.Partition], b) } for partition, bands := range byPartition { violations = append(violations, reflectPartition(partition, bands, cfg)...) } violations = append(violations, reflectCoverage(byPartition, cfg)...) violations = append(violations, reflectDuplicates(out)...) return violations } // reflectDuplicates flags exact-duplicate band rows: a quote table // must not list the same (Partition, LimitBottom, LimitTop) triple // twice for cost bands, nor the same (City, LimitBottom, LimitTop) // twice for strip-fee bands. r10v3 (qwen3-32B) probe surfaced the // failure mode -- the LLM emitted the entire JSON object multiple // times concatenated, which after parsing presented as duplicate // rows -- and the reflector previously had no rule to catch it, // letting downstream billing see two identical (province, weight // segment) tuples and risk double-counting. Distinct from // segment_overlap (which flags partial-range intersections like // [0,500] vs [400,800]); duplicates are exact equality. Reported // once per offending key on the second occurrence so a 3-fold // repeat yields two violations (2nd and 3rd). // // reflectDuplicates 报告"完全相同"的重复段行: 一张报价表不该对同一 // (Partition, LimitBottom, LimitTop) 普通段或同一 (City, LimitBottom, // LimitTop) 单带费段出现两次. r10v3 (qwen3-32B) probe 实测暴露此故障 // 模式 -- LLM 把整个 JSON object 拼接输出多次, 反序列化后呈现为重复 // 行, 反射器此前无对应规则下游计费层会看到同一 (省, 段) 两次, 有 // 双计费风险. 与 segment_overlap 区分 (那是 [0,500] vs [400,800] 之 // 类的部分相交); 重复是完全相等. 每个冲突 key 在第 2 次出现时报一条, // 所以 3 次重复会出 2 条 violations (第 2 / 第 3 次各一). func reflectDuplicates(out Output) []Violation { var violations []Violation // Cost bands: (Partition, LimitBottom, LimitTop) must be unique. // // 普通段: (Partition, LimitBottom, LimitTop) 唯一. seen := map[string]int{} for _, b := range out.Bands { if b.IsStripFee { continue } key := fmt.Sprintf("%s|%d-%d", b.Partition, b.LimitBottom, b.LimitTop) seen[key]++ if seen[key] >= 2 { violations = append(violations, Violation{ RuleID: RuleDuplicateBand, Partition: b.Partition, WeightG: b.LimitTop, Detail: fmt.Sprintf("partition=%q 段 %d-%dg 重复出现 (第 %d 次)", b.Partition, b.LimitBottom, b.LimitTop, seen[key]), }) } } // Strip-fee bands: (City, LimitBottom, LimitTop) must be unique. // // 单带费段: (City, LimitBottom, LimitTop) 唯一. seenStrip := map[string]int{} for _, b := range out.StripFees { key := fmt.Sprintf("%s|%d-%d", b.City, b.LimitBottom, b.LimitTop) seenStrip[key]++ if seenStrip[key] >= 2 { violations = append(violations, Violation{ RuleID: RuleDuplicateStripFee, Partition: b.City, WeightG: b.LimitTop, Detail: fmt.Sprintf("city=%q 单带费段 %d-%dg 重复出现 (第 %d 次)", b.City, b.LimitBottom, b.LimitTop, seenStrip[key]), }) } } return violations } // reflectPartition runs all per-partition rules on bands belonging // to one partition. // // reflectPartition 跑单分区的全部分区内规则. func reflectPartition(partition string, bands []Band, cfg ReflectConfig) []Violation { var violations []Violation if len(bands) == 0 { return violations } // Sort by LimitBottom so adjacent-band rules are well-defined. // The original order is preserved by the partition sort below // only insofar as LimitBottom ties (we treat that as // segment_order violation). // 按 LimitBottom 升序排序, 相邻段规则才有定义. sorted := make([]Band, len(bands)) copy(sorted, bands) sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].LimitBottom < sorted[j].LimitBottom }) // segment_order: any out-of-order bands by LimitBottom. // segment_order: 任何 LimitBottom 顺序错乱的段. for i := range bands { if bands[i].LimitBottom != sorted[i].LimitBottom || bands[i].LimitTop != sorted[i].LimitTop { violations = append(violations, Violation{ RuleID: RuleSegmentOrder, Partition: partition, Detail: fmt.Sprintf("分区 %s 的段未按 LimitBottom 升序", partition), }) break } } // segment_start_nonzero: first band must start at 0. // segment_start_nonzero: 首段下限必须为 0. if sorted[0].LimitBottom != 0 { violations = append(violations, Violation{ RuleID: RuleSegmentStartZero, Partition: partition, Detail: fmt.Sprintf("分区 %s 首段 LimitBottom=%d, 应为 0", partition, sorted[0].LimitBottom), }) } // segment_end_short: last band must reach cfg.MaxWeightG. // segment_end_short: 末段必须到期望最大重量. if cfg.MaxWeightG > 0 && sorted[len(sorted)-1].LimitTop < cfg.MaxWeightG { violations = append(violations, Violation{ RuleID: RuleSegmentEndShort, Partition: partition, Detail: fmt.Sprintf("分区 %s 末段 LimitTop=%d, 应 >= %d", partition, sorted[len(sorted)-1].LimitTop, cfg.MaxWeightG), }) } // segment_gap / segment_overlap on each adjacent pair. // 相邻段对的 gap / overlap. for i := 0; i < len(sorted)-1; i++ { prev, next := sorted[i], sorted[i+1] switch { case next.LimitBottom > prev.LimitTop: violations = append(violations, Violation{ RuleID: RuleSegmentGap, Partition: partition, Detail: fmt.Sprintf("分区 %s 段 [%d,%d] 与 [%d,%d] 之间有缺口", partition, prev.LimitBottom, prev.LimitTop, next.LimitBottom, next.LimitTop), }) case next.LimitBottom < prev.LimitTop: violations = append(violations, Violation{ RuleID: RuleSegmentOverlap, Partition: partition, Detail: fmt.Sprintf("分区 %s 段 [%d,%d] 与 [%d,%d] 有重叠", partition, prev.LimitBottom, prev.LimitTop, next.LimitBottom, next.LimitTop), }) } } // band_incomplete: each band must have at least one priced lever. // band_incomplete: 每段至少首重价或续重价之一非零. for _, b := range sorted { if b.BaseAmount == 0 && b.IncrementPrice == 0 { violations = append(violations, Violation{ RuleID: RuleBandIncomplete, Partition: partition, Detail: fmt.Sprintf("分区 %s 段 [%d,%d] 首重价与续重价都为 0", partition, b.LimitBottom, b.LimitTop), }) } } // first_lt_increment: per-gram first-weight unit price must not // fall below the per-gram continuation unit price -- but ONLY // when base_weight equals increment_weight. When base_weight is // much larger than increment_weight (e.g. 3kg first-segment // flat-rate band 3.5 元 with 1kg continuation 1.5 元/kg), the // "per-gram" division is just a flat-rate average vs. an // incremental unit price; comparing them is apples-to-oranges // (large packages get bulk discount, marginal kg costs more). // PM (alpha.25): 内蒙古 6元/kg 首重 vs 3元/kg 续重 case is the // real reverse-logic, where base_weight=1000=increment_weight. // // first_lt_increment: 每段首重每 g 单价不得 < 续重每 g 单价 -- // 仅在 base_weight 等于 increment_weight 时检查. 当 base_weight // >> increment_weight (e.g. 3kg 首段一口价 3.5 + 续重 1kg 1.5), // per-gram 除法是"首段平均"vs"边际单价", 比无意义 (大包整段折扣, // 续重边际单价高合理). PM (alpha.25): 内蒙古 6元/kg 首重 vs 3元/kg // 续重才是真反逻辑 (base_weight=1000=increment_weight). for _, b := range sorted { if b.BaseWeight <= 0 || b.IncrementWeight <= 0 || b.IncrementPrice <= 0 { continue } if b.BaseWeight != b.IncrementWeight { continue } firstPerG := b.BaseAmount / float64(b.BaseWeight) incrPerG := b.IncrementPrice / float64(b.IncrementWeight) if firstPerG+1e-9 < incrPerG { violations = append(violations, Violation{ RuleID: RuleFirstLessIncrement, Partition: partition, Detail: fmt.Sprintf("分区 %s 段 [%d,%d] 首重每 g %.5f 元 < 续重每 g %.5f 元 (反逻辑)", partition, b.LimitBottom, b.LimitTop, firstPerG, incrPerG), }) } } // monotonic + numeric_range across band-derived sample weights. // 单调 + 数值域校验, 采样点从 bands 自身边界派生. samples := derivedSamples(sorted) var prevCost float64 covered := false for _, w := range samples { cost, err := CalculateShipCost(sorted, w) if err != nil { // Sample derived from a band must be covered; if not, // it implies a gap or order violation already raised. // 派生采样点应必命中; 不命中则已被 gap/order 违规覆盖. continue } if covered && cost+1e-9 < prevCost { violations = append(violations, Violation{ RuleID: RuleMonotonic, Partition: partition, WeightG: w, GotCost: cost, PrevCost: prevCost, Detail: fmt.Sprintf("分区 %s 重量 %dg 成本 %.2f 元 < 上一采样 %.2f 元 (单调性破坏)", partition, w, cost, prevCost), }) } prevCost = cost covered = true } return violations } // derivedSamples returns sorted unique sample weights drawn from the // bands' own boundaries (LimitBottom+1, BaseWeight, BaseWeight+1, // LimitTop). The resulting list is naturally sorted ascending so a // monotonic pass over it tests cost(w) monotonicity. // // derivedSamples 从 bands 自身边界派生采样重量 (LimitBottom+1 / // BaseWeight / BaseWeight+1 / LimitTop), 去重后升序返回; 单次遍历 // 即可验 cost(w) 单调性. func derivedSamples(bands []Band) []int { seen := map[int]struct{}{} add := func(w int) { if w <= 0 { return } seen[w] = struct{}{} } for _, b := range bands { add(b.LimitBottom + 1) add(b.BaseWeight) add(b.BaseWeight + 1) add(b.LimitTop) } out := make([]int, 0, len(seen)) for w := range seen { out = append(out, w) } sort.Ints(out) return out } // reflectCoverage checks that every cfg.ExpectedPartitions value // appears in the parsed output. // // reflectCoverage 验证 cfg.ExpectedPartitions 中每个分区都在输出中 // 出现. func reflectCoverage(byPartition map[string][]Band, cfg ReflectConfig) []Violation { if len(cfg.ExpectedPartitions) == 0 { return nil } var violations []Violation for _, want := range cfg.ExpectedPartitions { if _, ok := byPartition[want]; !ok { violations = append(violations, Violation{ RuleID: RuleCoverage, Partition: want, Detail: fmt.Sprintf("期望分区 %s 缺失", want), }) } } return violations }