Darkmatter · agent stage: prod
preview@internal.darkmatter
Events received
184870
Ignored
184547
Jobs dispatched
329

Event ba9c77a0…

← All events

Event

Delivery
ba9c77a0-83e2-11f1-8ac7-47f556769824
Event
pull_request_review_comment
Action
created
Received
2026-07-20T02:28:45.062Z
Signature
valid
Parsed
yes
Sender
chatgpt-codex-connector[bot]
Repo
voytravel/voy
Status
ignored — bot_or_missing_actor

Headers

{
  "accept": "*/*",
  "accept-encoding": "gzip, br",
  "cf-connecting-ip": "140.82.115.242",
  "cf-ipcountry": "US",
  "cf-ray": "a1de88c54c6ec98f",
  "cf-visitor": "{\"scheme\":\"https\"}",
  "connection": "Keep-Alive",
  "content-length": "47326",
  "content-type": "application/json",
  "host": "github-bot.darkmatter.io",
  "user-agent": "GitHub-Hookshot/11cb4cc",
  "x-forwarded-proto": "https",
  "x-github-delivery": "ba9c77a0-83e2-11f1-8ac7-47f556769824",
  "x-github-event": "pull_request_review_comment",
  "x-github-hook-id": "628737947",
  "x-github-hook-installation-target-id": "3663660",
  "x-github-hook-installation-target-type": "integration",
  "x-real-ip": "140.82.115.242"
}

Payload

{
  "action": "created",
  "comment": {
    "url": "https://api.github.com/repos/voytravel/voy/pulls/comments/3611762868",
    "pull_request_review_id": 4731872457,
    "id": 3611762868,
    "node_id": "PRRC_kwDOPLnB3s7XRyC0",
    "diff_hunk": "@@ -0,0 +1,618 @@\n+import {\n+  canonicalPlaceEditorialLinks,\n+  canonicalPlaces,\n+  editorialReconciliationState,\n+} from \"@voytravel/db/schema\";\n+import { eq } from \"drizzle-orm\";\n+import type { PlacesEnrichmentDb } from \"./db.js\";\n+\n+export type MatchMethod = \"google_place_id\" | \"normalized_name_geo\" | \"manual\";\n+\n+export type ReconcileOptions = {\n+  sourceKeys: string[];\n+  city: string | null;\n+  countryCode: string | null;\n+  category: string | null;\n+  limit: number;\n+  write: boolean;\n+  changedSince: Date | null;\n+};\n+\n+export type EditorialPlaceRow = {\n+  id: string;\n+  primary_name: string;\n+  normalized_primary_name: string;\n+  category: string;\n+  subcategories: unknown;\n+  city: string | null;\n+  region: string | null;\n+  country_code: string | null;\n+  lat: string | null;\n+  lng: string | null;\n+  google_place_id: string | null;\n+  google_maps_uri: string | null;\n+  metadata: Record<string, unknown> | null;\n+  last_refreshed_at: Date;\n+};\n+\n+type SourceLinkRow = {\n+  place_id: string;\n+  key: string;\n+  label: string;\n+};\n+\n+type CanonicalPlaceRow = typeof canonicalPlaces.$inferSelect;\n+\n+export type SourceInfo = {\n+  sourceKeys: string[];\n+  sourceLabels: string[];\n+};\n+\n+export type Summary = {\n+  dryRun: boolean;\n+  changedSince: Date | null;\n+  maxSourceRefreshedAt: Date | null;\n+  recordsRead: number;\n+  canonicalCreated: number;\n+  canonicalUpdated: number;\n+  editorialLinksUpserted: number;\n+  skippedAlreadyLinked: number;\n+};\n+\n+export type EditorialReconciliationStateRow =\n+  typeof editorialReconciliationState.$inferSelect;\n+\n+const ADVISORY_LOCK_NAMESPACE = 4313;\n+const ADVISORY_LOCK_RESOURCE = 1;\n+\n+export class EditorialReconciliationLockError extends Error {\n+  constructor() {\n+    super(\"Editorial reconciliation is already running\");\n+    this.name = \"EditorialReconciliationLockError\";\n+  }\n+}\n+\n+export async function runEditorialReconciliation(\n+  db: PlacesEnrichmentDb,\n+  options: ReconcileOptions,\n+): Promise<Summary> {\n+  if (!options.write) {\n+    return runEditorialReconciliationBody(db, options);\n+  }\n+\n+  const lockAcquired = await tryAcquireEditorialReconciliationLock(db);\n+  if (!lockAcquired) {\n+    throw new EditorialReconciliationLockError();\n+  }\n+\n+  try {\n+    return await runEditorialReconciliationBody(db, options);\n+  } finally {\n+    await releaseEditorialReconciliationLock(db);\n+  }\n+}\n+\n+export async function getEditorialReconciliationState(\n+  db: PlacesEnrichmentDb,\n+  key = \"default\",\n+): Promise<EditorialReconciliationStateRow | null> {\n+  return (\n+    (await db.query.editorialReconciliationState.findFirst({\n+      where: eq(editorialReconciliationState.key, key),\n+    })) ?? null\n+  );\n+}\n+\n+export async function putEditorialReconciliationState(\n+  db: PlacesEnrichmentDb,\n+  input: {\n+    key?: string;\n+    lastSourceRefreshAt: Date | null;\n+  },\n+): Promise<void> {\n+  const key = input.key ?? \"default\";\n+  await db\n+    .insert(editorialReconciliationState)\n+    .values({\n+      key,\n+      lastSourceRefreshAt: input.lastSourceRefreshAt,\n+    })\n+    .onConflictDoUpdate({\n+      target: [editorialReconciliationState.key],\n+      set: {\n+        lastSourceRefreshAt: input.lastSourceRefreshAt,\n+        updatedAt: new Date(),\n+      },\n+    });\n+}\n+\n+export async function fetchEditorialPlaces(\n+  db: PlacesEnrichmentDb,\n+  options: ReconcileOptions,\n+): Promise<EditorialPlaceRow[]> {\n+  const conditions = [\n+    \"s.is_active = true\",\n+    \"r.record_status = 'active'\",\n+    \"s.key = any($1::text[])\",\n+  ];\n+  const values: Array<string[] | string | number | Date> = [options.sourceKeys];\n+\n+  if (options.city) {\n+    values.push(options.city);\n+    conditions.push(`p.city = $${values.length}`);\n+  }\n+  if (options.countryCode) {\n+    values.push(options.countryCode);\n+    conditions.push(`p.country_code = $${values.length}`);\n+  }\n+  if (options.category) {\n+    values.push(options.category);\n+    conditions.push(`p.category::text = $${values.length}`);\n+  }\n+  if (options.changedSince) {\n+    values.push(options.changedSince);\n+    conditions.push(`p.last_refreshed_at >= $${values.length}`);\n+  }\n+\n+  values.push(options.limit);\n+  const limitPosition = values.length;\n+\n+  const result = await db.$client.query<EditorialPlaceRow>(\n+    `\n+      select distinct\n+        p.id::text as id,\n+        p.primary_name,\n+        p.normalized_primary_name,\n+        p.category::text as category,\n+        p.subcategories,\n+        p.city,\n+        p.region,\n+        p.country_code,\n+        p.lat::text as lat,\n+        p.lng::text as lng,\n+        p.google_place_id,\n+        p.google_maps_uri,\n+        p.metadata,\n+        p.last_refreshed_at\n+      from recommendation_places p\n+      inner join recommendation_place_sources ps on ps.place_id = p.id\n+      inner join recommendation_sources s on s.id = ps.source_id\n+      inner join recommendation_source_records r on r.id = ps.source_record_id\n+      where ${conditions.join(\" and \")}\n+      order by p.last_refreshed_at desc, p.id desc\n+      limit $${limitPosition}\n+    `,\n+    values,\n+  );\n+\n+  return result.rows;\n+}\n+\n+export async function fetchSourceInfoByPlaceId(\n+  db: PlacesEnrichmentDb,\n+  placeIds: string[],\n+): Promise<Map<string, SourceInfo>> {\n+  if (placeIds.length === 0) {\n+    return new Map();\n+  }\n+\n+  const result = await db.$client.query<SourceLinkRow>(\n+    `\n+      select\n+        ps.place_id::text as place_id,\n+        s.key,\n+        s.label\n+      from recommendation_place_sources ps\n+      inner join recommendation_sources s on s.id = ps.source_id\n+      inner join recommendation_source_records r on r.id = ps.source_record_id\n+      where ps.place_id = any($1::uuid[])\n+        and s.is_active = true\n+        and r.record_status = 'active'\n+      order by ps.place_id, s.label\n+    `,\n+    [placeIds],\n+  );\n+\n+  const sourceInfoByPlaceId = new Map<string, SourceInfo>();\n+  for (const row of result.rows) {\n+    const current = sourceInfoByPlaceId.get(row.place_id) ?? {\n+      sourceKeys: [],\n+      sourceLabels: [],\n+    };\n+    if (!current.sourceKeys.includes(row.key)) {\n+      current.sourceKeys.push(row.key);\n+    }\n+    if (!current.sourceLabels.includes(row.label)) {\n+      current.sourceLabels.push(row.label);\n+    }\n+    sourceInfoByPlaceId.set(row.place_id, current);\n+  }\n+\n+  return sourceInfoByPlaceId;\n+}\n+\n+export async function findExistingCanonicalPlace(\n+  db: PlacesEnrichmentDb,\n+  row: EditorialPlaceRow,\n+): Promise<CanonicalPlaceRow | null> {\n+  if (row.google_place_id) {\n+    const match = await db.query.canonicalPlaces.findFirst({\n+      where: eq(canonicalPlaces.googlePlaceId, row.google_place_id),\n+    });\n+    if (match) {\n+      return match;\n+    }\n+  }\n+\n+  const lat = decimalToNumber(row.lat);\n+  const lng = decimalToNumber(row.lng);\n+  if (lat !== null && lng !== null) {\n+    const geoMatch = await db.$client.query<CanonicalPlaceRow>(\n+      `\n+        select *\n+        from canonical_places\n+        where normalized_primary_name = $1\n+          and (\n+            ($2::text is null and city is null)\n+            or city = $2\n+          )\n+          and (\n+            ($3::text is null and country_code is null)\n+            or country_code = $3\n+          )\n+          and abs(lat::float - $4) <= 0.002\n+          and abs(lng::float - $5) <= 0.002\n+        limit 1\n+      `,\n+      [row.normalized_primary_name, row.city, row.country_code, lat, lng],\n+    );\n+    if (geoMatch.rows[0]) {\n+      return geoMatch.rows[0];\n+    }\n+  }\n+\n+  if (row.city || row.country_code) {\n+    const coarseMatch = await db.$client.query<CanonicalPlaceRow>(\n+      `\n+        select *\n+        from canonical_places\n+        where normalized_primary_name = $1\n+          and (\n+            ($2::text is null and city is null)\n+            or city = $2\n+          )\n+          and (\n+            ($3::text is null and country_code is null)\n+            or country_code = $3\n+          )\n+        limit 2\n+      `,\n+      [row.normalized_primary_name, row.city, row.country_code],\n+    );\n+    if (coarseMatch.rows.length === 1) {\n+      return coarseMatch.rows[0] ?? null;\n+    }\n+  }\n+\n+  return null;\n+}\n+\n+export async function createCanonicalPlace(\n+  db: PlacesEnrichmentDb,\n+  row: EditorialPlaceRow,\n+  sourceInfo: SourceInfo,\n+): Promise<string> {\n+  const placeTypes = buildPlaceTypes(row);\n+  const metadata = buildCanonicalMetadata(row, null, sourceInfo);\n+  const formattedAddress = buildFormattedAddress(row);\n+  const websiteUri = readWebsiteUri(row.metadata);\n+  const rating = readRating(row.metadata);\n+  const reviewCount = readReviewCount(row.metadata);\n+  const now = new Date();\n+\n+  const [inserted] = await db\n+    .insert(canonicalPlaces)\n+    .values({\n+      primaryName: row.primary_name,\n+      normalizedPrimaryName: row.normalized_primary_name,\n+      formattedAddress,\n+      city: row.city,\n+      region: row.region,\n+      countryCode: row.country_code,\n+      lat: row.lat,\n+      lng: row.lng,\n+      googlePlaceId: row.google_place_id,\n+      googleMapsUri: row.google_maps_uri,\n+      websiteUri,\n+      rating,\n+      reviewCount,\n+      placeTypes,\n+      googleRaw: null,\n+      osmRaw: null,\n+      metadata,\n+      lastFetchedAt: now,\n+    })\n+    .returning({ id: canonicalPlaces.id });\n+\n+  if (!inserted?.id) {\n+    throw new Error(\n+      `Failed to create canonical place for editorial place ${row.id}`,\n+    );\n+  }\n+\n+  return inserted.id;\n+}\n+\n+export async function updateCanonicalPlace(\n+  db: PlacesEnrichmentDb,\n+  existing: CanonicalPlaceRow,\n+  row: EditorialPlaceRow,\n+  sourceInfo: SourceInfo,\n+): Promise<string> {\n+  const placeTypes = Array.from(\n+    new Set([...(existing.placeTypes ?? []), ...buildPlaceTypes(row)]),\n+  );\n+  const metadata = buildCanonicalMetadata(row, existing.metadata, sourceInfo);\n+\n+  await db\n+    .update(canonicalPlaces)\n+    .set({\n+      primaryName: existing.primaryName || row.primary_name,\n+      normalizedPrimaryName:\n+        existing.normalizedPrimaryName || row.normalized_primary_name,\n+      formattedAddress: existing.formattedAddress || buildFormattedAddress(row),\n+      city: existing.city || row.city,\n+      region: existing.region || row.region,\n+      countryCode: existing.countryCode || row.country_code,\n+      lat: existing.lat || row.lat,\n+      lng: existing.lng || row.lng,\n+      googlePlaceId: existing.googlePlaceId || row.google_place_id,\n+      googleMapsUri: existing.googleMapsUri || row.google_maps_uri,\n+      websiteUri: existing.websiteUri || readWebsiteUri(row.metadata),\n+      rating: existing.rating || readRating(row.metadata),\n+      reviewCount: existing.reviewCount ?? readReviewCount(row.metadata),\n+      placeTypes,\n+      metadata,\n+      lastFetchedAt: new Date(),\n+      updatedAt: new Date(),\n+    })\n+    .where(eq(canonicalPlaces.id, existing.id));\n+\n+  return existing.id;\n+}\n+\n+export function determineMatchMethod(\n+  existingCanonical: CanonicalPlaceRow | null,\n+  row: EditorialPlaceRow,\n+): MatchMethod {\n+  if (row.google_place_id) {\n+    return \"google_place_id\";\n+  }\n+  if (existingCanonical && row.lat && row.lng) {\n+    return \"normalized_name_geo\";\n+  }\n+  return \"manual\";\n+}\n+\n+export function buildPlaceTypes(row: EditorialPlaceRow): string[] {\n+  const subcategories = Array.isArray(row.subcategories)\n+    ? row.subcategories.filter(\n+        (entry): entry is string => typeof entry === \"string\",\n+      )\n+    : [];\n+  return Array.from(new Set([row.category, ...subcategories].filter(Boolean)));\n+}\n+\n+export function buildCanonicalMetadata(\n+  row: EditorialPlaceRow,\n+  existing: Record<string, unknown> | null,\n+  sourceInfo: SourceInfo,\n+): Record<string, unknown> {\n+  const nextMetadata: Record<string, unknown> = { ...(existing ?? {}) };\n+  nextMetadata.editorial = {\n+    recommendationPlaceId: row.id,\n+    category: row.category,\n+    sourceKeys: sourceInfo.sourceKeys,\n+    sourceLabels: sourceInfo.sourceLabels,\n+    photoUrl: readPhotoUrl(row.metadata),\n+    lastReconciledAt: new Date().toISOString(),\n+  };\n+  return nextMetadata;\n+}\n+\n+export function buildFormattedAddress(row: EditorialPlaceRow): string | null {\n+  const parts = [row.city, row.region, row.country_code].filter(\n+    (value): value is string =>\n+      typeof value === \"string\" && value.trim().length > 0,\n+  );\n+  return parts.length > 0 ? parts.join(\", \") : null;\n+}\n+\n+export function readWebsiteUri(\n+  metadata: Record<string, unknown> | null,\n+): string | null {\n+  return (\n+    cleanStringValue(metadata?.websiteUri) ??\n+    cleanStringValue(metadata?.website_uri) ??\n+    cleanStringValue(metadata?.websiteUrl) ??\n+    cleanStringValue(metadata?.website_url)\n+  );\n+}\n+\n+export function readPhotoUrl(\n+  metadata: Record<string, unknown> | null,\n+): string | null {\n+  return (\n+    cleanStringValue(metadata?.photoUrl) ??\n+    cleanStringValue(metadata?.photo_url) ??\n+    cleanStringValue(metadata?.imageUrl) ??\n+    cleanStringValue(metadata?.image_url)\n+  );\n+}\n+\n+export function readRating(\n+  metadata: Record<string, unknown> | null,\n+): string | null {\n+  return formatNumericDecimal(\n+    getNumericValue(metadata?.rating) ??\n+      getNumericValue(metadata?.google_rating),\n+    2,\n+  );\n+}\n+\n+export function readReviewCount(\n+  metadata: Record<string, unknown> | null,\n+): number | null {\n+  const numeric =\n+    getNumericValue(metadata?.reviewCount) ??\n+    getNumericValue(metadata?.review_count) ??\n+    getNumericValue(metadata?.google_review_count);\n+  return numeric === null ? null : Math.round(numeric);\n+}\n+\n+export function decimalToNumber(\n+  value: string | null | undefined,\n+): number | null {\n+  if (!value) return null;\n+  const numeric = Number(value);\n+  return Number.isFinite(numeric) ? numeric : null;\n+}\n+\n+export function formatNumericDecimal(\n+  value: number | null,\n+  precision: number,\n+): string | null {\n+  if (value === null || !Number.isFinite(value)) return null;\n+  return value.toFixed(precision);\n+}\n+\n+export function getNumericValue(value: unknown): number | null {\n+  if (typeof value === \"number\" && Number.isFinite(value)) {\n+    return value;\n+  }\n+  if (typeof value === \"string\" && value.trim()) {\n+    const parsed = Number(value);\n+    return Number.isFinite(parsed) ? parsed : null;\n+  }\n+  return null;\n+}\n+\n+export function cleanStringValue(value: unknown): string | null {\n+  return typeof value === \"string\" && value.trim().length > 0\n+    ? value.trim()\n+    : null;\n+}\n+\n+export function splitCsv(value: string | undefined): string[] {\n+  return (value ?? \"\")\n+    .split(\",\")\n+    .map((entry) => entry.trim())\n+    .filter(Boolean);\n+}\n+\n+async function runEditorialReconciliationBody(\n+  db: PlacesEnrichmentDb,\n+  options: ReconcileOptions,\n+): Promise<Summary> {\n+  const editorialPlaces = await fetchEditorialPlaces(db, options);\n+  const sourceInfoByPlaceId = await fetchSourceInfoByPlaceId(\n+    db,\n+    editorialPlaces.map((row) => row.id),\n+  );\n+\n+  const summary: Summary = {\n+    dryRun: !options.write,\n+    changedSince: options.changedSince,\n+    maxSourceRefreshedAt: null,\n+    recordsRead: editorialPlaces.length,\n+    canonicalCreated: 0,\n+    canonicalUpdated: 0,\n+    editorialLinksUpserted: 0,\n+    skippedAlreadyLinked: 0,\n+  };\n+\n+  for (const row of editorialPlaces) {\n+    if (\n+      !summary.maxSourceRefreshedAt ||\n+      row.last_refreshed_at > summary.maxSourceRefreshedAt\n+    ) {\n+      summary.maxSourceRefreshedAt = row.last_refreshed_at;\n+    }\n+\n+    const sourceInfo = sourceInfoByPlaceId.get(row.id) ?? {\n+      sourceKeys: [],\n+      sourceLabels: [],\n+    };\n+    const existingLink = await db.query.canonicalPlaceEditorialLinks.findFirst({\n+      where: eq(canonicalPlaceEditorialLinks.recommendationPlaceId, row.id),\n+    });\n+    const linkedCanonical = existingLink\n+      ? await db.query.canonicalPlaces.findFirst({\n+          where: eq(canonicalPlaces.id, existingLink.canonicalPlaceId),\n+        })\n+      : null;\n+    const existingCanonical =\n+      linkedCanonical ?? (await findExistingCanonicalPlace(db, row));\n+    const matchMethod = determineMatchMethod(existingCanonical, row);\n+\n+    if (!options.write) {\n+      if (existingCanonical) {\n+        summary.canonicalUpdated += 1;\n+      } else {\n+        summary.canonicalCreated += 1;\n+      }\n+      summary.editorialLinksUpserted += 1;\n+      continue;\n+    }\n+\n+    const canonicalPlaceId = existingCanonical\n+      ? await updateCanonicalPlace(db, existingCanonical, row, sourceInfo)\n+      : await createCanonicalPlace(db, row, sourceInfo);\n+\n+    await db\n+      .insert(canonicalPlaceEditorialLinks)\n+      .values({\n+        canonicalPlaceId,\n+        recommendationPlaceId: row.id,\n+        matchMethod,\n+        matchConfidence: \"1\",\n+      })\n+      .onConflictDoUpdate({\n+        target: [canonicalPlaceEditorialLinks.recommendationPlaceId],\n+        set: {\n+          canonicalPlaceId,\n+          matchMethod,\n+          matchConfidence: \"1\",\n+          updatedAt: new Date(),\n+        },\n+      });\n+\n+    if (existingCanonical) {\n+      summary.canonicalUpdated += 1;\n+    } else {\n+      summary.canonicalCreated += 1;\n+    }\n+    summary.editorialLinksUpserted += 1;\n+  }\n+\n+  return summary;\n+}\n+\n+async function tryAcquireEditorialReconciliationLock(\n+  db: PlacesEnrichmentDb,\n+): Promise<boolean> {\n+  const result = await db.$client.query<{ locked: boolean }>(\n+    \"select pg_try_advisory_lock($1, $2) as locked\",",
    "path": "apps/places-enrichment/src/editorial-reconciliation.ts",
    "commit_id": "951458f8e8874c92bbb8b2ed0d021335ee3ba3cf",
    "original_commit_id": "951458f8e8874c92bbb8b2ed0d021335ee3ba3cf",
    "user": {
      "login": "chatgpt-codex-connector[bot]",
      "id": 199175422,
      "node_id": "BOT_kgDOC98s_g",
      "avatar_url": "https://avatars.githubusercontent.com/in/1144995?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D",
      "html_url": "https://github.com/apps/chatgpt-codex-connector",
      "followers_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/followers",
      "following_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/following{/other_user}",
      "gists_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/subscriptions",
      "organizations_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/orgs",
      "repos_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/repos",
      "events_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/events{/privacy}",
      "received_events_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/received_events",
      "type": "Bot",
      "user_view_type": "public",
      "site_admin": false
    },
    "body": "**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub>  Use one client for advisory locks**\n\nFor `--write` reconciliation runs, `db.$client.query` can acquire one pooled PostgreSQL session for `pg_try_advisory_lock` and another for `pg_advisory_unlock`; because these advisory locks are session-scoped, a successful run can leave the lock held on an idle pooled connection and make later reconciliations fail as already running until the process restarts. Acquire and release the lock on the same checked-out client, or use a transaction-scoped advisory lock within one transaction.\n\nUseful? React with 👍 / 👎.",
    "created_at": "2026-07-20T02:28:41Z",
    "updated_at": "2026-07-20T02:28:42Z",
    "html_url": "https://github.com/voytravel/voy/pull/1144#discussion_r3611762868",
    "pull_request_url": "https://api.github.com/repos/voytravel/voy/pulls/1144",
    "_links": {
      "self": {
        "href": "https://api.github.com/repos/voytravel/voy/pulls/comments/3611762868"
      },
      "html": {
        "href": "https://github.com/voytravel/voy/pull/1144#discussion_r3611762868"
      },
      "pull_request": {
        "href": "https://api.github.com/repos/voytravel/voy/pulls/1144"
      }
    },
    "reactions": {
      "url": "https://api.github.com/repos/voytravel/voy/pulls/comments/3611762868/reactions",
      "total_count": 0,
      "+1": 0,
      "-1": 0,
      "laugh": 0,
      "hooray": 0,
      "confused": 0,
      "heart": 0,
      "rocket": 0,
      "eyes": 0
    },
    "start_line": null,
    "original_start_line": null,
    "start_side": null,
    "line": 605,
    "original_line": 605,
    "side": "RIGHT",
    "author_association": "NONE",
    "original_position": 605,
    "position": 605,
    "subject_type": "line"
  },
  "pull_request": {
    "url": "https://api.github.com/repos/voytravel/voy/pulls/1144",
    "id": 4073379586,
    "node_id": "PR_kwDOPLnB3s7yytcC",
    "html_url": "https://github.com/voytravel/voy/pull/1144",
    "diff_url": "https://github.com/voytravel/voy/pull/1144.diff",
    "patch_url": "https://github.com/voytravel/voy/pull/1144.patch",
    "issue_url": "https://api.github.com/repos/voytravel/voy/issues/1144",
    "number": 1144,
    "state": "open",
    "locked": false,
    "title": "Preserve cached place detail richness",
    "user": {
      "login": "blazezhenli-sys",
      "id": 256320923,
      "node_id": "U_kgDOD0clmw",
      "avatar_url": "https://avatars.githubusercontent.com/u/256320923?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/blazezhenli-sys",
      "html_url": "https://github.com/blazezhenli-sys",
      "followers_url": "https://api.github.com/users/blazezhenli-sys/followers",
      "following_url": "https://api.github.com/users/blazezhenli-sys/following{/other_user}",
      "gists_url": "https://api.github.com/users/blazezhenli-sys/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/blazezhenli-sys/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/blazezhenli-sys/subscriptions",
      "organizations_url": "https://api.github.com/users/blazezhenli-sys/orgs",
      "repos_url": "https://api.github.com/users/blazezhenli-sys/repos",
      "events_url": "https://api.github.com/users/blazezhenli-sys/events{/privacy}",
      "received_events_url": "https://api.github.com/users/blazezhenli-sys/received_events",
      "type": "User",
      "user_view_type": "public",
      "site_admin": false
    },
    "body": "## Summary\n- preserve previously stored `place_details` JSON fields when later writes for the same place/language/profile are thinner\n- mirror that behavior in the in-memory test repository used by places-enrichment tests\n- add regression coverage for repeated core and rich detail writes\n\n## Testing\n- pnpm --filter @voytravel/places-enrichment test\n- pnpm --filter @voytravel/places-enrichment typecheck\n",
    "created_at": "2026-07-17T03:02:22Z",
    "updated_at": "2026-07-20T02:18:33Z",
    "closed_at": null,
    "merged_at": null,
    "merge_commit_sha": null,
    "assignees": [],
    "requested_reviewers": [
      {
        "login": "michaelmwu",
        "id": 421504,
        "node_id": "MDQ6VXNlcjQyMTUwNA==",
        "avatar_url": "https://avatars.githubusercontent.com/u/421504?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/michaelmwu",
        "html_url": "https://github.com/michaelmwu",
        "followers_url": "https://api.github.com/users/michaelmwu/followers",
        "following_url": "https://api.github.com/users/michaelmwu/following{/other_user}",
        "gists_url": "https://api.github.com/users/michaelmwu/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/michaelmwu/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/michaelmwu/subscriptions",
        "organizations_url": "https://api.github.com/users/michaelmwu/orgs",
        "repos_url": "https://api.github.com/users/michaelmwu/repos",
        "events_url": "https://api.github.com/users/michaelmwu/events{/privacy}",
        "received_events_url": "https://api.github.com/users/michaelmwu/received_events",
        "type": "User",
        "user_view_type": "public",
        "site_admin": false
      },
      {
        "login": "nickick",
        "id": 1316662,
        "node_id": "MDQ6VXNlcjEzMTY2NjI=",
        "avatar_url": "https://avatars.githubusercontent.com/u/1316662?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/nickick",
        "html_url": "https://github.com/nickick",
        "followers_url": "https://api.github.com/users/nickick/followers",
        "following_url": "https://api.github.com/users/nickick/following{/other_user}",
        "gists_url": "https://api.github.com/users/nickick/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/nickick/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/nickick/subscriptions",
        "organizations_url": "https://api.github.com/users/nickick/orgs",
        "repos_url": "https://api.github.com/users/nickick/repos",
        "events_url": "https://api.github.com/users/nickick/events{/privacy}",
        "received_events_url": "https://api.github.com/users/nickick/received_events",
        "type": "User",
        "user_view_type": "public",
        "site_admin": false
      },
      {
        "login": "mylordkaz",
        "id": 128312170,
        "node_id": "U_kgDOB6Xjag",
        "avatar_url": "https://avatars.githubusercontent.com/u/128312170?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/mylordkaz",
        "html_url": "https://github.com/mylordkaz",
        "followers_url": "https://api.github.com/users/mylordkaz/followers",
        "following_url": "https://api.github.com/users/mylordkaz/following{/other_user}",
        "gists_url": "https://api.github.com/users/mylordkaz/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/mylordkaz/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/mylordkaz/subscriptions",
        "organizations_url": "https://api.github.com/users/mylordkaz/orgs",
        "repos_url": "https://api.github.com/users/mylordkaz/repos",
        "events_url": "https://api.github.com/users/mylordkaz/events{/privacy}",
        "received_events_url": "https://api.github.com/users/mylordkaz/received_events",
        "type": "User",
        "user_view_type": "public",
        "site_admin": false
      }
    ],
    "requested_teams": [],
    "labels": [],
    "milestone": null,
    "draft": false,
    "commits_url": "https://api.github.com/repos/voytravel/voy/pulls/1144/commits",
    "review_comments_url": "https://api.github.com/repos/voytravel/voy/pulls/1144/comments",
    "review_comment_url": "https://api.github.com/repos/voytravel/voy/pulls/comments{/number}",
    "comments_url": "https://api.github.com/repos/voytravel/voy/issues/1144/comments",
    "statuses_url": "https://api.github.com/repos/voytravel/voy/statuses/951458f8e8874c92bbb8b2ed0d021335ee3ba3cf",
    "head": {
      "label": "voytravel:blaze/location-discovery-service-followup",
      "ref": "blaze/location-discovery-service-followup",
      "sha": "951458f8e8874c92bbb8b2ed0d021335ee3ba3cf",
      "user": {
        "login": "voytravel",
        "id": 213144510,
        "node_id": "O_kgDODLRTvg",
        "avatar_url": "https://avatars.githubusercontent.com/u/213144510?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/voytravel",
        "html_url": "https://github.com/voytravel",
        "followers_url": "https://api.github.com/users/voytravel/followers",
        "following_url": "https://api.github.com/users/voytravel/following{/other_user}",
        "gists_url": "https://api.github.com/users/voytravel/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/voytravel/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/voytravel/subscriptions",
        "organizations_url": "https://api.github.com/users/voytravel/orgs",
        "repos_url": "https://api.github.com/users/voytravel/repos",
        "events_url": "https://api.github.com/users/voytravel/events{/privacy}",
        "received_events_url": "https://api.github.com/users/voytravel/received_events",
        "type": "Organization",
        "user_view_type": "public",
        "site_admin": false
      },
      "repo": {
        "id": 1018806750,
        "node_id": "R_kgDOPLnB3g",
        "name": "voy",
        "full_name": "voytravel/voy",
        "private": true,
        "owner": {
          "login": "voytravel",
          "id": 213144510,
          "node_id": "O_kgDODLRTvg",
          "avatar_url": "https://avatars.githubusercontent.com/u/213144510?v=4",
          "gravatar_id": "",
          "url": "https://api.github.com/users/voytravel",
          "html_url": "https://github.com/voytravel",
          "followers_url": "https://api.github.com/users/voytravel/followers",
          "following_url": "https://api.github.com/users/voytravel/following{/other_user}",
          "gists_url": "https://api.github.com/users/voytravel/gists{/gist_id}",
          "starred_url": "https://api.github.com/users/voytravel/starred{/owner}{/repo}",
          "subscriptions_url": "https://api.github.com/users/voytravel/subscriptions",
          "organizations_url": "https://api.github.com/users/voytravel/orgs",
          "repos_url": "https://api.github.com/users/voytravel/repos",
          "events_url": "https://api.github.com/users/voytravel/events{/privacy}",
          "received_events_url": "https://api.github.com/users/voytravel/received_events",
          "type": "Organization",
          "user_view_type": "public",
          "site_admin": false
        },
        "html_url": "https://github.com/voytravel/voy",
        "description": "Voy Monorepo",
        "fork": false,
        "url": "https://api.github.com/repos/voytravel/voy",
        "forks_url": "https://api.github.com/repos/voytravel/voy/forks",
        "keys_url": "https://api.github.com/repos/voytravel/voy/keys{/key_id}",
        "collaborators_url": "https://api.github.com/repos/voytravel/voy/collaborators{/collaborator}",
        "teams_url": "https://api.github.com/repos/voytravel/voy/teams",
        "hooks_url": "https://api.github.com/repos/voytravel/voy/hooks",
        "issue_events_url": "https://api.github.com/repos/voytravel/voy/issues/events{/number}",
        "events_url": "https://api.github.com/repos/voytravel/voy/events",
        "assignees_url": "https://api.github.com/repos/voytravel/voy/assignees{/user}",
        "branches_url": "https://api.github.com/repos/voytravel/voy/branches{/branch}",
        "tags_url": "https://api.github.com/repos/voytravel/voy/tags",
        "blobs_url": "https://api.github.com/repos/voytravel/voy/git/blobs{/sha}",
        "git_tags_url": "https://api.github.com/repos/voytravel/voy/git/tags{/sha}",
        "git_refs_url": "https://api.github.com/repos/voytravel/voy/git/refs{/sha}",
        "trees_url": "https://api.github.com/repos/voytravel/voy/git/trees{/sha}",
        "statuses_url": "https://api.github.com/repos/voytravel/voy/statuses/{sha}",
        "languages_url": "https://api.github.com/repos/voytravel/voy/languages",
        "stargazers_url": "https://api.github.com/repos/voytravel/voy/stargazers",
        "contributors_url": "https://api.github.com/repos/voytravel/voy/contributors",
        "subscribers_url": "https://api.github.com/repos/voytravel/voy/subscribers",
        "subscription_url": "https://api.github.com/repos/voytravel/voy/subscription",
        "commits_url": "https://api.github.com/repos/voytravel/voy/commits{/sha}",
        "git_commits_url": "https://api.github.com/repos/voytravel/voy/git/commits{/sha}",
        "comments_url": "https://api.github.com/repos/voytravel/voy/comments{/number}",
        "issue_comment_url": "https://api.github.com/repos/voytravel/voy/issues/comments{/number}",
        "contents_url": "https://api.github.com/repos/voytravel/voy/contents/{+path}",
        "compare_url": "https://api.github.com/repos/voytravel/voy/compare/{base}...{head}",
        "merges_url": "https://api.github.com/repos/voytravel/voy/merges",
        "archive_url": "https://api.github.com/repos/voytravel/voy/{archive_format}{/ref}",
        "downloads_url": "https://api.github.com/repos/voytravel/voy/downloads",
        "issues_url": "https://api.github.com/repos/voytravel/voy/issues{/number}",
        "pulls_url": "https://api.github.com/repos/voytravel/voy/pulls{/number}",
        "milestones_url": "https://api.github.com/repos/voytravel/voy/milestones{/number}",
        "notifications_url": "https://api.github.com/repos/voytravel/voy/notifications{?since,all,participating}",
        "labels_url": "https://api.github.com/repos/voytravel/voy/labels{/name}",
        "releases_url": "https://api.github.com/repos/voytravel/voy/releases{/id}",
        "deployments_url": "https://api.github.com/repos/voytravel/voy/deployments",
        "created_at": "2025-07-13T04:51:01Z",
        "updated_at": "2026-07-18T15:45:05Z",
        "pushed_at": "2026-07-20T02:18:32Z",
        "git_url": "git://github.com/voytravel/voy.git",
        "ssh_url": "org-213144510@github.com:voytravel/voy.git",
        "clone_url": "https://github.com/voytravel/voy.git",
        "svn_url": "https://github.com/voytravel/voy",
        "homepage": null,
        "size": 67831,
        "stargazers_count": 1,
        "watchers_count": 1,
        "language": "TypeScript",
        "has_issues": true,
        "has_projects": true,
        "has_downloads": true,
        "has_wiki": false,
        "has_pages": false,
        "has_discussions": true,
        "forks_count": 0,
        "mirror_url": null,
        "archived": false,
        "disabled": false,
        "open_issues_count": 36,
        "license": {
          "key": "mit",
          "name": "MIT License",
          "spdx_id": "MIT",
          "url": "https://api.github.com/licenses/mit",
          "node_id": "MDc6TGljZW5zZTEz"
        },
        "allow_forking": false,
        "is_template": false,
        "web_commit_signoff_required": false,
        "has_pull_requests": true,
        "pull_request_creation_policy": "all",
        "topics": [],
        "visibility": "private",
        "forks": 0,
        "open_issues": 36,
        "watchers": 1,
        "default_branch": "develop",
        "allow_squash_merge": true,
        "allow_merge_commit": true,
        "allow_rebase_merge": true,
        "allow_auto_merge": true,
        "delete_branch_on_merge": true,
        "allow_update_branch": true,
        "use_squash_pr_title_as_default": false,
        "squash_merge_commit_message": "COMMIT_MESSAGES",
        "squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
        "merge_commit_message": "PR_TITLE",
        "merge_commit_title": "MERGE_MESSAGE"
      }
    },
    "base": {
      "label": "voytravel:develop",
      "ref": "develop",
      "sha": "3518ba3516cab8544a1295282500e44b9fc76981",
      "user": {
        "login": "voytravel",
        "id": 213144510,
        "node_id": "O_kgDODLRTvg",
        "avatar_url": "https://avatars.githubusercontent.com/u/213144510?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/voytravel",
        "html_url": "https://github.com/voytravel",
        "followers_url": "https://api.github.com/users/voytravel/followers",
        "following_url": "https://api.github.com/users/voytravel/following{/other_user}",
        "gists_url": "https://api.github.com/users/voytravel/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/voytravel/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/voytravel/subscriptions",
        "organizations_url": "https://api.github.com/users/voytravel/orgs",
        "repos_url": "https://api.github.com/users/voytravel/repos",
        "events_url": "https://api.github.com/users/voytravel/events{/privacy}",
        "received_events_url": "https://api.github.com/users/voytravel/received_events",
        "type": "Organization",
        "user_view_type": "public",
        "site_admin": false
      },
      "repo": {
        "id": 1018806750,
        "node_id": "R_kgDOPLnB3g",
        "name": "voy",
        "full_name": "voytravel/voy",
        "private": true,
        "owner": {
          "login": "voytravel",
          "id": 213144510,
          "node_id": "O_kgDODLRTvg",
          "avatar_url": "https://avatars.githubusercontent.com/u/213144510?v=4",
          "gravatar_id": "",
          "url": "https://api.github.com/users/voytravel",
          "html_url": "https://github.com/voytravel",
          "followers_url": "https://api.github.com/users/voytravel/followers",
          "following_url": "https://api.github.com/users/voytravel/following{/other_user}",
          "gists_url": "https://api.github.com/users/voytravel/gists{/gist_id}",
          "starred_url": "https://api.github.com/users/voytravel/starred{/owner}{/repo}",
          "subscriptions_url": "https://api.github.com/users/voytravel/subscriptions",
          "organizations_url": "https://api.github.com/users/voytravel/orgs",
          "repos_url": "https://api.github.com/users/voytravel/repos",
          "events_url": "https://api.github.com/users/voytravel/events{/privacy}",
          "received_events_url": "https://api.github.com/users/voytravel/received_events",
          "type": "Organization",
          "user_view_type": "public",
          "site_admin": false
        },
        "html_url": "https://github.com/voytravel/voy",
        "description": "Voy Monorepo",
        "fork": false,
        "url": "https://api.github.com/repos/voytravel/voy",
        "forks_url": "https://api.github.com/repos/voytravel/voy/forks",
        "keys_url": "https://api.github.com/repos/voytravel/voy/keys{/key_id}",
        "collaborators_url": "https://api.github.com/repos/voytravel/voy/collaborators{/collaborator}",
        "teams_url": "https://api.github.com/repos/voytravel/voy/teams",
        "hooks_url": "https://api.github.com/repos/voytravel/voy/hooks",
        "issue_events_url": "https://api.github.com/repos/voytravel/voy/issues/events{/number}",
        "events_url": "https://api.github.com/repos/voytravel/voy/events",
        "assignees_url": "https://api.github.com/repos/voytravel/voy/assignees{/user}",
        "branches_url": "https://api.github.com/repos/voytravel/voy/branches{/branch}",
        "tags_url": "https://api.github.com/repos/voytravel/voy/tags",
        "blobs_url": "https://api.github.com/repos/voytravel/voy/git/blobs{/sha}",
        "git_tags_url": "https://api.github.com/repos/voytravel/voy/git/tags{/sha}",
        "git_refs_url": "https://api.github.com/repos/voytravel/voy/git/refs{/sha}",
        "trees_url": "https://api.github.com/repos/voytravel/voy/git/trees{/sha}",
        "statuses_url": "https://api.github.com/repos/voytravel/voy/statuses/{sha}",
        "languages_url": "https://api.github.com/repos/voytravel/voy/languages",
        "stargazers_url": "https://api.github.com/repos/voytravel/voy/stargazers",
        "contributors_url": "https://api.github.com/repos/voytravel/voy/contributors",
        "subscribers_url": "https://api.github.com/repos/voytravel/voy/subscribers",
        "subscription_url": "https://api.github.com/repos/voytravel/voy/subscription",
        "commits_url": "https://api.github.com/repos/voytravel/voy/commits{/sha}",
        "git_commits_url": "https://api.github.com/repos/voytravel/voy/git/commits{/sha}",
        "comments_url": "https://api.github.com/repos/voytravel/voy/comments{/number}",
        "issue_comment_url": "https://api.github.com/repos/voytravel/voy/issues/comments{/number}",
        "contents_url": "https://api.github.com/repos/voytravel/voy/contents/{+path}",
        "compare_url": "https://api.github.com/repos/voytravel/voy/compare/{base}...{head}",
        "merges_url": "https://api.github.com/repos/voytravel/voy/merges",
        "archive_url": "https://api.github.com/repos/voytravel/voy/{archive_format}{/ref}",
        "downloads_url": "https://api.github.com/repos/voytravel/voy/downloads",
        "issues_url": "https://api.github.com/repos/voytravel/voy/issues{/number}",
        "pulls_url": "https://api.github.com/repos/voytravel/voy/pulls{/number}",
        "milestones_url": "https://api.github.com/repos/voytravel/voy/milestones{/number}",
        "notifications_url": "https://api.github.com/repos/voytravel/voy/notifications{?since,all,participating}",
        "labels_url": "https://api.github.com/repos/voytravel/voy/labels{/name}",
        "releases_url": "https://api.github.com/repos/voytravel/voy/releases{/id}",
        "deployments_url": "https://api.github.com/repos/voytravel/voy/deployments",
        "created_at": "2025-07-13T04:51:01Z",
        "updated_at": "2026-07-18T15:45:05Z",
        "pushed_at": "2026-07-20T02:18:32Z",
        "git_url": "git://github.com/voytravel/voy.git",
        "ssh_url": "org-213144510@github.com:voytravel/voy.git",
        "clone_url": "https://github.com/voytravel/voy.git",
        "svn_url": "https://github.com/voytravel/voy",
        "homepage": null,
        "size": 67831,
        "stargazers_count": 1,
        "watchers_count": 1,
        "language": "TypeScript",
        "has_issues": true,
        "has_projects": true,
        "has_downloads": true,
        "has_wiki": false,
        "has_pages": false,
        "has_discussions": true,
        "forks_count": 0,
        "mirror_url": null,
        "archived": false,
        "disabled": false,
        "open_issues_count": 36,
        "license": {
          "key": "mit",
          "name": "MIT License",
          "spdx_id": "MIT",
          "url": "https://api.github.com/licenses/mit",
          "node_id": "MDc6TGljZW5zZTEz"
        },
        "allow_forking": false,
        "is_template": false,
        "web_commit_signoff_required": false,
        "has_pull_requests": true,
        "pull_request_creation_policy": "all",
        "topics": [],
        "visibility": "private",
        "forks": 0,
        "open_issues": 36,
        "watchers": 1,
        "default_branch": "develop",
        "allow_squash_merge": true,
        "allow_merge_commit": true,
        "allow_rebase_merge": true,
        "allow_auto_merge": true,
        "delete_branch_on_merge": true,
        "allow_update_branch": true,
        "use_squash_pr_title_as_default": false,
        "squash_merge_commit_message": "COMMIT_MESSAGES",
        "squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
        "merge_commit_message": "PR_TITLE",
        "merge_commit_title": "MERGE_MESSAGE"
      }
    },
    "_links": {
      "self": {
        "href": "https://api.github.com/repos/voytravel/voy/pulls/1144"
      },
      "html": {
        "href": "https://github.com/voytravel/voy/pull/1144"
      },
      "issue": {
        "href": "https://api.github.com/repos/voytravel/voy/issues/1144"
      },
      "comments": {
        "href": "https://api.github.com/repos/voytravel/voy/issues/1144/comments"
      },
      "review_comments": {
        "href": "https://api.github.com/repos/voytravel/voy/pulls/1144/comments"
      },
      "review_comment": {
        "href": "https://api.github.com/repos/voytravel/voy/pulls/comments{/number}"
      },
      "commits": {
        "href": "https://api.github.com/repos/voytravel/voy/pulls/1144/commits"
      },
      "statuses": {
        "href": "https://api.github.com/repos/voytravel/voy/statuses/951458f8e8874c92bbb8b2ed0d021335ee3ba3cf"
      }
    },
    "author_association": "CONTRIBUTOR",
    "auto_merge": null,
    "assignee": null,
    "active_lock_reason": null
  },
  "repository": {
    "id": 1018806750,
    "node_id": "R_kgDOPLnB3g",
    "name": "voy",
    "full_name": "voytravel/voy",
    "private": true,
    "owner": {
      "login": "voytravel",
      "id": 213144510,
      "node_id": "O_kgDODLRTvg",
      "avatar_url": "https://avatars.githubusercontent.com/u/213144510?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/voytravel",
      "html_url": "https://github.com/voytravel",
      "followers_url": "https://api.github.com/users/voytravel/followers",
      "following_url": "https://api.github.com/users/voytravel/following{/other_user}",
      "gists_url": "https://api.github.com/users/voytravel/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/voytravel/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/voytravel/subscriptions",
      "organizations_url": "https://api.github.com/users/voytravel/orgs",
      "repos_url": "https://api.github.com/users/voytravel/repos",
      "events_url": "https://api.github.com/users/voytravel/events{/privacy}",
      "received_events_url": "https://api.github.com/users/voytravel/received_events",
      "type": "Organization",
      "user_view_type": "public",
      "site_admin": false
    },
    "html_url": "https://github.com/voytravel/voy",
    "description": "Voy Monorepo",
    "fork": false,
    "url": "https://api.github.com/repos/voytravel/voy",
    "forks_url": "https://api.github.com/repos/voytravel/voy/forks",
    "keys_url": "https://api.github.com/repos/voytravel/voy/keys{/key_id}",
    "collaborators_url": "https://api.github.com/repos/voytravel/voy/collaborators{/collaborator}",
    "teams_url": "https://api.github.com/repos/voytravel/voy/teams",
    "hooks_url": "https://api.github.com/repos/voytravel/voy/hooks",
    "issue_events_url": "https://api.github.com/repos/voytravel/voy/issues/events{/number}",
    "events_url": "https://api.github.com/repos/voytravel/voy/events",
    "assignees_url": "https://api.github.com/repos/voytravel/voy/assignees{/user}",
    "branches_url": "https://api.github.com/repos/voytravel/voy/branches{/branch}",
    "tags_url": "https://api.github.com/repos/voytravel/voy/tags",
    "blobs_url": "https://api.github.com/repos/voytravel/voy/git/blobs{/sha}",
    "git_tags_url": "https://api.github.com/repos/voytravel/voy/git/tags{/sha}",
    "git_refs_url": "https://api.github.com/repos/voytravel/voy/git/refs{/sha}",
    "trees_url": "https://api.github.com/repos/voytravel/voy/git/trees{/sha}",
    "statuses_url": "https://api.github.com/repos/voytravel/voy/statuses/{sha}",
    "languages_url": "https://api.github.com/repos/voytravel/voy/languages",
    "stargazers_url": "https://api.github.com/repos/voytravel/voy/stargazers",
    "contributors_url": "https://api.github.com/repos/voytravel/voy/contributors",
    "subscribers_url": "https://api.github.com/repos/voytravel/voy/subscribers",
    "subscription_url": "https://api.github.com/repos/voytravel/voy/subscription",
    "commits_url": "https://api.github.com/repos/voytravel/voy/commits{/sha}",
    "git_commits_url": "https://api.github.com/repos/voytravel/voy/git/commits{/sha}",
    "comments_url": "https://api.github.com/repos/voytravel/voy/comments{/number}",
    "issue_comment_url": "https://api.github.com/repos/voytravel/voy/issues/comments{/number}",
    "contents_url": "https://api.github.com/repos/voytravel/voy/contents/{+path}",
    "compare_url": "https://api.github.com/repos/voytravel/voy/compare/{base}...{head}",
    "merges_url": "https://api.github.com/repos/voytravel/voy/merges",
    "archive_url": "https://api.github.com/repos/voytravel/voy/{archive_format}{/ref}",
    "downloads_url": "https://api.github.com/repos/voytravel/voy/downloads",
    "issues_url": "https://api.github.com/repos/voytravel/voy/issues{/number}",
    "pulls_url": "https://api.github.com/repos/voytravel/voy/pulls{/number}",
    "milestones_url": "https://api.github.com/repos/voytravel/voy/milestones{/number}",
    "notifications_url": "https://api.github.com/repos/voytravel/voy/notifications{?since,all,participating}",
    "labels_url": "https://api.github.com/repos/voytravel/voy/labels{/name}",
    "releases_url": "https://api.github.com/repos/voytravel/voy/releases{/id}",
    "deployments_url": "https://api.github.com/repos/voytravel/voy/deployments",
    "created_at": "2025-07-13T04:51:01Z",
    "updated_at": "2026-07-18T15:45:05Z",
    "pushed_at": "2026-07-20T02:18:32Z",
    "git_url": "git://github.com/voytravel/voy.git",
    "ssh_url": "org-213144510@github.com:voytravel/voy.git",
    "clone_url": "https://github.com/voytravel/voy.git",
    "svn_url": "https://github.com/voytravel/voy",
    "homepage": null,
    "size": 67831,
    "stargazers_count": 1,
    "watchers_count": 1,
    "language": "TypeScript",
    "has_issues": true,
    "has_projects": true,
    "has_downloads": true,
    "has_wiki": false,
    "has_pages": false,
    "has_discussions": true,
    "forks_count": 0,
    "mirror_url": null,
    "archived": false,
    "disabled": false,
    "open_issues_count": 36,
    "license": {
      "key": "mit",
      "name": "MIT License",
      "spdx_id": "MIT",
      "url": "https://api.github.com/licenses/mit",
      "node_id": "MDc6TGljZW5zZTEz"
    },
    "allow_forking": false,
    "is_template": false,
    "web_commit_signoff_required": false,
    "has_pull_requests": true,
    "pull_request_creation_policy": "all",
    "topics": [],
    "visibility": "private",
    "forks": 0,
    "open_issues": 36,
    "watchers": 1,
    "default_branch": "develop",
    "custom_properties": {}
  },
  "organization": {
    "login": "voytravel",
    "id": 213144510,
    "node_id": "O_kgDODLRTvg",
    "url": "https://api.github.com/orgs/voytravel",
    "repos_url": "https://api.github.com/orgs/voytravel/repos",
    "events_url": "https://api.github.com/orgs/voytravel/events",
    "hooks_url": "https://api.github.com/orgs/voytravel/hooks",
    "issues_url": "https://api.github.com/orgs/voytravel/issues",
    "members_url": "https://api.github.com/orgs/voytravel/members{/member}",
    "public_members_url": "https://api.github.com/orgs/voytravel/public_members{/member}",
    "avatar_url": "https://avatars.githubusercontent.com/u/213144510?v=4",
    "description": ""
  },
  "enterprise": {
    "id": 469843,
    "slug": "darkmatter",
    "name": "darkmatter",
    "node_id": "E_kgDOAAcrUw",
    "avatar_url": "https://avatars.githubusercontent.com/b/469843?v=4",
    "description": "",
    "website_url": "darkmatter.io",
    "html_url": "https://github.com/enterprises/darkmatter",
    "created_at": "2025-09-07T16:01:00Z",
    "updated_at": "2026-07-07T15:59:26Z"
  },
  "sender": {
    "login": "chatgpt-codex-connector[bot]",
    "id": 199175422,
    "node_id": "BOT_kgDOC98s_g",
    "avatar_url": "https://avatars.githubusercontent.com/in/1144995?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D",
    "html_url": "https://github.com/apps/chatgpt-codex-connector",
    "followers_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/followers",
    "following_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/following{/other_user}",
    "gists_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/subscriptions",
    "organizations_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/orgs",
    "repos_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/repos",
    "events_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/events{/privacy}",
    "received_events_url": "https://api.github.com/users/chatgpt-codex-connector%5Bbot%5D/received_events",
    "type": "Bot",
    "user_view_type": "public",
    "site_admin": false
  },
  "installation": {
    "id": 143976268,
    "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMTQzOTc2MjY4"
  }
}