cricinfo-cli-go 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/AGENTS.md +63 -0
  2. package/CONTRIBUTORS.md +75 -0
  3. package/LICENSE +21 -0
  4. package/Makefile +131 -0
  5. package/README.md +130 -0
  6. package/bin/cricinfo.js +44 -0
  7. package/cmd/cricinfo/main.go +15 -0
  8. package/go.mod +10 -0
  9. package/go.sum +10 -0
  10. package/internal/app/app.go +11 -0
  11. package/internal/app/app_test.go +122 -0
  12. package/internal/buildinfo/buildinfo.go +16 -0
  13. package/internal/cli/analysis.go +262 -0
  14. package/internal/cli/analysis_test.go +175 -0
  15. package/internal/cli/competitions.go +154 -0
  16. package/internal/cli/competitions_test.go +165 -0
  17. package/internal/cli/leagues.go +297 -0
  18. package/internal/cli/leagues_test.go +194 -0
  19. package/internal/cli/matches.go +403 -0
  20. package/internal/cli/matches_test.go +413 -0
  21. package/internal/cli/players.go +263 -0
  22. package/internal/cli/players_test.go +384 -0
  23. package/internal/cli/root.go +141 -0
  24. package/internal/cli/search.go +119 -0
  25. package/internal/cli/teams.go +214 -0
  26. package/internal/cli/teams_test.go +192 -0
  27. package/internal/cricinfo/analysis.go +1401 -0
  28. package/internal/cricinfo/analysis_phase15_test.go +267 -0
  29. package/internal/cricinfo/client.go +471 -0
  30. package/internal/cricinfo/client_test.go +280 -0
  31. package/internal/cricinfo/cmd/fixture-refresh/main.go +145 -0
  32. package/internal/cricinfo/competitions.go +405 -0
  33. package/internal/cricinfo/competitions_phase13_test.go +234 -0
  34. package/internal/cricinfo/coverage_ledger.go +122 -0
  35. package/internal/cricinfo/coverage_ledger_test.go +253 -0
  36. package/internal/cricinfo/decode.go +115 -0
  37. package/internal/cricinfo/decode_test.go +100 -0
  38. package/internal/cricinfo/entity_index.go +618 -0
  39. package/internal/cricinfo/entity_index_test.go +175 -0
  40. package/internal/cricinfo/fixture_matrix.go +243 -0
  41. package/internal/cricinfo/fixture_matrix_test.go +49 -0
  42. package/internal/cricinfo/fixtures_test.go +264 -0
  43. package/internal/cricinfo/historical_hydration.go +1641 -0
  44. package/internal/cricinfo/historical_phase14_test.go +542 -0
  45. package/internal/cricinfo/leagues.go +1210 -0
  46. package/internal/cricinfo/leagues_phase12_test.go +324 -0
  47. package/internal/cricinfo/live_leagues_test.go +169 -0
  48. package/internal/cricinfo/live_matches_test.go +203 -0
  49. package/internal/cricinfo/live_matrix_test.go +118 -0
  50. package/internal/cricinfo/live_players_test.go +122 -0
  51. package/internal/cricinfo/live_search_test.go +86 -0
  52. package/internal/cricinfo/live_smoke_test.go +213 -0
  53. package/internal/cricinfo/live_teams_test.go +104 -0
  54. package/internal/cricinfo/matches.go +1508 -0
  55. package/internal/cricinfo/matches_phase7_test.go +207 -0
  56. package/internal/cricinfo/matches_phase9_test.go +253 -0
  57. package/internal/cricinfo/normalize_entities.go +1727 -0
  58. package/internal/cricinfo/normalize_leagues.go +346 -0
  59. package/internal/cricinfo/players.go +1332 -0
  60. package/internal/cricinfo/players_phase10_test.go +174 -0
  61. package/internal/cricinfo/players_phase11_test.go +373 -0
  62. package/internal/cricinfo/render_contract.go +1088 -0
  63. package/internal/cricinfo/render_phase4_test.go +633 -0
  64. package/internal/cricinfo/renderer.go +1689 -0
  65. package/internal/cricinfo/resolver.go +813 -0
  66. package/internal/cricinfo/resolver_test.go +244 -0
  67. package/internal/cricinfo/teams.go +603 -0
  68. package/internal/cricinfo/teams_phase8_test.go +231 -0
  69. package/internal/cricinfo/testdata/fixtures/README.md +43 -0
  70. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/broadcasts.json +11 -0
  71. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/officials.json +150 -0
  72. package/internal/cricinfo/testdata/fixtures/details-plays/detail-110.json +157 -0
  73. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52545007.json +145 -0
  74. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52559021.json +143 -0
  75. package/internal/cricinfo/testdata/fixtures/details-plays/plays.json +15 -0
  76. package/internal/cricinfo/testdata/fixtures/endpoint-matrix.tsv +19 -0
  77. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow-1.json +12 -0
  78. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow.json +42 -0
  79. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/innings-1-2.json +38 -0
  80. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnership-1.json +31 -0
  81. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnerships.json +42 -0
  82. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-offdays.json +20 -0
  83. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-ondays.json +21 -0
  84. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar.json +14 -0
  85. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-2025.json +13 -0
  86. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-group-1.json +13 -0
  87. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-groups.json +11 -0
  88. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-type-1.json +13 -0
  89. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-types.json +11 -0
  90. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/seasons.json +30 -0
  91. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-item-1.json +72 -0
  92. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-root.json +3 -0
  93. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings.json +15 -0
  94. package/internal/cricinfo/testdata/fixtures/matches-competitions/competition.json +460 -0
  95. package/internal/cricinfo/testdata/fixtures/matches-competitions/event-1529474.json +86 -0
  96. package/internal/cricinfo/testdata/fixtures/matches-competitions/matchcards-1527966.json +368 -0
  97. package/internal/cricinfo/testdata/fixtures/matches-competitions/situation-1529474.json +10 -0
  98. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257-statistics.json +126 -0
  99. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257.json +113 -0
  100. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-1-statistics-0.json +208 -0
  101. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-2-statistics-0.json +252 -0
  102. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores.json +74 -0
  103. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-statistics-0.json +1008 -0
  104. package/internal/cricinfo/testdata/fixtures/root-discovery/events.json +72 -0
  105. package/internal/cricinfo/testdata/fixtures/root-discovery/root.json +28 -0
  106. package/internal/cricinfo/testdata/fixtures/team-competitor/competitor-789643.json +40 -0
  107. package/internal/cricinfo/testdata/fixtures/team-competitor/leaders-789643.json +353 -0
  108. package/internal/cricinfo/testdata/fixtures/team-competitor/records-789643.json +91 -0
  109. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772-object.json +231 -0
  110. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772.json +235 -0
  111. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-789643.json +322 -0
  112. package/internal/cricinfo/testdata/fixtures/team-competitor/scores-789643.json +19 -0
  113. package/internal/cricinfo/testdata/fixtures/team-competitor/statistics-789643.json +629 -0
  114. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643-athletes.json +7 -0
  115. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643.json +67 -0
  116. package/internal/cricinfo/testdata/golden/match-empty.golden +1 -0
  117. package/internal/cricinfo/testdata/golden/match-list.golden +2 -0
  118. package/internal/cricinfo/testdata/golden/match-partial.golden +3 -0
  119. package/internal/cricinfo/types.go +54 -0
  120. package/package.json +51 -0
  121. package/scripts/postinstall.js +153 -0
@@ -0,0 +1,122 @@
1
+ package cricinfo
2
+
3
+ // TemplateCoverageEntry maps a researched endpoint template to public command coverage.
4
+ type TemplateCoverageEntry struct {
5
+ Template string
6
+ CommandFamily string
7
+ Command string
8
+ View string
9
+ }
10
+
11
+ // FieldPathCoverageEntry maps a field-path family to public command coverage.
12
+ type FieldPathCoverageEntry struct {
13
+ Family string
14
+ CommandFamily string
15
+ Command string
16
+ View string
17
+ }
18
+
19
+ var templateCoverageLedger = map[string]TemplateCoverageEntry{
20
+ "/": {Template: "/", CommandFamily: "leagues", Command: "leagues list", View: "root discovery seed"},
21
+ "/athletes": {Template: "/athletes", CommandFamily: "search", Command: "search players", View: "global athlete discovery seed"},
22
+ "/athletes/{id}": {Template: "/athletes/{id}", CommandFamily: "players", Command: "players profile", View: "player profile"},
23
+ "/athletes/{id}/news": {Template: "/athletes/{id}/news", CommandFamily: "players", Command: "players news", View: "player news list"},
24
+ "/athletes/{id}/statistics": {Template: "/athletes/{id}/statistics", CommandFamily: "players", Command: "players stats", View: "player statistics categories"},
25
+ "/events": {Template: "/events", CommandFamily: "matches", Command: "matches list", View: "event discovery"},
26
+ "/events/{id}": {Template: "/events/{id}", CommandFamily: "matches", Command: "matches show", View: "event competition expansion"},
27
+ "/events/{id}/competitions/{id}": {Template: "/events/{id}/competitions/{id}", CommandFamily: "competitions", Command: "competitions show", View: "competition summary"},
28
+ "/events/{id}/teams/{id}": {Template: "/events/{id}/teams/{id}", CommandFamily: "teams", Command: "teams show", View: "event-team identity subview"},
29
+ "/leagues": {Template: "/leagues", CommandFamily: "leagues", Command: "leagues list", View: "league discovery"},
30
+ "/leagues/{id}": {Template: "/leagues/{id}", CommandFamily: "leagues", Command: "leagues show", View: "league summary"},
31
+ "/leagues/{id}/athletes/{id}": {Template: "/leagues/{id}/athletes/{id}", CommandFamily: "leagues", Command: "leagues athletes", View: "league athlete profile"},
32
+ "/leagues/{id}/athletes/{n}": {Template: "/leagues/{id}/athletes/{n}", CommandFamily: "leagues", Command: "leagues athletes", View: "league athlete index page"},
33
+ "/leagues/{id}/calendar": {Template: "/leagues/{id}/calendar", CommandFamily: "leagues", Command: "leagues calendar", View: "calendar root"},
34
+ "/leagues/{id}/calendar/offdays": {Template: "/leagues/{id}/calendar/offdays", CommandFamily: "leagues", Command: "leagues calendar", View: "calendar offdays section"},
35
+ "/leagues/{id}/calendar/ondays": {Template: "/leagues/{id}/calendar/ondays", CommandFamily: "leagues", Command: "leagues calendar", View: "calendar ondays section"},
36
+ "/leagues/{id}/events": {Template: "/leagues/{id}/events", CommandFamily: "leagues", Command: "leagues events", View: "league event list"},
37
+ "/leagues/{id}/events/{id}": {Template: "/leagues/{id}/events/{id}", CommandFamily: "leagues", Command: "leagues events", View: "event expansion"},
38
+ "/leagues/{id}/events/{id}/competitions/{id}": {Template: "/leagues/{id}/events/{id}/competitions/{id}", CommandFamily: "competitions", Command: "competitions show", View: "competition summary"},
39
+ "/leagues/{id}/events/{id}/competitions/{id}/broadcasts": {Template: "/leagues/{id}/events/{id}/competitions/{id}/broadcasts", CommandFamily: "competitions", Command: "competitions broadcasts", View: "competition broadcasts"},
40
+ "/leagues/{id}/events/{id}/competitions/{id}/details": {Template: "/leagues/{id}/events/{id}/competitions/{id}/details", CommandFamily: "matches", Command: "matches details", View: "delivery ref page"},
41
+ "/leagues/{id}/events/{id}/competitions/{id}/details/{id}": {Template: "/leagues/{id}/events/{id}/competitions/{id}/details/{id}", CommandFamily: "matches", Command: "matches details", View: "delivery event detail"},
42
+ "/leagues/{id}/events/{id}/competitions/{id}/details/{n}": {Template: "/leagues/{id}/events/{id}/competitions/{id}/details/{n}", CommandFamily: "matches", Command: "matches details", View: "delivery event detail"},
43
+ "/leagues/{id}/events/{id}/competitions/{id}/matchcards": {Template: "/leagues/{id}/events/{id}/competitions/{id}/matchcards", CommandFamily: "matches", Command: "matches scorecard", View: "batting/bowling/partnership cards"},
44
+ "/leagues/{id}/events/{id}/competitions/{id}/odds": {Template: "/leagues/{id}/events/{id}/competitions/{id}/odds", CommandFamily: "competitions", Command: "competitions odds", View: "competition odds"},
45
+ "/leagues/{id}/events/{id}/competitions/{id}/officials": {Template: "/leagues/{id}/events/{id}/competitions/{id}/officials", CommandFamily: "competitions", Command: "competitions officials", View: "competition officials"},
46
+ "/leagues/{id}/events/{id}/competitions/{id}/plays": {Template: "/leagues/{id}/events/{id}/competitions/{id}/plays", CommandFamily: "matches", Command: "matches plays", View: "play-derived delivery events"},
47
+ "/leagues/{id}/events/{id}/competitions/{id}/situation": {Template: "/leagues/{id}/events/{id}/competitions/{id}/situation", CommandFamily: "matches", Command: "matches situation", View: "match situation"},
48
+ "/leagues/{id}/events/{id}/competitions/{id}/situation/odds": {Template: "/leagues/{id}/events/{id}/competitions/{id}/situation/odds", CommandFamily: "matches", Command: "matches situation", View: "situation odds subview"},
49
+ "/leagues/{id}/events/{id}/competitions/{id}/status": {Template: "/leagues/{id}/events/{id}/competitions/{id}/status", CommandFamily: "matches", Command: "matches status", View: "match status"},
50
+ "/leagues/{id}/events/{id}/competitions/{id}/tickets": {Template: "/leagues/{id}/events/{id}/competitions/{id}/tickets", CommandFamily: "competitions", Command: "competitions tickets", View: "competition tickets"},
51
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}", CommandFamily: "teams", Command: "teams show --match <match>", View: "competitor summary"},
52
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/leaders": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/leaders", CommandFamily: "teams", Command: "teams leaders --match <match>", View: "team leaders"},
53
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores", CommandFamily: "matches", Command: "matches innings", View: "team innings list"},
54
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}", CommandFamily: "matches", Command: "matches innings", View: "innings pointer"},
55
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}", CommandFamily: "matches", Command: "matches deliveries", View: "period innings detail"},
56
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/fow": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/fow", CommandFamily: "matches", Command: "matches fow", View: "fall-of-wicket list"},
57
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/fow/{n}": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/fow/{n}", CommandFamily: "matches", Command: "matches fow", View: "fall-of-wicket detail"},
58
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/leaders": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/leaders", CommandFamily: "matches", Command: "matches deliveries", View: "period leaders subview"},
59
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/partnerships": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/partnerships", CommandFamily: "matches", Command: "matches partnerships", View: "partnership list"},
60
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/partnerships/{n}": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/partnerships/{n}", CommandFamily: "matches", Command: "matches partnerships", View: "partnership detail"},
61
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/statistics/{n}": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/linescores/{n}/{n}/statistics/{n}", CommandFamily: "matches", Command: "matches deliveries", View: "period statistics timelines"},
62
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/records": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/records", CommandFamily: "teams", Command: "teams records --match <match>", View: "team records categories"},
63
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/roster": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/roster", CommandFamily: "teams", Command: "teams roster --match <match>", View: "match roster entries"},
64
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/roster/{id}/linescores": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/roster/{id}/linescores", CommandFamily: "players", Command: "players innings --match <match>", View: "player innings list"},
65
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/roster/{id}/linescores/{n}/{n}/statistics/{n}": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/roster/{id}/linescores/{n}/{n}/statistics/{n}", CommandFamily: "players", Command: "players innings --match <match>", View: "player innings period statistics"},
66
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/roster/{id}/statistics/{n}": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/roster/{id}/statistics/{n}", CommandFamily: "players", Command: "players match-stats --match <match>", View: "player match statistics"},
67
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/scores": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/scores", CommandFamily: "teams", Command: "teams scores --match <match>", View: "team score"},
68
+ "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/statistics": {Template: "/leagues/{id}/events/{id}/competitions/{id}/competitors/{id}/statistics", CommandFamily: "teams", Command: "teams statistics --match <match>", View: "team statistics categories"},
69
+ "/leagues/{id}/seasons": {Template: "/leagues/{id}/seasons", CommandFamily: "leagues", Command: "leagues seasons", View: "season list"},
70
+ "/leagues/{id}/seasons/{id}": {Template: "/leagues/{id}/seasons/{id}", CommandFamily: "seasons", Command: "seasons show", View: "season detail"},
71
+ "/leagues/{id}/seasons/{id}/types": {Template: "/leagues/{id}/seasons/{id}/types", CommandFamily: "seasons", Command: "seasons types", View: "season type list"},
72
+ "/leagues/{id}/seasons/{id}/types/{n}": {Template: "/leagues/{id}/seasons/{id}/types/{n}", CommandFamily: "seasons", Command: "seasons types", View: "season type detail"},
73
+ "/leagues/{id}/seasons/{id}/types/{n}/groups": {Template: "/leagues/{id}/seasons/{id}/types/{n}/groups", CommandFamily: "seasons", Command: "seasons groups", View: "season group list"},
74
+ "/leagues/{id}/standings": {Template: "/leagues/{id}/standings", CommandFamily: "standings", Command: "standings show", View: "standings groups"},
75
+ "/teams/{id}": {Template: "/teams/{id}", CommandFamily: "teams", Command: "teams show", View: "global team profile"},
76
+ }
77
+
78
+ var fieldPathFamilyCoverageLedger = map[string]FieldPathCoverageEntry{
79
+ "athlete": {Family: "athlete", CommandFamily: "players", Command: "players profile", View: "player identity"},
80
+ "athletesInvolved": {Family: "athletesInvolved", CommandFamily: "matches", Command: "matches details", View: "delivery athlete involvement"},
81
+ "batsman": {Family: "batsman", CommandFamily: "matches", Command: "matches details", View: "delivery batting context"},
82
+ "bowler": {Family: "bowler", CommandFamily: "matches", Command: "matches details", View: "delivery bowling context"},
83
+ "broadcasts": {Family: "broadcasts", CommandFamily: "competitions", Command: "competitions broadcasts", View: "competition broadcasts"},
84
+ "competitions": {Family: "competitions", CommandFamily: "competitions", Command: "competitions metadata", View: "competition metadata root"},
85
+ "competitors": {Family: "competitors", CommandFamily: "teams", Command: "teams show --match <match>", View: "match competitor view"},
86
+ "details": {Family: "details", CommandFamily: "matches", Command: "matches details", View: "delivery detail refs"},
87
+ "dismissal": {Family: "dismissal", CommandFamily: "players", Command: "players dismissals --match <match>", View: "dismissal metadata"},
88
+ "entries": {Family: "entries", CommandFamily: "teams", Command: "teams roster --match <match>", View: "roster entries"},
89
+ "fow": {Family: "fow", CommandFamily: "matches", Command: "matches fow", View: "fall-of-wicket timeline"},
90
+ "innings": {Family: "innings", CommandFamily: "matches", Command: "matches innings", View: "innings summary"},
91
+ "items": {Family: "items", CommandFamily: "matches", Command: "matches list", View: "page envelope items"},
92
+ "leagues": {Family: "leagues", CommandFamily: "leagues", Command: "leagues show", View: "league hierarchy"},
93
+ "matchcards": {Family: "matchcards", CommandFamily: "matches", Command: "matches scorecard", View: "scorecard cards"},
94
+ "odds": {Family: "odds", CommandFamily: "competitions", Command: "competitions odds", View: "competition odds"},
95
+ "officials": {Family: "officials", CommandFamily: "competitions", Command: "competitions officials", View: "competition officials"},
96
+ "over": {Family: "over", CommandFamily: "matches", Command: "matches deliveries", View: "over timeline"},
97
+ "partnerships": {Family: "partnerships", CommandFamily: "matches", Command: "matches partnerships", View: "partnership timeline"},
98
+ "seasons": {Family: "seasons", CommandFamily: "seasons", Command: "seasons groups", View: "season hierarchy"},
99
+ "situation": {Family: "situation", CommandFamily: "matches", Command: "matches situation", View: "situation snapshot"},
100
+ "splits": {Family: "splits", CommandFamily: "players", Command: "players stats", View: "statistics splits and categories"},
101
+ "status": {Family: "status", CommandFamily: "matches", Command: "matches status", View: "status summary"},
102
+ "teams": {Family: "teams", CommandFamily: "teams", Command: "teams show", View: "team identity"},
103
+ "tickets": {Family: "tickets", CommandFamily: "competitions", Command: "competitions tickets", View: "competition tickets"},
104
+ }
105
+
106
+ // TemplateCoverageLedger returns a copy of the researched-template coverage ledger.
107
+ func TemplateCoverageLedger() map[string]TemplateCoverageEntry {
108
+ out := make(map[string]TemplateCoverageEntry, len(templateCoverageLedger))
109
+ for template, entry := range templateCoverageLedger {
110
+ out[template] = entry
111
+ }
112
+ return out
113
+ }
114
+
115
+ // FieldPathFamilyCoverageLedger returns a copy of the field-family coverage ledger.
116
+ func FieldPathFamilyCoverageLedger() map[string]FieldPathCoverageEntry {
117
+ out := make(map[string]FieldPathCoverageEntry, len(fieldPathFamilyCoverageLedger))
118
+ for family, entry := range fieldPathFamilyCoverageLedger {
119
+ out[family] = entry
120
+ }
121
+ return out
122
+ }
@@ -0,0 +1,253 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "bufio"
5
+ "os"
6
+ "path/filepath"
7
+ "sort"
8
+ "strings"
9
+ "testing"
10
+ )
11
+
12
+ func TestTemplateCoverageLedgerIncludesAllResearchedTemplates(t *testing.T) {
13
+ t.Parallel()
14
+
15
+ ledger := TemplateCoverageLedger()
16
+ if len(ledger) == 0 {
17
+ t.Fatalf("template coverage ledger is empty")
18
+ }
19
+
20
+ templates := researchedTemplatesFromTSV(t)
21
+ if len(templates) == 0 {
22
+ t.Fatalf("expected researched templates from TSV")
23
+ }
24
+
25
+ missing := make([]string, 0)
26
+ for _, template := range templates {
27
+ entry, ok := ledger[template]
28
+ if !ok {
29
+ missing = append(missing, template)
30
+ continue
31
+ }
32
+ if strings.TrimSpace(entry.Template) == "" || entry.Template != template {
33
+ t.Fatalf("template ledger entry mismatch for %q: %+v", template, entry)
34
+ }
35
+ if strings.TrimSpace(entry.CommandFamily) == "" {
36
+ t.Fatalf("template %q has empty command family", template)
37
+ }
38
+ if strings.TrimSpace(entry.Command) == "" {
39
+ t.Fatalf("template %q has empty command mapping", template)
40
+ }
41
+ if strings.TrimSpace(entry.View) == "" {
42
+ t.Fatalf("template %q has empty result view mapping", template)
43
+ }
44
+ }
45
+
46
+ extras := make([]string, 0)
47
+ expected := make(map[string]struct{}, len(templates))
48
+ for _, template := range templates {
49
+ expected[template] = struct{}{}
50
+ }
51
+ for template := range ledger {
52
+ if _, ok := expected[template]; !ok {
53
+ extras = append(extras, template)
54
+ }
55
+ }
56
+
57
+ sort.Strings(missing)
58
+ sort.Strings(extras)
59
+ if len(missing) > 0 || len(extras) > 0 {
60
+ t.Fatalf("template coverage drift: missing=%v extras=%v", missing, extras)
61
+ }
62
+ }
63
+
64
+ func TestFieldPathFamilyCoverageLedgerKnownFamiliesMapped(t *testing.T) {
65
+ t.Parallel()
66
+
67
+ ledger := FieldPathFamilyCoverageLedger()
68
+ if len(ledger) == 0 {
69
+ t.Fatalf("field-path family coverage ledger is empty")
70
+ }
71
+
72
+ catalogFamilies := researchedFieldPathFamilies(t)
73
+ if len(catalogFamilies) == 0 {
74
+ t.Fatalf("expected field-path families from catalog")
75
+ }
76
+
77
+ expectedFamilies := []string{
78
+ "athlete",
79
+ "athletesInvolved",
80
+ "batsman",
81
+ "bowler",
82
+ "broadcasts",
83
+ "competitions",
84
+ "competitors",
85
+ "details",
86
+ "dismissal",
87
+ "entries",
88
+ "fow",
89
+ "innings",
90
+ "items",
91
+ "leagues",
92
+ "matchcards",
93
+ "odds",
94
+ "officials",
95
+ "over",
96
+ "partnerships",
97
+ "seasons",
98
+ "situation",
99
+ "splits",
100
+ "status",
101
+ "teams",
102
+ "tickets",
103
+ }
104
+
105
+ missingLedger := make([]string, 0)
106
+ missingCatalog := make([]string, 0)
107
+ for _, family := range expectedFamilies {
108
+ entry, ok := ledger[family]
109
+ if !ok {
110
+ missingLedger = append(missingLedger, family)
111
+ continue
112
+ }
113
+ if strings.TrimSpace(entry.Family) == "" || entry.Family != family {
114
+ t.Fatalf("field-path ledger entry mismatch for %q: %+v", family, entry)
115
+ }
116
+ if strings.TrimSpace(entry.CommandFamily) == "" {
117
+ t.Fatalf("field-path family %q has empty command family", family)
118
+ }
119
+ if strings.TrimSpace(entry.Command) == "" {
120
+ t.Fatalf("field-path family %q has empty command mapping", family)
121
+ }
122
+ if strings.TrimSpace(entry.View) == "" {
123
+ t.Fatalf("field-path family %q has empty result view mapping", family)
124
+ }
125
+ if _, ok := catalogFamilies[family]; !ok {
126
+ missingCatalog = append(missingCatalog, family)
127
+ }
128
+ }
129
+
130
+ sort.Strings(missingLedger)
131
+ sort.Strings(missingCatalog)
132
+ if len(missingLedger) > 0 || len(missingCatalog) > 0 {
133
+ t.Fatalf("field-path family coverage drift: missing ledger=%v missing catalog=%v", missingLedger, missingCatalog)
134
+ }
135
+ }
136
+
137
+ func researchedTemplatesFromTSV(t *testing.T) []string {
138
+ t.Helper()
139
+
140
+ path := filepath.Join(repoRoot(t), "gg", "agent-outputs", "cricinfo-working-templates.tsv")
141
+ file, err := os.Open(path)
142
+ if err != nil {
143
+ t.Fatalf("open working templates TSV %q: %v", path, err)
144
+ }
145
+ defer file.Close()
146
+
147
+ seen := map[string]struct{}{}
148
+ templates := make([]string, 0, 64)
149
+ scanner := bufio.NewScanner(file)
150
+ for scanner.Scan() {
151
+ line := strings.TrimSpace(scanner.Text())
152
+ if line == "" {
153
+ continue
154
+ }
155
+ parts := strings.SplitN(line, "\t", 3)
156
+ if len(parts) < 2 {
157
+ t.Fatalf("unexpected TSV row %q", line)
158
+ }
159
+ template := strings.TrimSpace(parts[1])
160
+ if template == "" {
161
+ continue
162
+ }
163
+ if _, ok := seen[template]; ok {
164
+ continue
165
+ }
166
+ seen[template] = struct{}{}
167
+ templates = append(templates, template)
168
+ }
169
+ if err := scanner.Err(); err != nil {
170
+ t.Fatalf("scan working templates TSV %q: %v", path, err)
171
+ }
172
+
173
+ sort.Strings(templates)
174
+ return templates
175
+ }
176
+
177
+ func researchedFieldPathFamilies(t *testing.T) map[string]struct{} {
178
+ t.Helper()
179
+
180
+ path := filepath.Join(repoRoot(t), "gg", "agent-outputs", "cricinfo-field-path-catalog.txt")
181
+ file, err := os.Open(path)
182
+ if err != nil {
183
+ t.Fatalf("open field-path catalog %q: %v", path, err)
184
+ }
185
+ defer file.Close()
186
+
187
+ families := map[string]struct{}{}
188
+ scanner := bufio.NewScanner(file)
189
+ for scanner.Scan() {
190
+ path := strings.TrimSpace(scanner.Text())
191
+ if path == "" {
192
+ continue
193
+ }
194
+ family := firstFieldPathFamily(path)
195
+ if family == "" {
196
+ continue
197
+ }
198
+ families[family] = struct{}{}
199
+ }
200
+ if err := scanner.Err(); err != nil {
201
+ t.Fatalf("scan field-path catalog %q: %v", path, err)
202
+ }
203
+
204
+ return families
205
+ }
206
+
207
+ func firstFieldPathFamily(path string) string {
208
+ parts := strings.Split(path, ".")
209
+ for _, part := range parts {
210
+ token := strings.TrimSpace(part)
211
+ if token == "" || token == "$ref" {
212
+ continue
213
+ }
214
+ if isNumericToken(token) {
215
+ continue
216
+ }
217
+ return token
218
+ }
219
+ return ""
220
+ }
221
+
222
+ func isNumericToken(value string) bool {
223
+ value = strings.TrimSpace(value)
224
+ if value == "" {
225
+ return false
226
+ }
227
+ for _, r := range value {
228
+ if r < '0' || r > '9' {
229
+ return false
230
+ }
231
+ }
232
+ return true
233
+ }
234
+
235
+ func repoRoot(t *testing.T) string {
236
+ t.Helper()
237
+
238
+ dir, err := os.Getwd()
239
+ if err != nil {
240
+ t.Fatalf("getwd: %v", err)
241
+ }
242
+
243
+ for {
244
+ if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
245
+ return dir
246
+ }
247
+ parent := filepath.Dir(dir)
248
+ if parent == dir {
249
+ t.Fatalf("unable to locate repository root from %q", dir)
250
+ }
251
+ dir = parent
252
+ }
253
+ }
@@ -0,0 +1,115 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "sort"
7
+ "strings"
8
+ )
9
+
10
+ // DecodePage decodes a paginated envelope payload.
11
+ func DecodePage[T any](data []byte) (*Page[T], error) {
12
+ var page Page[T]
13
+ if err := json.Unmarshal(data, &page); err != nil {
14
+ return nil, fmt.Errorf("decode paginated payload: %w", err)
15
+ }
16
+
17
+ if page.Items == nil {
18
+ page.Items = []T{}
19
+ }
20
+
21
+ return &page, nil
22
+ }
23
+
24
+ // DecodeObjectCollection decodes array-shaped or object-shaped collection fields.
25
+ func DecodeObjectCollection[T any](data []byte, field string) ([]T, error) {
26
+ field = strings.TrimSpace(field)
27
+ if field == "" {
28
+ return nil, fmt.Errorf("collection field is required")
29
+ }
30
+
31
+ var envelope map[string]json.RawMessage
32
+ if err := json.Unmarshal(data, &envelope); err != nil {
33
+ return nil, fmt.Errorf("decode envelope: %w", err)
34
+ }
35
+
36
+ raw, ok := envelope[field]
37
+ if !ok {
38
+ return nil, fmt.Errorf("collection field %q not found", field)
39
+ }
40
+ trimmed := strings.TrimSpace(string(raw))
41
+ if trimmed == "" || trimmed == "null" {
42
+ return []T{}, nil
43
+ }
44
+
45
+ if strings.HasPrefix(trimmed, "[") {
46
+ var items []T
47
+ if err := json.Unmarshal(raw, &items); err != nil {
48
+ return nil, fmt.Errorf("decode array collection field %q: %w", field, err)
49
+ }
50
+ return items, nil
51
+ }
52
+
53
+ if strings.HasPrefix(trimmed, "{") {
54
+ var keyed map[string]json.RawMessage
55
+ if err := json.Unmarshal(raw, &keyed); err != nil {
56
+ return nil, fmt.Errorf("decode object collection field %q: %w", field, err)
57
+ }
58
+
59
+ keys := make([]string, 0, len(keyed))
60
+ for k := range keyed {
61
+ keys = append(keys, k)
62
+ }
63
+ sort.Strings(keys)
64
+
65
+ items := make([]T, 0, len(keys))
66
+ for _, key := range keys {
67
+ var item T
68
+ if err := json.Unmarshal(keyed[key], &item); err != nil {
69
+ return nil, fmt.Errorf("decode object collection field %q key %q: %w", field, key, err)
70
+ }
71
+ items = append(items, item)
72
+ }
73
+ return items, nil
74
+ }
75
+
76
+ return nil, fmt.Errorf("collection field %q is neither array nor object", field)
77
+ }
78
+
79
+ // DecodeStatsObject decodes a single-object stats payload.
80
+ func DecodeStatsObject(data []byte) (*StatsObject, error) {
81
+ var stats StatsObject
82
+ if err := json.Unmarshal(data, &stats); err != nil {
83
+ return nil, fmt.Errorf("decode stats payload: %w", err)
84
+ }
85
+ return &stats, nil
86
+ }
87
+
88
+ // ExtractOptionalRef gets a nested {"$ref":"..."} field when present.
89
+ func ExtractOptionalRef(data []byte, field string) (*Ref, bool, error) {
90
+ field = strings.TrimSpace(field)
91
+ if field == "" {
92
+ return nil, false, fmt.Errorf("field is required")
93
+ }
94
+
95
+ var envelope map[string]json.RawMessage
96
+ if err := json.Unmarshal(data, &envelope); err != nil {
97
+ return nil, false, fmt.Errorf("decode envelope: %w", err)
98
+ }
99
+
100
+ raw, ok := envelope[field]
101
+ if !ok || string(raw) == "null" {
102
+ return nil, false, nil
103
+ }
104
+
105
+ var ref Ref
106
+ if err := json.Unmarshal(raw, &ref); err != nil {
107
+ return nil, false, fmt.Errorf("decode ref field %q: %w", field, err)
108
+ }
109
+
110
+ if strings.TrimSpace(ref.URL) == "" {
111
+ return nil, false, nil
112
+ }
113
+
114
+ return &ref, true, nil
115
+ }
@@ -0,0 +1,100 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "testing"
5
+ )
6
+
7
+ type testItem struct {
8
+ ID int `json:"id"`
9
+ }
10
+
11
+ func TestDecodePage(t *testing.T) {
12
+ t.Parallel()
13
+
14
+ page, err := DecodePage[Ref]([]byte(`{"count":1,"items":[{"$ref":"http://example.com/a"}],"pageCount":1,"pageIndex":1,"pageSize":20}`))
15
+ if err != nil {
16
+ t.Fatalf("DecodePage error: %v", err)
17
+ }
18
+
19
+ if page.Count != 1 || page.PageSize != 20 {
20
+ t.Fatalf("unexpected page metadata: %+v", page)
21
+ }
22
+ if len(page.Items) != 1 || page.Items[0].URL != "http://example.com/a" {
23
+ t.Fatalf("unexpected page items: %+v", page.Items)
24
+ }
25
+ }
26
+
27
+ func TestDecodeObjectCollectionArray(t *testing.T) {
28
+ t.Parallel()
29
+
30
+ items, err := DecodeObjectCollection[testItem]([]byte(`{"entries":[{"id":1},{"id":2}]}`), "entries")
31
+ if err != nil {
32
+ t.Fatalf("DecodeObjectCollection error: %v", err)
33
+ }
34
+ if len(items) != 2 || items[0].ID != 1 || items[1].ID != 2 {
35
+ t.Fatalf("unexpected array items: %+v", items)
36
+ }
37
+ }
38
+
39
+ func TestDecodeObjectCollectionObject(t *testing.T) {
40
+ t.Parallel()
41
+
42
+ items, err := DecodeObjectCollection[testItem]([]byte(`{"entries":{"b":{"id":2},"a":{"id":1}}}`), "entries")
43
+ if err != nil {
44
+ t.Fatalf("DecodeObjectCollection error: %v", err)
45
+ }
46
+ if len(items) != 2 || items[0].ID != 1 || items[1].ID != 2 {
47
+ t.Fatalf("unexpected object items order/content: %+v", items)
48
+ }
49
+ }
50
+
51
+ func TestDecodeObjectCollectionNull(t *testing.T) {
52
+ t.Parallel()
53
+
54
+ items, err := DecodeObjectCollection[testItem]([]byte(`{"entries":null}`), "entries")
55
+ if err != nil {
56
+ t.Fatalf("DecodeObjectCollection error: %v", err)
57
+ }
58
+ if len(items) != 0 {
59
+ t.Fatalf("expected empty slice, got %+v", items)
60
+ }
61
+ }
62
+
63
+ func TestDecodeStatsObject(t *testing.T) {
64
+ t.Parallel()
65
+
66
+ stats, err := DecodeStatsObject([]byte(`{"$ref":"http://example.com/stats","athlete":{"$ref":"http://example.com/athletes/1"},"splits":{"categories":[]}}`))
67
+ if err != nil {
68
+ t.Fatalf("DecodeStatsObject error: %v", err)
69
+ }
70
+
71
+ if stats.Ref != "http://example.com/stats" {
72
+ t.Fatalf("unexpected stats ref: %q", stats.Ref)
73
+ }
74
+ if stats.Athlete == nil || stats.Athlete.URL == "" {
75
+ t.Fatalf("expected athlete ref in stats object")
76
+ }
77
+ if len(stats.Splits) == 0 {
78
+ t.Fatalf("expected splits payload")
79
+ }
80
+ }
81
+
82
+ func TestExtractOptionalRef(t *testing.T) {
83
+ t.Parallel()
84
+
85
+ ref, ok, err := ExtractOptionalRef([]byte(`{"status":{"$ref":"http://example.com/status"}}`), "status")
86
+ if err != nil {
87
+ t.Fatalf("ExtractOptionalRef error: %v", err)
88
+ }
89
+ if !ok || ref == nil || ref.URL != "http://example.com/status" {
90
+ t.Fatalf("unexpected optional ref output: ok=%v ref=%+v", ok, ref)
91
+ }
92
+
93
+ nilRef, nilOk, nilErr := ExtractOptionalRef([]byte(`{"status":null}`), "status")
94
+ if nilErr != nil {
95
+ t.Fatalf("ExtractOptionalRef null error: %v", nilErr)
96
+ }
97
+ if nilOk || nilRef != nil {
98
+ t.Fatalf("expected null optional ref, got ok=%v ref=%+v", nilOk, nilRef)
99
+ }
100
+ }