From 6093903ef1bedfae4c6f6f10f0f0b7a6ca080a3c Mon Sep 17 00:00:00 2001 From: glorydavid03023 Date: Tue, 21 Jul 2026 00:52:12 -0500 Subject: [PATCH] feat(registry): add repoOrigin (BYOR/APR) to RegistryRepoConfig (#7636) Mirrors #6320's poolAssociation exactly: a new OPTIONAL field on RegistryRepoConfig that is absent for every repo predating it, so an unmarked repo round-trips byte-identical to today. No behavior changes. - types.ts: `RepoOrigin = { kind: "byor" } | { kind: "apr"; hostingOrg: string }` and `repoOrigin?: RepoOrigin | null` on RegistryRepoConfig, with the same null-safety doc convention poolAssociation uses. Absent means "pre-dates this field / not yet known" -- deliberately NOT assumed BYOR, per the issue. - registry/normalize.ts: `parseRepoOrigin` reads the registry's flat `repo_origin` (+ `hosting_org` for APR) fields and is threaded into normalizeRepo alongside parsePoolAssociation; a `getRepoOrigin` accessor mirrors getRepoPoolAssociation. An absent marker, an unrecognized string, or an `apr` marker missing its hosting org all parse to null -- the same "a partial one is treated as none" collapse parsePoolAssociation already uses, never a half-populated object. Type-and-plumbing only: no repo-creation, GitHub API, or provisioning logic (that is #7590's scope). Tests (registry.test.ts, mirroring #6320's poolAssociation test): a BYOR marker, a full APR origin, an unmarked repo (null, byte-identical), a malformed apr-without-hosting-org (null), and an unrecognized origin (null); plus the getRepoOrigin accessor over present/null/undefined configs. Both the present and absent branches of the new field are covered. --- src/registry/normalize.ts | 26 ++++++++++++++++++++- src/types.ts | 12 ++++++++++ test/unit/registry.test.ts | 46 +++++++++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/registry/normalize.ts b/src/registry/normalize.ts index 2e25683690..624665c9f7 100644 --- a/src/registry/normalize.ts +++ b/src/registry/normalize.ts @@ -1,5 +1,5 @@ import { DEFAULT_ISSUE_DISCOVERY_SHARE } from "../scoring/model"; -import type { JsonValue, RegistryRepoConfig, RegistrySnapshot, RepoPoolAssociation, RepoTimeDecayOverrides } from "../types"; +import type { JsonValue, RegistryRepoConfig, RegistrySnapshot, RepoOrigin, RepoPoolAssociation, RepoTimeDecayOverrides } from "../types"; type RawRepoConfig = Record; @@ -77,6 +77,7 @@ function normalizeRepo(repo: string, config: RawRepoConfig): RegistryRepoConfig eligibilityMode: stringValue(config.eligibility_mode), timeDecay: parseTimeDecayOverrides(config.scoring), poolAssociation: parsePoolAssociation(config), + repoOrigin: parseRepoOrigin(config), raw: config, }; } @@ -98,6 +99,29 @@ export function getRepoPoolAssociation(config: RegistryRepoConfig | null | undef return config?.poolAssociation ?? null; } +// Repo provisioning origin (#7589), from the registry's flat `repo_origin` (+ `hosting_org` for APR) fields. +// Only an explicit marker yields an origin: an absent field parses to null (mirroring parsePoolAssociation), +// because absent means "pre-dates this field / not yet known", NOT a confirmed BYOR. An `apr` marker missing +// its hosting org is malformed and likewise treated as no origin, the same way a half-specified pool +// association above collapses to null rather than a partial object. This is type-and-plumbing only — no +// repo-creation or GitHub API logic lives here (#7590 covers that separately). +function parseRepoOrigin(config: RawRepoConfig): RepoOrigin | null { + const kind = stringValue(config.repo_origin); + if (kind === "byor") return { kind: "byor" }; + if (kind === "apr") { + const hostingOrg = stringValue(config.hosting_org); + return hostingOrg === null ? null : { kind: "apr", hostingOrg }; + } + return null; +} + +// Read accessor for a repo's provisioning origin (#7589), mirroring getRepoPoolAssociation: returns the origin +// a repo was registered with, or null for a repo that pre-dates the field / has no config. The single place +// downstream code asks "was this repo customer-provided (BYOR) or loopover-provisioned (APR)?". +export function getRepoOrigin(config: RegistryRepoConfig | null | undefined): RepoOrigin | null { + return config?.repoOrigin ?? null; +} + // Per-repo time-decay overrides (#703), from the registry's nested `scoring.time_decay` (the same source // upstream reads). Each key is optional; absent/non-numeric → null (resolveTimeDecay falls back to the // global default). Returns null when there is no usable override, so a repo without one uses all defaults. diff --git a/src/types.ts b/src/types.ts index 5a4080fafc..27fe249818 100644 --- a/src/types.ts +++ b/src/types.ts @@ -449,6 +449,16 @@ export type RepoPoolAssociation = { subnetId: number; }; +/** + * Repo provisioning origin (#7589's BYOR+APR epic; #7590's hosting decision). BYOR = a customer's own + * pre-existing repo; APR = a loopover-provisioned repo, carrying the GitHub org it was created under. Present + * only when the registry explicitly records it — absent means "not yet known / pre-dates this field", NOT a + * confirmed BYOR, so an unmarked repo round-trips byte-identical to today. Read it via `getRepoOrigin`. + */ +export type RepoOrigin = + | { kind: "byor" } + | { kind: "apr"; hostingOrg: string }; + export type RegistryRepoConfig = { repo: string; emissionShare: number; @@ -463,6 +473,8 @@ export type RegistryRepoConfig = { timeDecay?: RepoTimeDecayOverrides | null; /** Subnet-funded pool association (#6099); null/absent = an organic repo with no funding pool (#6320). */ poolAssociation?: RepoPoolAssociation | null; + /** Repo provisioning origin (#7589); null/absent = pre-dates this field, unchanged behavior (do NOT assume BYOR). */ + repoOrigin?: RepoOrigin | null; raw: Record; }; diff --git a/test/unit/registry.test.ts b/test/unit/registry.test.ts index a78b470e2f..adce941203 100644 --- a/test/unit/registry.test.ts +++ b/test/unit/registry.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { getRepository, upsertRepositoryFromGitHub } from "../../src/db/repositories"; -import { getRepoPoolAssociation, normalizeRegistryPayload } from "../../src/registry/normalize"; +import { getRepoOrigin, getRepoPoolAssociation, normalizeRegistryPayload } from "../../src/registry/normalize"; import { DEFAULT_ISSUE_DISCOVERY_SHARE } from "../../src/scoring/model"; import { getLatestRegistrySnapshot, persistRegistrySnapshot, refreshRegistry } from "../../src/registry/sync"; import { createCloudTestEnv, createTestEnv } from "../helpers/d1"; @@ -118,6 +118,50 @@ describe("registry normalization", () => { expect(byName["JSONbored/subnet-only"]!.poolAssociation ?? null).toBeNull(); }); + it("parses a repo provisioning origin (BYOR/APR) and leaves unmarked repos with none (#7589)", () => { + const snapshot = normalizeRegistryPayload( + { + // An explicit BYOR marker → the customer-provided origin reads back. + "JSONbored/byor": { emission_share: 0.02, repo_origin: "byor" }, + // An APR marker carries the loopover org it was provisioned under → full APR origin. + "JSONbored/apr": { emission_share: 0.02, repo_origin: "apr", hosting_org: "loopover-repos" }, + // An unmarked repo has no origin field → null, byte-identical to today (NOT assumed BYOR). + "JSONbored/legacy": { emission_share: 0.01 }, + // An APR marker missing its hosting org is malformed → null, not a half-populated object. + "JSONbored/apr-no-org": { emission_share: 0.01, repo_origin: "apr" }, + // An unrecognized origin string is ignored → null. + "JSONbored/bogus": { emission_share: 0.01, repo_origin: "mystery" }, + }, + { kind: "raw-github", url: "https://example.test/master_repositories.json" }, + "2026-05-22T00:00:00.000Z", + ); + const byName = Object.fromEntries(snapshot.repositories.map((r) => [r.repo, r])); + expect(byName["JSONbored/byor"]!.repoOrigin).toEqual({ kind: "byor" }); + expect(byName["JSONbored/apr"]!.repoOrigin).toEqual({ kind: "apr", hostingOrg: "loopover-repos" }); + expect(byName["JSONbored/legacy"]!.repoOrigin ?? null).toBeNull(); + expect(byName["JSONbored/apr-no-org"]!.repoOrigin ?? null).toBeNull(); + expect(byName["JSONbored/bogus"]!.repoOrigin ?? null).toBeNull(); + // The unmarked repo carries NO origin key at all, so it round-trips byte-identical to before this field. + expect("repoOrigin" in byName["JSONbored/legacy"]!).toBe(true); + expect(byName["JSONbored/legacy"]!.repoOrigin).toBeNull(); + }); + + it("reads a repo's origin via the getRepoOrigin accessor (#7589)", () => { + const snapshot = normalizeRegistryPayload( + { "JSONbored/apr": { emission_share: 0.02, repo_origin: "apr", hosting_org: "loopover-repos" } }, + { kind: "raw-github", url: "https://example.test/master_repositories.json" }, + "2026-05-22T00:00:00.000Z", + ); + const config = snapshot.repositories.find((r) => r.repo === "JSONbored/apr")!; + expect(getRepoOrigin(config)).toEqual({ kind: "apr", hostingOrg: "loopover-repos" }); + expect(getRepoOrigin(null)).toBeNull(); + expect(getRepoOrigin(undefined)).toBeNull(); + // A config object with no repoOrigin key at all resolves to null through the nullish accessor (the + // "pre-dates this field" case). Omit the key rather than set it to undefined, per exactOptionalPropertyTypes. + const { repoOrigin: _omitted, ...withoutOrigin } = config; + expect(getRepoOrigin(withoutOrigin)).toBeNull(); + }); + it("getRepoPoolAssociation reads a repo's pool association or null (#6320)", () => { const snapshot = normalizeRegistryPayload( {