feat: rank growth events with grounded evidence

This commit is contained in:
2026-08-22 18:44:59 +08:00
parent 7f9c16be4b
commit 1a8a61cfe7
20 changed files with 2555 additions and 132 deletions

View File

@@ -3,10 +3,15 @@ package eventcatalog
import (
"errors"
"fmt"
"regexp"
"strings"
"time"
)
var canonicalOccasionKeyPattern = regexp.MustCompile(`^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`)
var jalaliBreaks = [...]int{-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178}
type EventType string
type Tone string
type CommercialPosture string
@@ -41,7 +46,8 @@ const (
type Event struct {
ID string `json:"id"`
OccasionKey string `json:"occasionKey,omitempty"`
Variant string `json:"variant,omitempty"`
Variant string `json:"eventVariant,omitempty"`
JalaliYear int `json:"jalaliYear,omitempty"`
TitleFA string `json:"titleFa"`
TitleEN string `json:"titleEn,omitempty"`
Aliases []string `json:"aliases,omitempty"`
@@ -86,6 +92,7 @@ func ResolveRecurrence(rule Recurrence, year int, location *time.Location) (time
func ValidateEvents(events []Event) error {
seen := make(map[string]struct{}, len(events))
canonical := make(map[string]struct{}, len(events))
canonicalYears := make(map[string]struct{}, len(events))
for _, event := range events {
if strings.TrimSpace(event.ID) == "" || strings.TrimSpace(event.TitleFA) == "" || strings.TrimSpace(event.SourceURI) == "" || strings.TrimSpace(event.SourceVersion) == "" || strings.TrimSpace(event.InclusionRationale) == "" || strings.TrimSpace(event.GeographicScope) == "" {
return fmt.Errorf("event catalog: event %q is incomplete", event.ID)
@@ -94,8 +101,11 @@ func ValidateEvents(events []Event) error {
return fmt.Errorf("event catalog: duplicate event %q", event.ID)
}
seen[event.ID] = struct{}{}
if event.OccasionKey == "" {
event.OccasionKey = event.ID
if !canonicalOccasionKeyPattern.MatchString(event.OccasionKey) {
return fmt.Errorf("event catalog: event %q has invalid canonical occasion key", event.ID)
}
if hasYearSegment(event.OccasionKey) {
return fmt.Errorf("event catalog: event %q has year-specific occasion key", event.ID)
}
if event.Variant == "" {
event.Variant = "PRIMARY"
@@ -137,10 +147,106 @@ func ValidateEvents(events []Event) error {
return fmt.Errorf("event catalog: duplicate canonical occurrence %q", event.ID)
}
canonical[canonicalKey] = struct{}{}
if event.JalaliYear != 0 {
canonicalYearKey := fmt.Sprintf("%s\x00%s\x00%d", event.OccasionKey, event.Variant, event.JalaliYear)
if _, exists := canonicalYears[canonicalYearKey]; exists {
return fmt.Errorf("event catalog: duplicate canonical occurrence in Jalali year %d for event %q", event.JalaliYear, event.ID)
}
canonicalYears[canonicalYearKey] = struct{}{}
}
}
return nil
}
// ValidateDeclaredCoverage verifies that each year advertised by an import has
// at least one dated event. JalaliYear is explicit review metadata: it avoids
// guessing calendar boundaries from Gregorian dates during catalog approval.
func ValidateDeclaredCoverage(events []Event, startJalaliYear, endJalaliYear int) error {
if startJalaliYear <= 0 || endJalaliYear < startJalaliYear {
return errors.New("event catalog: invalid declared Jalali coverage")
}
covered := make(map[int]struct{}, endJalaliYear-startJalaliYear+1)
for _, event := range events {
if event.JalaliYear < startJalaliYear || event.JalaliYear > endJalaliYear {
return fmt.Errorf("event catalog: event %q lacks valid Jalali year within declared coverage", event.ID)
}
start, err := time.Parse(time.DateOnly, event.StartDate)
if err != nil {
return fmt.Errorf("event catalog: event %q start date: %w", event.ID, err)
}
end, err := time.Parse(time.DateOnly, event.EndDate)
if err != nil {
return fmt.Errorf("event catalog: event %q end date: %w", event.ID, err)
}
startJalaliYear := gregorianToJalaliYear(start)
endJalaliYear := gregorianToJalaliYear(end)
belongsToOccurrenceYear := startJalaliYear == event.JalaliYear || endJalaliYear == event.JalaliYear
preparesImmediateNextYear := event.Variant == "CAMPAIGN_WINDOW" && endJalaliYear+1 == event.JalaliYear && gregorianToJalaliYear(end.AddDate(0, 0, 1)) == event.JalaliYear
if !belongsToOccurrenceYear && !preparesImmediateNextYear {
return fmt.Errorf("event catalog: event %q Jalali year is inconsistent with its Gregorian date", event.ID)
}
covered[event.JalaliYear] = struct{}{}
}
for year := startJalaliYear; year <= endJalaliYear; year++ {
if _, exists := covered[year]; !exists {
return fmt.Errorf("event catalog: declared coverage has no event in Jalali year %d", year)
}
}
return nil
}
func hasYearSegment(value string) bool {
digits := 0
for _, character := range value {
if character >= '0' && character <= '9' {
digits++
if digits >= 4 {
return true
}
} else {
digits = 0
}
}
return false
}
func gregorianToJalaliYear(date time.Time) int {
jy := date.Year() - 621
firstFarvardin := gregorianDayNumber(date.Year(), 3, jalaliMarchDay(jy))
if gregorianDayNumber(date.Year(), int(date.Month()), date.Day()) < firstFarvardin {
jy--
}
return jy
}
func jalaliMarchDay(jy int) int {
gy := jy + 621
leapJ := -14
previousBreak := jalaliBreaks[0]
jump := 0
for index := 1; index < len(jalaliBreaks); index++ {
currentBreak := jalaliBreaks[index]
jump = currentBreak - previousBreak
if jy < currentBreak {
break
}
leapJ += jump/33*8 + (jump%33)/4
previousBreak = currentBreak
}
n := jy - previousBreak
leapJ += n/33*8 + (n%33+3)/4
if jump%33 == 4 && jump-n == 4 {
leapJ++
}
leapG := gy/4 - ((gy/100+1)*3)/4 - 150
return 20 + leapJ - leapG
}
func gregorianDayNumber(gy, gm, gd int) int {
dayNumber := ((gy+(gm-8)/6+100100)*1461)/4 + (153*((gm+9)%12)+2)/5 + gd - 34840408
return dayNumber - (((gy+100100+(gm-8)/6)/100)*3)/4 + 752
}
func nthWeekday(year int, month time.Month, weekday time.Weekday, occurrence int, location *time.Location) time.Time {
first := time.Date(year, month, 1, 0, 0, 0, 0, location)
offset := (int(weekday) - int(first.Weekday()) + 7) % 7

View File

@@ -21,8 +21,8 @@ func TestValidateEventsDistinguishesCelebrationAndMourning(t *testing.T) {
t.Parallel()
events := []Event{
{ID: "nowruz-1405", TitleFA: "نوروز", StartDate: "2026-03-21", EndDate: "2026-03-24", Type: TypeCultural, Tone: ToneCelebratory, CommercialPosture: PosturePromote, SourceURI: "https://calendar.ut.ac.ir/", SourceVersion: "1405", InclusionRationale: "تقویم رسمی", GeographicScope: "IR", RelevanceBPS: 10000, ConfidenceBPS: 10000},
{ID: "ashura-1405", TitleFA: "عاشورای حسینی", StartDate: "2026-06-25", EndDate: "2026-06-25", Type: TypeReligious, Tone: ToneMourning, CommercialPosture: PostureNoPromotion, SourceURI: "https://calendar.ut.ac.ir/", SourceVersion: "1405", InclusionRationale: "تقویم رسمی", GeographicScope: "IR", RelevanceBPS: 10000, ConfidenceBPS: 10000},
{ID: "nowruz-1405", OccasionKey: "nowruz", TitleFA: "نوروز", StartDate: "2026-03-21", EndDate: "2026-03-24", Type: TypeCultural, Tone: ToneCelebratory, CommercialPosture: PosturePromote, SourceURI: "https://calendar.ut.ac.ir/", SourceVersion: "1405", InclusionRationale: "تقویم رسمی", GeographicScope: "IR", RelevanceBPS: 10000, ConfidenceBPS: 10000},
{ID: "ashura-1405", OccasionKey: "ashura", TitleFA: "عاشورای حسینی", StartDate: "2026-06-25", EndDate: "2026-06-25", Type: TypeReligious, Tone: ToneMourning, CommercialPosture: PostureNoPromotion, SourceURI: "https://calendar.ut.ac.ir/", SourceVersion: "1405", InclusionRationale: "تقویم رسمی", GeographicScope: "IR", RelevanceBPS: 10000, ConfidenceBPS: 10000},
}
if err := ValidateEvents(events); err != nil {
t.Fatal(err)
@@ -32,8 +32,76 @@ func TestValidateEventsDistinguishesCelebrationAndMourning(t *testing.T) {
func TestValidateEventsRejectsPromotionalMourningEvent(t *testing.T) {
t.Parallel()
event := Event{ID: "mourning", TitleFA: "یادبود", StartDate: "2026-01-01", EndDate: "2026-01-01", Type: TypeCultural, Tone: ToneMourning, CommercialPosture: PosturePromote, SourceURI: "https://example.test", SourceVersion: "v1", InclusionRationale: "reviewed", GeographicScope: "IR", RelevanceBPS: 10000, ConfidenceBPS: 10000}
event := Event{ID: "mourning", OccasionKey: "mourning", TitleFA: "یادبود", StartDate: "2026-01-01", EndDate: "2026-01-01", Type: TypeCultural, Tone: ToneMourning, CommercialPosture: PosturePromote, SourceURI: "https://example.test", SourceVersion: "v1", InclusionRationale: "reviewed", GeographicScope: "IR", RelevanceBPS: 10000, ConfidenceBPS: 10000}
if err := ValidateEvents([]Event{event}); err == nil {
t.Fatal("expected mourning promotion validation error")
}
}
func TestValidateEventsRejectsYearSpecificOccasionKey(t *testing.T) {
t.Parallel()
event := validCatalogEvent("black-friday-2026", "2026-11-27", 1405)
event.OccasionKey = "black-friday-2026"
event.Recurrence = RecurrenceBlackFriday
if err := ValidateEvents([]Event{event}); err == nil {
t.Fatal("expected year-specific occasion key validation error")
}
}
func TestValidateEventsRejectsNonCanonicalOccasionKey(t *testing.T) {
t.Parallel()
event := validCatalogEvent("black-friday-2026", "2026-11-27", 1405)
event.OccasionKey = "Black_Friday_2026a"
if err := ValidateEvents([]Event{event}); err == nil {
t.Fatal("expected non-canonical occasion key validation error")
}
}
func TestValidateEventsRejectsDuplicateOccasionVariantInJalaliYear(t *testing.T) {
t.Parallel()
first := validCatalogEvent("yalda-first-1405", "2026-12-12", 1405)
first.OccasionKey = "yalda"
first.Variant = "CAMPAIGN_WINDOW"
second := validCatalogEvent("yalda-second-1405", "2026-12-18", 1405)
second.OccasionKey = first.OccasionKey
second.Variant = first.Variant
if err := ValidateEvents([]Event{first, second}); err == nil {
t.Fatal("expected duplicate occasion variant validation error")
}
}
func TestValidateDeclaredCoverageUsesActualJalaliYear(t *testing.T) {
t.Parallel()
event := validCatalogEvent("eid-al-fitr-1406", "2027-03-10", 1406)
event.OccasionKey = "eid-al-fitr"
event.Variant = "OPERATIONAL_WINDOW"
if err := ValidateDeclaredCoverage([]Event{event}, 1406, 1406); err == nil {
t.Fatal("expected Gregorian date in Jalali 1405 to be rejected as 1406")
}
}
func TestValidateDeclaredCoverageAllowsPreparationWindowEndingBeforeOccasionYear(t *testing.T) {
t.Parallel()
event := validCatalogEvent("nowruz-demand-window-1405", "2026-02-20", 1405)
event.OccasionKey = "nowruz"
event.Variant = "CAMPAIGN_WINDOW"
event.EndDate = "2026-03-20"
if err := ValidateDeclaredCoverage([]Event{event}, 1405, 1405); err != nil {
t.Fatal(err)
}
}
func validCatalogEvent(id, date string, jalaliYear int) Event {
return Event{
ID: id, OccasionKey: "black-friday", Variant: "PRIMARY", JalaliYear: jalaliYear,
TitleFA: "رویداد", StartDate: date, EndDate: date, Type: TypeCommercial,
Tone: ToneNeutral, CommercialPosture: PostureCaution, SourceURI: "https://example.test/event",
SourceVersion: "reviewed", InclusionRationale: "reviewed", GeographicScope: "IR",
RelevanceBPS: 8000, ConfidenceBPS: 8000,
}
}

View File

@@ -3,12 +3,13 @@ package eventcatalog
import (
"encoding/json"
"os"
"strings"
"testing"
"time"
)
func TestImpactfulBusinessCatalogIsValidAndIndustryPersonalized(t *testing.T) {
data, err := os.ReadFile("../../catalogs/impactful-business-events-1405-v1.json")
data, err := os.ReadFile("../../catalogs/impactful-business-events-1405-v2.json")
if err != nil {
t.Fatal(err)
}
@@ -42,3 +43,77 @@ func TestImpactfulBusinessCatalogIsValidAndIndustryPersonalized(t *testing.T) {
t.Fatalf("catalog must provide year-round candidate coverage; got %d events across %d months", len(input.Events), len(months))
}
}
func TestImpactfulBusinessCatalogLinksExactHistoricalOccurrences(t *testing.T) {
data, err := os.ReadFile("../../catalogs/impactful-business-events-1405-v2.json")
if err != nil {
t.Fatal(err)
}
var input Import
if err := json.Unmarshal(data, &input); err != nil {
t.Fatal(err)
}
required := []struct {
occasionKey string
variant string
}{
{"education-enrollment", "CAMPAIGN_WINDOW"},
{"black-friday", "PRIMARY"},
{"cyber-monday", "PRIMARY"},
{"yalda", "CAMPAIGN_WINDOW"},
{"nowruz", "CAMPAIGN_WINDOW"},
{"nowruz", "OPERATIONAL_WINDOW"},
}
for _, identity := range required {
years := make(map[int]bool)
for _, event := range input.Events {
if event.OccasionKey == identity.occasionKey && event.Variant == identity.variant {
years[event.JalaliYear] = true
}
}
for year := 1402; year <= 1405; year++ {
if !years[year] {
t.Errorf("%s/%s lacks occurrence for %d", identity.occasionKey, identity.variant, year)
}
}
}
}
func TestImpactfulBusinessCatalogDoesNotMislabelGregorian2027AsJalali1406(t *testing.T) {
data, err := os.ReadFile("../../catalogs/impactful-business-events-1405-v2.json")
if err != nil {
t.Fatal(err)
}
var input Import
if err := json.Unmarshal(data, &input); err != nil {
t.Fatal(err)
}
for _, event := range input.Events {
if (event.OccasionKey == "ramadan" || event.OccasionKey == "eid-al-fitr") && event.JalaliYear == 1406 {
t.Errorf("%s is before Nowruz 1406 and must not be labeled as Jalali 1406", event.ID)
}
}
}
func TestImpactfulBusinessCatalogAttributesCustomWindowsToInternalPolicy(t *testing.T) {
data, err := os.ReadFile("../../catalogs/impactful-business-events-1405-v2.json")
if err != nil {
t.Fatal(err)
}
var input Import
if err := json.Unmarshal(data, &input); err != nil {
t.Fatal(err)
}
customWindows := map[string]bool{
"education-enrollment": true,
"yalda": true,
"nowruz": true,
"ramadan": true,
}
for _, event := range input.Events {
if event.Variant == "CAMPAIGN_WINDOW" && customWindows[event.OccasionKey] && !strings.HasPrefix(event.SourceURI, "urn:zarinpal:event-window-policy:") {
t.Errorf("%s custom window must cite the internal derivation policy, got %q", event.ID, event.SourceURI)
}
}
}

View File

@@ -32,7 +32,10 @@ func ValidateImport(input Import, currentJalaliYear int) error {
if len(input.Events) == 0 {
return errors.New("event catalog: import contains no events")
}
return ValidateEvents(input.Events)
if err := ValidateEvents(input.Events); err != nil {
return err
}
return ValidateDeclaredCoverage(input.Events, input.CoverageStartJalaliYear, input.CoverageEndJalaliYear)
}
func ValidateApproval(importedBy, reviewedBy string) error {
@@ -45,11 +48,22 @@ func ValidateApproval(importedBy, reviewedBy string) error {
return nil
}
func ValidateReviewableChecksum(checksum string) error {
normalized := strings.ToLower(strings.TrimSpace(checksum))
if normalized == "" || strings.Contains(normalized, "review-required") || strings.Contains(normalized, "pending-review") || strings.Contains(normalized, "placeholder") {
return errors.New("event catalog: source checksum is not independently reviewed")
}
return nil
}
type Repository struct{ pool *pgxpool.Pool }
func NewRepository(pool *pgxpool.Pool) *Repository { return &Repository{pool: pool} }
func (r *Repository) ImportDraft(ctx context.Context, input Import) error {
if err := ValidateEvents(input.Events); err != nil {
return err
}
tx, err := r.pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return fmt.Errorf("starting calendar import: %w", err)
@@ -62,9 +76,6 @@ func (r *Repository) ImportDraft(ctx context.Context, input Import) error {
return fmt.Errorf("creating calendar catalog version: %w", err)
}
for _, event := range input.Events {
if event.OccasionKey == "" {
event.OccasionKey = event.ID
}
if event.Variant == "" {
event.Variant = "PRIMARY"
}
@@ -111,12 +122,16 @@ func (r *Repository) Approve(ctx context.Context, version, reviewer string) erro
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
var id int64
var importedBy string
if err := tx.QueryRow(ctx, `SELECT id,imported_by FROM calendar_catalog_versions WHERE version=$1 AND status='DRAFT' FOR UPDATE`, version).Scan(&id, &importedBy); err != nil {
var sourceChecksum string
if err := tx.QueryRow(ctx, `SELECT id,imported_by,source_checksum FROM calendar_catalog_versions WHERE version=$1 AND status='DRAFT' FOR UPDATE`, version).Scan(&id, &importedBy, &sourceChecksum); err != nil {
return fmt.Errorf("loading draft calendar catalog: %w", err)
}
if err := ValidateApproval(importedBy, reviewer); err != nil {
return err
}
if err := ValidateReviewableChecksum(sourceChecksum); err != nil {
return err
}
if _, err := tx.Exec(ctx, `UPDATE calendar_catalog_versions SET status='APPROVED',reviewed_by=$2,reviewed_at=now() WHERE id=$1`, id, reviewer); err != nil {
return fmt.Errorf("approving calendar catalog: %w", err)
}
@@ -136,9 +151,13 @@ func (r *Repository) Activate(ctx context.Context, version, actor string) error
return fmt.Errorf("locking calendar activation: %w", err)
}
var id int64
if err := tx.QueryRow(ctx, `SELECT id FROM calendar_catalog_versions WHERE version=$1 AND status='APPROVED' FOR UPDATE`, version).Scan(&id); err != nil {
var sourceChecksum string
if err := tx.QueryRow(ctx, `SELECT id,source_checksum FROM calendar_catalog_versions WHERE version=$1 AND status='APPROVED' FOR UPDATE`, version).Scan(&id, &sourceChecksum); err != nil {
return fmt.Errorf("loading approved calendar catalog: %w", err)
}
if err := ValidateReviewableChecksum(sourceChecksum); err != nil {
return err
}
if _, err := tx.Exec(ctx, `UPDATE calendar_catalog_versions SET status='RETIRED' WHERE status='ACTIVE'`); err != nil {
return fmt.Errorf("retiring active calendar catalog: %w", err)
}

View File

@@ -1,11 +1,14 @@
package eventcatalog
import "testing"
import (
"fmt"
"testing"
)
func TestValidateImportRequiresThreePastAndTwoFutureJalaliYears(t *testing.T) {
t.Parallel()
input := Import{Version: "1402-1407-v1", CoverageStartJalaliYear: 1402, CoverageEndJalaliYear: 1407, SourceURI: "https://calendar.ut.ac.ir/", SourceVersion: "reviewed", SourceChecksum: "abc", ImportedBy: "operator-a", Events: []Event{{ID: "nowruz", TitleFA: "نوروز", StartDate: "2026-03-21", EndDate: "2026-03-24", Type: TypeCultural, Tone: ToneCelebratory, CommercialPosture: PosturePromote, SourceURI: "https://calendar.ut.ac.ir/", SourceVersion: "1405", InclusionRationale: "تقویم رسمی", GeographicScope: "IR", RelevanceBPS: 10000, ConfidenceBPS: 10000}}}
input := validCoveredImport()
if err := ValidateImport(input, 1405); err != nil {
t.Fatal(err)
}
@@ -16,6 +19,39 @@ func TestValidateImportRequiresThreePastAndTwoFutureJalaliYears(t *testing.T) {
}
}
func TestValidateImportRejectsMisleadingDeclaredCoverage(t *testing.T) {
t.Parallel()
input := validCoveredImport()
input.Events = input.Events[:len(input.Events)-1]
if err := ValidateImport(input, 1405); err == nil {
t.Fatal("expected missing declared coverage year error")
}
}
func TestValidateImportRequiresExplicitOccasionKey(t *testing.T) {
t.Parallel()
input := validCoveredImport()
input.Events[0].OccasionKey = ""
if err := ValidateImport(input, 1405); err == nil {
t.Fatal("expected missing occasion key error")
}
}
func TestValidateReviewableChecksumRejectsPlaceholder(t *testing.T) {
t.Parallel()
for _, checksum := range []string{"review-required-before-activation-1405-v2", "pending-review", "placeholder"} {
if err := ValidateReviewableChecksum(checksum); err == nil {
t.Errorf("expected placeholder checksum %q to be rejected", checksum)
}
}
if err := ValidateReviewableChecksum("sha256:5f70bf18a086007016e948b04aed3b82103a36be3a0ab2aeeabe46d778b8bdc0"); err != nil {
t.Fatal(err)
}
}
func TestValidateApprovalRequiresIndependentReviewer(t *testing.T) {
t.Parallel()
@@ -26,3 +62,15 @@ func TestValidateApprovalRequiresIndependentReviewer(t *testing.T) {
t.Fatal(err)
}
}
func validCoveredImport() Import {
events := make([]Event, 0, 6)
for year := 1402; year <= 1407; year++ {
gregorianYear := year + 621
event := validCatalogEvent(fmt.Sprintf("education-enrollment-%d", year), fmt.Sprintf("%d-09-01", gregorianYear), year)
event.OccasionKey = "education-enrollment"
event.Variant = "CAMPAIGN_WINDOW"
events = append(events, event)
}
return Import{Version: "1402-1407-v2", CoverageStartJalaliYear: 1402, CoverageEndJalaliYear: 1407, SourceURI: "https://calendar.ut.ac.ir/", SourceVersion: "reviewed", SourceChecksum: "abc", ImportedBy: "operator-a", Events: events}
}

View File

@@ -3,7 +3,7 @@ package eventcatalog
import "testing"
func TestValidateEventsRejectsIncorrectBlackFridayDate(t *testing.T) {
event := Event{ID: "black-friday-2026", TitleFA: "جمعه سیاه", StartDate: "2026-11-26", EndDate: "2026-11-26", Recurrence: RecurrenceBlackFriday, Type: TypeCommercial, Tone: ToneNeutral, CommercialPosture: PostureCaution, SourceURI: "https://example.test/black-friday", SourceVersion: "2026", InclusionRationale: "رفتار تجاری مرتبط", GeographicScope: "IR", RelevanceBPS: 8000, ConfidenceBPS: 8000}
event := Event{ID: "black-friday-2026", OccasionKey: "black-friday", TitleFA: "جمعه سیاه", StartDate: "2026-11-26", EndDate: "2026-11-26", Recurrence: RecurrenceBlackFriday, Type: TypeCommercial, Tone: ToneNeutral, CommercialPosture: PostureCaution, SourceURI: "https://example.test/black-friday", SourceVersion: "2026", InclusionRationale: "رفتار تجاری مرتبط", GeographicScope: "IR", RelevanceBPS: 8000, ConfidenceBPS: 8000}
if err := ValidateEvents([]Event{event}); err == nil {
t.Fatal("expected recurrence mismatch")
}