Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/registry/normalize.ts
Original file line number Diff line number Diff line change
@@ -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<string, JsonValue>;

Expand Down Expand Up @@ -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,
};
}
Expand All @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<string, JsonValue>;
};

Expand Down
46 changes: 45 additions & 1 deletion test/unit/registry.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(
{
Expand Down