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
22 changes: 21 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, RepoOrigin, RepoPoolAssociation, RepoTimeDecayOverrides } from "../types";
import type { CustomerPoolAssociation, 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),
customerPoolAssociation: parseCustomerPoolAssociation(config),
repoOrigin: parseRepoOrigin(config),
raw: config,
};
Expand All @@ -99,6 +100,25 @@ export function getRepoPoolAssociation(config: RegistryRepoConfig | null | undef
return config?.poolAssociation ?? null;
}

// Customer-funded pool association (#7679), from the registry's flat `pool_id`/`funder_account` fields —
// parallel to parsePoolAssociation, but the funding source is a paying customer's account, not a subnet netuid.
// Both must be present and non-empty for an association to exist; a repo missing either (i.e. every repo with
// no customer-funded pool, including a subnet-funded one that carries `subnet_id` but no `funder_account`)
// parses to null and stays byte-identical to today.
function parseCustomerPoolAssociation(config: RawRepoConfig): CustomerPoolAssociation | null {
const poolId = stringValue(config.pool_id);
const funderAccount = stringValue(config.funder_account);
if (poolId === null || funderAccount === null) return null;
return { poolId, funderAccount };
}

// Read accessor for a repo's customer-funded pool association (#7679), mirroring getRepoPoolAssociation: the
// single place downstream code asks "is this repo funded by a paying customer?", distinct from the subnet-funded
// question getRepoPoolAssociation answers.
export function getCustomerPoolAssociation(config: RegistryRepoConfig | null | undefined): CustomerPoolAssociation | null {
return config?.customerPoolAssociation ?? 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
Expand Down
17 changes: 17 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,21 @@ export type RepoPoolAssociation = {
subnetId: number;
};

/**
* Customer-funded pool association for a registered repo (#7679, Wave 5's "two demographics" per #4778). The
* parallel to {@link RepoPoolAssociation} for a pool funded by a paying CUSTOMER rather than a Bittensor subnet:
* both pay out to gittensor-registered contributors as emissions (same payout mechanism), but the funding source
* differs — so `subnetId` (meaningless for a non-subnet customer) is replaced by the funding customer's account.
* Kept a separate type rather than an optional `subnetId` on `RepoPoolAssociation` so the two funding models stay
* structurally distinct. Both fields are required for a valid association — a partial one (only one field) is
* treated as no association, so a repo with no customer-pool fields round-trips byte-identical to today. Read it
* via `getCustomerPoolAssociation`.
*/
export type CustomerPoolAssociation = {
poolId: string;
funderAccount: string;
};

/**
* 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
Expand All @@ -473,6 +488,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;
/** Customer-funded pool association (#7679); null/absent = a repo with no customer-funded pool. */
customerPoolAssociation?: CustomerPoolAssociation | 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
43 changes: 42 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 { getRepoOrigin, getRepoPoolAssociation, normalizeRegistryPayload } from "../../src/registry/normalize";
import { getCustomerPoolAssociation, 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,47 @@ describe("registry normalization", () => {
expect(byName["JSONbored/subnet-only"]!.poolAssociation ?? null).toBeNull();
});

it("parses a customer-funded pool association, parallel to and distinct from the subnet-funded one (#7679)", () => {
const snapshot = normalizeRegistryPayload(
{
// A customer-funded repo carries a pool id and a funder account → a full customer association reads back.
"JSONbored/customer": { emission_share: 0.02, pool_id: "pool-cust", funder_account: "acme-corp" },
// A subnet-funded repo has pool_id + subnet_id but no funder_account → NO customer association (they stay distinct).
"JSONbored/subnet": { emission_share: 0.02, pool_id: "pool-74", subnet_id: 74 },
// An organic repo has neither → no customer association, byte-identical to today.
"JSONbored/organic": { emission_share: 0.01 },
// A partial customer association (pool id but no funder) is dropped, not a half-populated object.
"JSONbored/pool-only": { emission_share: 0.01, pool_id: "pool-x" },
// A partial customer association (funder but no pool id) is likewise dropped.
"JSONbored/funder-only": { emission_share: 0.01, funder_account: "acme-corp" },
},
{ 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/customer"]!.customerPoolAssociation).toEqual({ poolId: "pool-cust", funderAccount: "acme-corp" });
// The subnet-funded repo has its own poolAssociation but NO customer association — the two are independent.
expect(byName["JSONbored/subnet"]!.customerPoolAssociation ?? null).toBeNull();
expect(byName["JSONbored/subnet"]!.poolAssociation).toEqual({ poolId: "pool-74", subnetId: 74 });
expect(byName["JSONbored/organic"]!.customerPoolAssociation ?? null).toBeNull();
expect(byName["JSONbored/pool-only"]!.customerPoolAssociation ?? null).toBeNull();
expect(byName["JSONbored/funder-only"]!.customerPoolAssociation ?? null).toBeNull();
});

it("reads a repo's customer-funded pool via the getCustomerPoolAssociation accessor (#7679)", () => {
const snapshot = normalizeRegistryPayload(
{ "JSONbored/customer": { emission_share: 0.02, pool_id: "pool-cust", funder_account: "acme-corp" } },
{ 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/customer")!;
expect(getCustomerPoolAssociation(config)).toEqual({ poolId: "pool-cust", funderAccount: "acme-corp" });
expect(getCustomerPoolAssociation(null)).toBeNull();
expect(getCustomerPoolAssociation(undefined)).toBeNull();
const { customerPoolAssociation: _omitted, ...withoutCustomerPool } = config;
expect(getCustomerPoolAssociation(withoutCustomerPool)).toBeNull();
});

it("parses a repo provisioning origin (BYOR/APR) and leaves unmarked repos with none (#7589)", () => {
const snapshot = normalizeRegistryPayload(
{
Expand Down