Validating the loosened resolution against the build before it found the one thing it over-did: nine drop points, all bearing the commodity name every drop point bears, where two donor pins each claimed the same serving pin as their match. A place is one place -- a serving pin already resolved is off the market, the next donor bearing its name finds its true neighbour or is held saying why, and the gate now fails any build where a match is not one-to-one. The maturity report grows a merge line per source per build -- donor pins, matches with their median distance, additions and adoptions, held, rejected -- so successive builds of one game sit above one another and a policy change reads as exactly the numbers that moved. Comparing the strict and refined builds this way showed all original pairs stable under the same winners, every promotion a genuine pair, and the drop points as the only excess: 270 matched, one-to-one, of 368. The resolution change bumps the policy revision so it supersedes deterministically.
- commit
- fb1f9088aa472a431336866c0e3a52299b348933
- parent
- 79c9044671ec14adffdad2be97992540d1c7f737
- author
- Dami <dami.etoile@gmail.com>
| M | tools/ | 5 | ++- |
| M | tools/ | 34 | ++++++++++++++++++-- |
| M | tools/ | 53 | ++++++++++++++++++++++++++++++ |
@@ -18,7 +18,10 @@ import (
1818 // kept field, a dropped category -- and among equal captures the registry1919 // serves the highest revision. Bump it when a policy change should supersede2020 // the builds already in every library.21-const policyRevision = 121+//22+// 1 first revisioned builds23+// 2 merge resolution: subset names, adoption, one-to-one matches24+const policyRevision = 22225 2326 // writeBundles packs each game into its own .atlas file, named by game,2427 // capture day, and stamp. The directory is a registry, not a mirror: a new@@ -216,6 +216,7 @@ func mergeMap(
216216 outcome := resolvePin(location, x, y, index, mappedKey, nearbyRadius)217217 switch outcome.kind {218218 case pinMatched:219+ index.claimed[outcome.match] = location.ID219220 pair := mergedPair{220221 Donor: location.ID,221222 Winner: outcome.match.ID,@@ -304,6 +305,10 @@ type winnerIndex struct {
304305 byName map[string][]int305306 categories map[string]*catalogCategory306307 ids map[int64]string308+ // claimed maps each serving pin already matched to the donor that309+ // matched it. A place is one place: the next donor bearing the same name310+ // must find its own, or say why it cannot.311+ claimed map[*catalogLocation]int64307312 }308313 309314 type placedPin struct {@@ -318,6 +323,7 @@ func indexWinner(winner *catalogMap, grid tileGrid) *winnerIndex {
318323 byName: make(map[string][]int),319324 categories: make(map[string]*catalogCategory),320325 ids: make(map[int64]string),326+ claimed: make(map[*catalogLocation]int64),321327 }322328 for groupIndex := range winner.Groups {323329 for categoryIndex := range winner.Groups[groupIndex].Categories {@@ -376,7 +382,17 @@ func resolvePin(
376382 377383 var nearest *placedPin378384 nearestDistance := math.Inf(1)385+ alreadyClaimed := false379386 consider := func(pin *placedPin, distance float64) {387+ // A serving pin another donor already resolved to is one place, not388+ // two: it cannot be matched again, but its nearness is remembered so389+ // the refusal can say what stood in the way.390+ if _, taken := index.claimed[pin.location]; taken {391+ if distance <= matchRadiusPx {392+ alreadyClaimed = true393+ }394+ return395+ }380396 if distance < nearestDistance {381397 nearest, nearestDistance = pin, distance382398 }@@ -409,6 +425,9 @@ func resolvePin(
409425 nearest.location.Title, nearestDistance)}410426 }411427 }428+ if alreadyClaimed {429+ return pinOutcome{kind: pinHeld, reason: "every nearby pin of this name is already matched"}430+ }412431 if _, shared := index.categories[mappedKey]; shared {413432 for at := range index.pins {414433 pin := &index.pins[at]@@ -568,14 +587,23 @@ func enrichedCount(merge *mergedSource) int {
568587 return count569588 }570589 571-// mergeGate is the merge's own audit: every donor pin accounted for, no572-// identifier doubled, nothing the serving map held made worse. It fails the573-// build rather than writing a bundle that quietly lost something.590+// mergeGate is the merge's own audit: every donor pin accounted for, every591+// match one-to-one, no identifier doubled, nothing the serving map held made592+// worse. It fails the build rather than writing a bundle that quietly lost593+// something -- or quietly agreed too much.574594 func mergeGate(merge *mergedSource, winner *catalogMap) error {575595 accounted := len(merge.Matched) + merge.Added + len(merge.Held) + len(merge.Rejected)576596 if accounted != merge.DonorPins {577597 return fmt.Errorf("merge accounts for %d of %d donor pins", accounted, merge.DonorPins)578598 }599+ claimed := make(map[int64]int64)600+ for _, pair := range merge.Matched {601+ if first, taken := claimed[pair.Winner]; taken {602+ return fmt.Errorf("serving pin %d matched by donors %d and %d; a place is one place",603+ pair.Winner, first, pair.Donor)604+ }605+ claimed[pair.Winner] = pair.Donor606+ }579607 seen := make(map[int64]string)580608 counted := 0581609 for _, group := range winner.Groups {@@ -37,6 +37,18 @@ type mapDetail struct {
3737 } `json:"locations"`3838 } `json:"categories"`3939 } `json:"groups"`40+ Merged []struct {41+ Source string `json:"source"`42+ DonorPins int `json:"donorPins"`43+ Matched []struct {44+ DistancePx int `json:"px"`45+ } `json:"matched"`46+ Added int `json:"added"`47+ Adopted []struct{} `json:"adopted"`48+ Held []struct{} `json:"held"`49+ Rejected []struct{} `json:"rejected"`50+ Alignment string `json:"alignment"`51+ } `json:"merged"`4052 Zones []struct {4153 Features []struct {4254 Coordinates json.RawMessage `json:"coordinates"`@@ -70,6 +82,21 @@ type measure struct {
7082 Variants int7183 IconsCarried int7284 IconsWanted int85+ Merges []mergeLine86+}87+88+// mergeLine is one source's agreement account within a build, so two builds'89+// lines sit above one another and a policy change reads as the numbers that90+// moved.91+type mergeLine struct {92+ Source string93+ DonorPins int94+ Matched int95+ MedianMatchPx int96+ Added int97+ Adopted int98+ Held int99+ Rejected int73100 }74101 75102 func main() {@@ -144,6 +171,11 @@ func run(dir string) error {
144171 m.Categories, m.Groups, m.TextSets, m.Zones, m.Vertices, m.Variants)145172 fmt.Printf(" icons %5d of %d marker categories carry one (%s)\n",146173 m.IconsCarried, m.IconsWanted, percent(m.IconsCarried, m.IconsWanted))174+ for _, line := range m.Merges {175+ fmt.Printf(" merge %s: %d donor pins · %d matched (median %dpx) · %d added (%d adopted) · %d held · %d rejected\n",176+ line.Source, line.DonorPins, line.Matched, line.MedianMatchPx,177+ line.Added, line.Adopted, line.Held, line.Rejected)178+ }147179 }148180 }149181 return nil@@ -226,6 +258,27 @@ func measureBundle(path string) (string, string, measure, error) {
226258 }227259 }228260 }261+ for _, merged := range detail.Merged {262+ distances := make([]int, 0, len(merged.Matched))263+ for _, pair := range merged.Matched {264+ distances = append(distances, pair.DistancePx)265+ }266+ sort.Ints(distances)267+ median := 0268+ if len(distances) > 0 {269+ median = distances[len(distances)/2]270+ }271+ m.Merges = append(m.Merges, mergeLine{272+ Source: merged.Source,273+ DonorPins: merged.DonorPins,274+ Matched: len(merged.Matched),275+ MedianMatchPx: median,276+ Added: merged.Added,277+ Adopted: len(merged.Adopted),278+ Held: len(merged.Held),279+ Rejected: len(merged.Rejected),280+ })281+ }229282 }230283 sort.Ints(lengths)231284 if len(lengths) > 0 {