Make every tool result say what it counted
CI / verify (push) Successful in 7m44s
CI / publish (push) Has been skipped

Asked how many capacity commitments were on the book, Piggy answered "3".
Production holds 5. It had called the idle-capacity tool, which filters to
blocks above an idle threshold, and read the length of that list as the size
of the book.

The system prompt already forbade this in terms — "never report a filtered
count as a total; pig_get_idle_capacity returns the blocks with idle hours,
not the book" — and the model did it anyway. That is the second time this
argument has been lost in the prompt, so it is settled in the payload
instead: a result that cannot describe its own scope will be misread
eventually, however firmly the prompt objects.

Every tool that returns a count or a collection now carries one shape:
what it covers, how many matched, out of how many, under which filters, and
whether the list was truncated. The denominators are read from the database
rather than inferred. The pre-formatted headline states the scope too, since
that is the sentence a small model quotes most readily — the idle tool now
opens "3 of 5 live capacity commitments on the book", which is the sentence
that makes the original mistake impossible to phrase.

Two details worth keeping. Record reads enumerate rather than filter, so
their scope states a boundary instead of a ratio: these are that record's own
figures, never book-wide totals. And the workspace summary's idle threshold
is deliberately recorded as 0, distinct from the idle tool's 0.25 — that
mismatch is why three different idle figures appeared across the UI, and
naming it in the data is how it stops being invisible.

Verified against the live model: the failing question now answers 5, demand
deals 13 and contracts 20 — each drawn from a payload whose filtered figure
was smaller — while "which blocks are sitting idle" still names exactly the
blocks that are.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude
2026-08-14 19:03:40 -07:00
parent 18d5f5bfc0
commit f2ef403ee9
6 changed files with 1117 additions and 113 deletions
+13 -2
View File
@@ -26,7 +26,7 @@ const UNITS_RULE = `Units, before you quote any figure:
- Never write a money figure in cents. "112 cents" and "112c" are both wrong; write $1.12. Every money figure you write starts with a dollar sign.
- Any field whose name ends in Pct, and utilisation, is a share between 0 and 1. 0.38 is 38 per cent.
- Any field whose name ends in GpuHours is a count of GPU-hours, not money.
- The headline string is the one figure already formatted in dollars. Quote it as written rather than reformatting it.
- The headline string is the one figure already formatted in dollars, and it also states what the result covers. Quote it as written rather than reformatting it.
- A null money field means not applicable, not zero. Say why it is absent.`;
/**
@@ -68,11 +68,22 @@ const DOMAIN_BRIEFING = `How this business works, so the figures mean what you s
* So: an explicit ban, the lookup tools named as the way out, and the maths
* formatting forbidden outright — `\boxed{}` is the tell that the model has
* stopped answering about a CRM and started solving a puzzle.
*
* The scope bullets are the second half of a fix whose first half is in the
* data. This rule already said, naming the tool, that a filtered count is not a
* total; on /capacity nemotron read `pig_get_idle_capacity`'s three blocks as
* the size of a five-commitment book anyway, because nothing in the payload
* contradicted it. Every result now carries `scope` with `matched`, `total` and
* `totalLabel`, so the instruction has a field to point at rather than a
* principle to hold — and that is the only form of this rule that has survived
* contact with a 30B model. Terse on purpose: it rides on every request.
*/
const GROUNDING_RULE = `Grounding, which overrides everything else:
- NEVER state a number, name, date or status about this business unless it appeared in a tool result in THIS conversation. Not from memory, not from what a figure "should" be, not by inference from the page you are on.
- If the tool you were given does not answer the question, do not guess and do not stop: pig_search_records finds a record by name and pig_get_record_by_id opens it. Reach for those before concluding anything.
- A tool result answers only what that tool covers. Never report a filtered count as a total: pig_get_idle_capacity returns the blocks with idle hours, not the book. If the result does not cover the question as asked, say what it does cover and what is missing.
- Every result says what it covers. Read its scope object first: matched is how many passed a filter, total is the whole set they were drawn from, totalLabel names what total counts, listed is how many rows the payload carries, filters names every threshold applied.
- Asked how many there are, quote total, never matched and never the length of a list you can see. matched answers "how many are unsold" or "how many match"; it is never the size of the book. If total does not cover the question as asked, say what the result does cover and what is missing.
- Two tools can report different counts of the same thing because they applied different thresholds. Say which threshold produced the figure you quote; it is in filters.
- If no tool can answer it, say exactly that and name what you would need. "I cannot see that from here" is a correct answer. An invented figure is not, and is worse than silence — someone will act on it.
- Never use LaTeX or mathematical notation. No \\boxed{}, no \\(...\\). Write plain prose and plain numbers.`;
+279 -62
View File
@@ -41,15 +41,52 @@ import {
} from '@pig/db';
import { CapacityService } from '@pig/api/src/services/capacity';
import { renewalAlarm } from '@pig/api/src/services/contracts';
import { and, asc, eq, gt, ilike, inArray, isNotNull, isNull } from 'drizzle-orm';
import { and, asc, count, eq, gt, ilike, inArray, isNotNull, isNull } from 'drizzle-orm';
import { z } from 'zod';
import type { PiggyChatContext } from './chat';
import { createAccountLifecycleTool } from './lifecycle-tools';
import { createPagePigTools } from './page-tools';
import { atLeast, createPagePigTools, resultScope, type ResultScope } from './page-tools';
import { defineTool, type AgentTool } from './provider';
const noInput = z.object({}).strict();
/**
* The per-collection cap on a record read.
*
* Named rather than repeated as a literal because the scope below reports it:
* a related list that came back exactly full is a list that was probably cut,
* and a cut list the reader cannot see is how "this account has 100 deals"
* gets said about an account with three hundred.
*/
const RELATED_LIMIT = 100;
/**
* The scope of a record read.
*
* Unlike the page tools, a record read filters nothing — it enumerates what
* belongs to one row — so `matched` equals `total` and the sentence is not
* hedged. What it must still say is the boundary, because the failure here is
* the same shape as the /capacity one measured in production: asked how many
* deals are on the book while an account is in focus, a model with only this
* payload counts the four in front of it. `totalLabel` therefore names the
* record and says the figures stop there.
*/
function recordScope(subject: string, collections: Record<string, readonly unknown[]>): ResultScope {
const entries = Object.entries(collections);
const rows = entries.reduce((sum, [, list]) => sum + list.length, 0);
const capped = entries.some(([, list]) => list.length >= RELATED_LIMIT);
const breakdown = entries.map(([label, list]) => `${list.length} ${label}`).join(', ');
return resultScope({
covers: `belong to ${subject}`,
matched: rows,
total: rows,
totalLabel: `record(s) belonging to ${subject} and to no other — ${breakdown}; these are that record's own figures, never book-wide totals`,
listed: rows,
filters: capped ? { rowCapPerCollection: RELATED_LIMIT } : {},
truncated: capped,
});
}
/**
* Interactive chat gets one scoped read tool for where it is, plus the lookup
* layer, and no ambient access.
@@ -102,12 +139,24 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
const [account] = await db.select().from(accounts).where(eq(accounts.id, context.id)).limit(1);
if (!account) throw missingRecord(context.type, context.id);
const [people, demand, supply, paperwork] = await Promise.all([
db.select().from(contacts).where(eq(contacts.accountId, context.id)).limit(100),
db.select().from(demandDeals).where(eq(demandDeals.accountId, context.id)).limit(100),
db.select().from(supplyDeals).where(eq(supplyDeals.accountId, context.id)).limit(100),
db.select().from(contracts).where(eq(contracts.accountId, context.id)).limit(100),
db.select().from(contacts).where(eq(contacts.accountId, context.id)).limit(RELATED_LIMIT),
db.select().from(demandDeals).where(eq(demandDeals.accountId, context.id)).limit(RELATED_LIMIT),
db.select().from(supplyDeals).where(eq(supplyDeals.accountId, context.id)).limit(RELATED_LIMIT),
db.select().from(contracts).where(eq(contracts.accountId, context.id)).limit(RELATED_LIMIT),
]);
return { account, contacts: people, demandDeals: demand, supplyDeals: supply, contracts: paperwork };
return {
scope: recordScope(`the account ${account.name}`, {
'contact(s)': people,
'demand deal(s)': demand,
'supply deal(s)': supply,
'contract(s)': paperwork,
}),
account,
contacts: people,
demandDeals: demand,
supplyDeals: supply,
contracts: paperwork,
};
}
if (context.type === 'contact') {
@@ -116,7 +165,13 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
const [account] = contact.accountId
? await db.select().from(accounts).where(eq(accounts.id, contact.accountId)).limit(1)
: [];
return { contact, account: account ?? null };
return {
scope: recordScope(`the contact ${contact.fullName}`, {
'account(s)': account ? [account] : [],
}),
contact,
account: account ?? null,
};
}
if (context.type === 'demand_deal') {
@@ -127,8 +182,16 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
.select()
.from(allocations)
.where(eq(allocations.demandDealId, deal.id))
.limit(100);
return { deal, account: account ?? null, allocations: reservations };
.limit(RELATED_LIMIT);
return {
scope: recordScope(`the demand deal ${deal.name}`, {
'allocation(s)': reservations,
'account(s)': account ? [account] : [],
}),
deal,
account: account ?? null,
allocations: reservations,
};
}
if (context.type === 'supply_deal') {
@@ -139,8 +202,16 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
.select()
.from(capacityCommitments)
.where(eq(capacityCommitments.supplyDealId, deal.id))
.limit(100);
return { deal, account: account ?? null, commitments };
.limit(RELATED_LIMIT);
return {
scope: recordScope(`the supply deal ${deal.name}`, {
'capacity commitment(s)': commitments,
'account(s)': account ? [account] : [],
}),
deal,
account: account ?? null,
commitments,
};
}
if (context.type === 'commitment') {
@@ -154,8 +225,14 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
.select()
.from(allocations)
.where(eq(allocations.capacityCommitmentId, commitment.id))
.limit(100);
return { commitment, allocations: reservations };
.limit(RELATED_LIMIT);
return {
scope: recordScope(`the capacity commitment ${commitment.name}`, {
'allocation(s)': reservations,
}),
commitment,
allocations: reservations,
};
}
const [contract] = await db.select().from(contracts).where(eq(contracts.id, context.id)).limit(1);
@@ -166,16 +243,26 @@ async function readFocusedRecord(db: Database, context: PiggyRecordContext): Pro
.select()
.from(contractObligations)
.where(eq(contractObligations.contractId, contract.id))
.limit(100),
.limit(RELATED_LIMIT),
]);
const metrics = serviceLevels[0]
? await db
.select()
.from(slaMetricTargets)
.where(eq(slaMetricTargets.slaTermId, serviceLevels[0].id))
.limit(100)
.limit(RELATED_LIMIT)
: [];
return { contract, slaTerms: serviceLevels, slaMetricTargets: metrics, obligations };
return {
scope: recordScope(`the contract ${contract.title}`, {
'SLA term(s)': serviceLevels,
'SLA metric target(s)': metrics,
'obligation(s)': obligations,
}),
contract,
slaTerms: serviceLevels,
slaMetricTargets: metrics,
obligations,
};
}
// ---------------------------------------------------------------------------
@@ -416,7 +503,18 @@ async function searchRecords(db: Database, query: string): Promise<unknown> {
const fragment = likeFragment(query);
const take = SEARCH_PER_TYPE + 1;
const [accountRows, demandRows, supplyRows, contractRows, commitmentRows] = await Promise.all([
const [
accountRows,
demandRows,
supplyRows,
contractRows,
commitmentRows,
accountsAll,
demandAll,
supplyAll,
contractsAll,
commitmentsAll,
] = await Promise.all([
db
.select({
id: accounts.id,
@@ -482,6 +580,14 @@ async function searchRecords(db: Database, query: string): Promise<unknown> {
.from(capacityCommitments)
.where(ilike(capacityCommitments.name, fragment))
.limit(take),
// The five denominators. A search that reports only its hits invites
// "there are 3 accounts" from a book of twenty-three, and these counts also
// make this tool able to answer how many of a thing exist at all.
db.select({ value: count() }).from(accounts).where(isNull(accounts.archivedAt)),
db.select({ value: count() }).from(demandDeals),
db.select({ value: count() }).from(supplyDeals),
db.select({ value: count() }).from(contracts),
db.select({ value: count() }).from(capacityCommitments),
]);
const names = await accountNames(db, [
@@ -491,6 +597,8 @@ async function searchRecords(db: Database, query: string): Promise<unknown> {
...commitmentRows.map((row) => row.accountId),
]);
const rows = (result: readonly { value: number }[]): number => result[0]?.value ?? 0;
return assembleSearchResult(query, {
accounts: accountRows,
demandDeals: demandRows,
@@ -498,6 +606,13 @@ async function searchRecords(db: Database, query: string): Promise<unknown> {
contracts: contractRows,
commitments: commitmentRows,
accountNames: names,
totals: {
account: rows(accountsAll),
demand_deal: rows(demandAll),
supply_deal: rows(supplyAll),
contract: rows(contractsAll),
commitment: rows(commitmentsAll),
},
});
}
@@ -549,8 +664,18 @@ export interface SearchRowSets {
costPerGpuHourCents: number;
}[];
accountNames: ReadonlyMap<string, string>;
/**
* How many rows each searched table holds in total — the denominators the
* per-type match counts are drawn from. Keyed by the same names the results
* carry, so a model reading `counts.account: 1` beside `totals.account: 23`
* cannot mistake a name match for a census.
*/
totals: Record<SearchedRecordType, number>;
}
/** The five types a name search covers. People are deliberately not indexed. */
export type SearchedRecordType = Exclude<PiggyRecordType, 'contact'>;
/**
* Ranking, capping and counting, with no database in sight.
*
@@ -664,20 +789,35 @@ export function assembleSearchResult(query: string, sets: SearchRowSets): unknow
commitmentsCut.truncated;
const results = ranked.slice(0, SEARCH_RESULTS);
const truncated = perTypeTruncated || ranked.length > results.length;
const searched = Object.values(sets.totals).reduce((sum, rows) => sum + rows, 0);
const breakdown = Object.entries(counts)
.filter(([, matches]) => matches > 0)
.map(([type, matches]) => `${matches} of ${sets.totals[type as SearchedRecordType]} ${type}(s)`)
.join(', ');
return {
headline:
results.length === 0
? `No account, deal, contract or capacity commitment has a name containing "${query}".`
: `${truncated ? 'at least ' : ''}${ranked.length} record(s) match "${query}": ` +
Object.entries(counts)
.filter(([, count]) => count > 0)
.map(([type, count]) => `${count} ${type}(s)`)
.join(', ') +
'.',
? `None of the ${searched} account(s), deal(s), contract(s) and capacity commitment(s) ` +
`on the book has a name containing "${query}".`
: `${truncated ? 'At least ' : ''}${ranked.length} of ${searched} searchable record(s) ` +
`match "${query}": ${breakdown}. Those are name matches, not totals; the counts they ` +
'were drawn from are beside them.',
scope: resultScope({
covers: `have a name containing "${query}"`,
matched: ranked.length,
total: searched,
totalLabel:
'record(s) searchable by name: accounts, demand deals, supply deals, contracts and capacity commitments',
listed: results.length,
filters: { query },
truncated,
}),
query,
truncated,
counts,
/** The denominator for each entry in `counts`, keyed identically. */
totals: sets.totals,
results: results.map((entry) => entry.hit),
};
}
@@ -702,26 +842,36 @@ export function assembleSearchResult(query: string, sets: SearchRowSets): unknow
*/
async function listRenewals(db: Database, side: 'demand' | 'supply' | undefined): Promise<unknown> {
const now = new Date();
const rows = await db
.select({ contract: contracts, accountName: accounts.name })
.from(contracts)
.leftJoin(accounts, eq(accounts.id, contracts.accountId))
.where(
and(
eq(contracts.status, 'executed'),
isNull(contracts.terminatedAt),
isNotNull(contracts.expiresAt),
gt(contracts.expiresAt, now),
side ? eq(contracts.side, side) : undefined,
),
)
.orderBy(asc(contracts.expiresAt))
.limit(SCAN_LIMIT + 1);
// The denominator is counted on the same side filter the list uses, so
// "4 of 20" and "4 of 11 on the demand side" are both answers to the
// question that was actually asked.
const [rows, all] = await Promise.all([
db
.select({ contract: contracts, accountName: accounts.name })
.from(contracts)
.leftJoin(accounts, eq(accounts.id, contracts.accountId))
.where(
and(
eq(contracts.status, 'executed'),
isNull(contracts.terminatedAt),
isNotNull(contracts.expiresAt),
gt(contracts.expiresAt, now),
side ? eq(contracts.side, side) : undefined,
),
)
.orderBy(asc(contracts.expiresAt))
.limit(SCAN_LIMIT + 1),
db
.select({ value: count() })
.from(contracts)
.where(side ? eq(contracts.side, side) : undefined),
]);
return assembleRenewals(rows.slice(0, SCAN_LIMIT), {
now,
side,
truncated: rows.length > SCAN_LIMIT,
totalContracts: all[0]?.value ?? 0,
});
}
@@ -745,9 +895,15 @@ export interface RenewalRow {
*/
export function assembleRenewals(
rows: readonly RenewalRow[],
options: { now: Date; side?: 'demand' | 'supply'; truncated: boolean },
options: {
now: Date;
side?: 'demand' | 'supply';
truncated: boolean;
/** Every contract on this side, whatever its status. The denominator. */
totalContracts: number;
},
): unknown {
const { now, side, truncated } = options;
const { now, side, truncated, totalContracts } = options;
const renewals = rows
.flatMap(({ contract, accountName }) => {
// The query already requires an expiry; narrowing here rather than
@@ -788,26 +944,51 @@ export function assembleRenewals(
const statedValueCents = noticeOpen.reduce((sum, row) => sum + (row.valueCents ?? 0), 0);
const anyStatedValue = noticeOpen.some((row) => row.valueCents != null);
const sideLabel = side ? `${side}-side contract(s) on the book` : 'contract(s) on the book';
const listed = Math.min(renewals.length, EXEMPLARS);
return {
headline:
(nearest
? `${truncated ? 'At least ' : ''}${renewals.length} executed contract(s) still live` +
`${side ? ` on the ${side} side` : ''}. Nearest deadline: the ` +
? `${truncated ? 'At least ' : ''}${renewals.length} of ${totalContracts} ${sideLabel} ` +
'are executed and not yet expired' +
`${renewals.length > listed ? `; the nearest ${listed} are listed` : ''}. ` +
'Nearest deadline: the ' +
`${nearest.deadlineKind === 'renewal_notice' ? 'renewal notice' : 'expiry'} for ` +
`${nearest.title}${nearest.accountName ? ` (${nearest.accountName})` : ''} on ` +
`${nearest.deadlineAt.slice(0, 10)}` +
`${nearest.daysUntilDeadline < 0 ? ', which has already passed' : ''}.`
: `No executed contract${side ? ` on the ${side} side` : ''} has an expiry date ahead of it.`) +
: `None of the ${totalContracts} ${sideLabel} is executed with an expiry date ahead of it.`) +
(noticeOpen.length > 0
? ` ${noticeOpen.length} notice window(s) already open` +
? ` ${noticeOpen.length} of those ${renewals.length} have a notice window already open` +
(anyStatedValue
? `, covering ${formatCents(statedValueCents)} of stated contract value.`
: '; none of those contracts states a value of its own.')
: ''),
scope: resultScope({
covers: 'are executed, not terminated and not yet expired',
matched: renewals.length,
total: totalContracts,
totalLabel: sideLabel,
listed,
filters: { side: side ?? 'both', status: 'executed', expired: 'excluded' },
truncated,
}),
side: side ?? 'both',
truncated,
count: renewals.length,
totalContracts,
noticeWindowOpenCount: noticeOpen.length,
/** A filter over a filter, so it states its own denominator too. */
noticeWindowOpenScope: resultScope({
covers: 'have a renewal-notice window that is already open',
matched: noticeOpen.length,
total: renewals.length,
totalLabel: `executed, unexpired ${sideLabel}`,
listed: 0,
filters: { renewalState: 'due' },
truncated,
}),
renewals: renewals.slice(0, EXEMPLARS),
};
}
@@ -836,11 +1017,19 @@ interface InventoryQuery {
* bounded read. The width never leaves this process; only EXEMPLARS rows do.
*/
async function listInventory(db: Database, query: InventoryQuery): Promise<unknown> {
const listings = await new CapacityService(db).searchInventory({
minGpuCount: query.minGpuCount,
requiresHighSpeedInterconnect: query.requiresFastInterconnect,
limit: SCAN_LIMIT,
});
const capacity = new CapacityService(db);
// The unfiltered read is the denominator, and it is taken through the same
// service rather than counted here: the service decides what "purchasable"
// means (it drops Unavailable stock), and a denominator computed from a
// second definition of that word would disagree with its own numerator.
const [listings, market] = await Promise.all([
capacity.searchInventory({
minGpuCount: query.minGpuCount,
requiresHighSpeedInterconnect: query.requiresFastInterconnect,
limit: SCAN_LIMIT,
}),
capacity.searchInventory({ limit: SCAN_LIMIT }),
]);
const providerNames = await accountNames(
db,
listings.flatMap((listing) => (listing.accountId ? [listing.accountId] : [])),
@@ -850,6 +1039,8 @@ async function listInventory(db: Database, query: InventoryQuery): Promise<unkno
// available that there was more behind it.
truncated: listings.length >= SCAN_LIMIT,
providerNames,
totalListings: market.length,
totalTruncated: market.length >= SCAN_LIMIT,
});
}
@@ -879,9 +1070,15 @@ export type InventoryOffer = Pick<
export function assembleInventoryResult(
query: InventoryQuery,
listings: readonly InventoryOffer[],
options: { truncated: boolean; providerNames: ReadonlyMap<string, string> },
options: {
truncated: boolean;
providerNames: ReadonlyMap<string, string>;
/** Purchasable listings on the market with no filter applied at all. */
totalListings: number;
totalTruncated: boolean;
},
): unknown {
const { truncated, providerNames: providers } = options;
const { truncated, providerNames: providers, totalListings, totalTruncated } = options;
const needle = query.gpuType?.toLowerCase();
const matched = needle
? listings.filter((listing) => listing.gpuType.toLowerCase().includes(needle))
@@ -894,23 +1091,43 @@ export function assembleInventoryResult(
);
const cheapest = ranked.find((listing) => listing.onDemandPriceCents != null);
const filters = {
gpuType: query.gpuType ?? null,
minGpuCount: query.minGpuCount ?? null,
requiresFastInterconnect: query.requiresFastInterconnect ?? false,
};
const anyFilter = Object.values(filters).some((value) => value !== null && value !== false);
const listed = Math.min(ranked.length, EXEMPLARS);
const market = 'purchasable listing(s) on the market';
return {
headline:
ranked.length === 0
? `No provider is currently listing capacity matching that request${query.gpuType ? ` for ${query.gpuType}` : ''}.`
: `${truncated ? 'At least ' : ''}${ranked.length} purchasable listing(s)` +
`${query.gpuType ? ` matching ${query.gpuType}` : ''}` +
? `None of the ${atLeast(totalListings, totalTruncated)} ${market} matches that ` +
`request${query.gpuType ? ` for ${query.gpuType}` : ''}.`
: `${ranked.length} of ${atLeast(totalListings, totalTruncated)} ${market} match` +
`${anyFilter ? ' the filters given' : ' (no filter was applied)'}` +
`${query.gpuType ? `, including ${query.gpuType}` : ''}` +
(cheapest
? `; cheapest on-demand is ${formatCents(cheapest.onDemandPriceCents ?? 0)} per ` +
`GPU-hour for ${cheapest.gpuType}.`
: '; none of them carry a published on-demand price.'),
: '; none of them carry a published on-demand price.') +
` ${listed} listed here.`,
scope: resultScope({
covers: anyFilter ? 'match the filters given' : 'are purchasable',
matched: ranked.length,
total: totalListings,
totalLabel: market,
listed,
// An unasked-for filter is not a filter: passing the three nulls through
// would have an unfiltered result describe itself as a slice.
filters: anyFilter ? filters : {},
truncated: truncated || totalTruncated,
}),
truncated,
count: ranked.length,
filters: {
gpuType: query.gpuType ?? null,
minGpuCount: query.minGpuCount ?? null,
requiresFastInterconnect: query.requiresFastInterconnect ?? false,
},
totalListings,
filters,
listings: ranked.slice(0, EXEMPLARS).map((listing) => shapeListing(listing, providers)),
};
}
+26 -5
View File
@@ -9,12 +9,16 @@ import {
demandDeals,
type Database,
} from '@pig/db';
import { and, desc, eq, inArray } from 'drizzle-orm';
import { and, count, desc, eq, inArray } from 'drizzle-orm';
import { z } from 'zod';
import { resultScope } from './page-tools';
import { defineTool, type AgentTool } from './provider';
const noInput = z.object({}).strict();
/** The per-collection cap here, matching `RELATED_LIMIT` in chat-tools. */
const RELATED_LIMIT = 100;
export function createAccountLifecycleTool(db: Database, accountId: string): AgentTool {
return defineTool({
name: 'pig_get_account_lifecycle',
@@ -23,11 +27,17 @@ export function createAccountLifecycleTool(db: Database, accountId: string): Age
execute: async () => {
const [account] = await db.select().from(accounts).where(eq(accounts.id, accountId)).limit(1);
if (!account) throw new Error('The account in focus no longer exists.');
const [deals, paperwork, recentActivity] = await Promise.all([
db.select().from(demandDeals).where(eq(demandDeals.accountId, accountId)).limit(100),
db.select().from(contracts).where(and(eq(contracts.accountId, accountId), eq(contracts.side, 'demand'))).limit(100),
const [deals, paperwork, recentActivity, dealsOnBook] = await Promise.all([
db.select().from(demandDeals).where(eq(demandDeals.accountId, accountId)).limit(RELATED_LIMIT),
db.select().from(contracts).where(and(eq(contracts.accountId, accountId), eq(contracts.side, 'demand'))).limit(RELATED_LIMIT),
db.select().from(activities).where(eq(activities.accountId, accountId)).orderBy(desc(activities.occurredAt)).limit(1),
// The denominator. This result is one account's slice of the book and
// every count in it is an account count; without the book's own figure
// beside them, "4 demand deals" is the only deal number in the payload
// and becomes the answer to a question about the whole book.
db.select({ value: count() }).from(demandDeals),
]);
const demandDealsOnBook = dealsOnBook[0]?.value ?? 0;
const dealIds = deals.map((deal) => deal.id);
const contractIds = paperwork.map((contract) => contract.id);
const [requests, reservations, obligations] = await Promise.all([
@@ -36,7 +46,18 @@ export function createAccountLifecycleTool(db: Database, accountId: string): Age
contractIds.length ? db.select().from(contractObligations).where(inArray(contractObligations.contractId, contractIds)) : [],
]);
return {
scope: resultScope({
covers: `belong to the account ${account.name}`,
matched: deals.length,
total: demandDealsOnBook,
totalLabel: 'demand deal(s) on the book',
listed: 0,
filters: { accountId, side: 'demand', rowCapPerCollection: RELATED_LIMIT },
truncated: deals.length >= RELATED_LIMIT || paperwork.length >= RELATED_LIMIT,
}),
account: { id: account.id, name: account.name },
demandDealsForThisAccount: deals.length,
demandDealsOnBook,
lifecycle: evaluateCustomerLifecycle({
accountId,
deals: deals.map((deal) => ({ ...deal })),
@@ -47,7 +68,7 @@ export function createAccountLifecycleTool(db: Database, accountId: string): Age
lastActivityAt: recentActivity[0]?.occurredAt ?? account.lastActivityAt,
lastActivityId: recentActivity[0]?.id,
}),
interpretation: 'Scores rank review attention. Capacity totals mean sold or reserved capacity, not customer workload utilization.',
interpretation: 'Scores rank review attention. Capacity totals mean sold or reserved capacity, not customer workload utilisation. Every figure here covers this one account, never the book.',
};
},
});
+319 -31
View File
@@ -48,7 +48,7 @@ import {
type Database,
} from '@pig/db';
import { CalendarService } from '@pig/api/src/services/calendar';
import { and, gte, inArray, isNull } from 'drizzle-orm';
import { and, count, gte, inArray, isNull } from 'drizzle-orm';
import { z } from 'zod';
import { piggyPageGuide, type PiggyPageToolName } from './page-routes';
import { defineTool, type AgentTool } from './provider';
@@ -84,8 +84,107 @@ const TRUNCATION_NOTE =
'book — present them as a lower bound, not as the whole.';
/** Prefixes a count the model must not read as exact. */
function atLeast(count: number, truncated: boolean): string {
return truncated ? `at least ${count}` : `${count}`;
export function atLeast(rows: number, truncated: boolean): string {
return truncated ? `at least ${rows}` : `${rows}`;
}
/**
* What a result covers, and what it was drawn from.
*
* Measured in production on /capacity, an hour before this was written. Asked
* "How many capacity commitments are on the book?", the model called
* `pig_get_idle_capacity` — the only tool that page offers — and answered "3".
* The book held 5. The tool filters to blocks at least 25% unsold, so 3 was the
* size of a filter; nothing in the payload said so, and reading the length of
* the list it had been handed as the size of the book was the only reading the
* data supported.
*
* The system prompt already forbade exactly that, naming this exact tool, and
* the model did it anyway. Prompting a 30B model out of a mistake its data
* invites does not work, so the data stopped inviting it: every result here
* that carries a count or a collection carries this object beside it, naming
* the filter, the denominator it was drawn from, and how much of the matched
* set is actually listed. A filtered count is therefore never the only number
* in its own result.
*
* One shape to learn rather than one per tool — a different shape per tool is
* how this happened. The result's primary subject gets a top-level `scope`;
* every other count or collection in the same payload gets its own, nested
* where the payload already groups it and named `<field>Scope` where it does
* not. And `summary` restates the numbers as prose on purpose — it is the
* field a small model quotes, and a figure it has to assemble out of three
* other fields is a figure it will assemble wrongly.
*
* `filters` is not decoration either. This product has shipped three different
* idle figures across three surfaces because each applied its own threshold, so
* a result that does not name the threshold it used cannot be reconciled with
* the screen beside it.
*/
export interface ResultScope {
/** The whole scope in one sentence, figures included. Quote this. */
summary: string;
/** What made a row match, as a clause: "are at least 25% unsold". */
covers: string;
/** How many rows matched. Never the answer to "how many are there". */
matched: number;
/** The set `matched` was drawn from. This is the total. */
total: number;
/** What `total` counts, as a noun phrase. */
totalLabel: string;
/** How many of `matched` this payload lists. The rest are counted only. */
listed: number;
/** Every filter applied, named, so a threshold is never invisible. */
filters: Record<string, string | number | boolean | null>;
/** True when a read hit its row cap, so both figures are lower bounds. */
truncated: boolean;
}
export function resultScope(input: {
covers: string;
matched: number;
total: number;
totalLabel: string;
listed: number;
filters?: Record<string, string | number | boolean | null>;
truncated?: boolean;
}): ResultScope {
const { covers, matched, total, totalLabel, listed } = input;
const filters = input.filters ?? {};
const truncated = input.truncated ?? false;
// Nothing was filtered out, so `matched` IS the total and saying otherwise
// would teach the model to distrust a figure that is exact.
const unfiltered = matched === total && Object.keys(filters).length === 0;
const listedClause = listed > 0 ? `; ${listed} listed here` : '';
// Both figures are hedged together when a read was cut. Hedging only the
// total would present a capped `matched` as exact, which is the same class of
// overstatement this whole object exists to stop.
const summary = unfiltered
? `All ${atLeast(total, truncated)} ${totalLabel}${listedClause}.`
: `${atLeast(matched, truncated)} of ${atLeast(total, truncated)} ${totalLabel} ` +
`${covers}${listedClause}. That is a filtered count — the total is ` +
`${atLeast(total, truncated)} ${totalLabel}.`;
return {
summary: truncated ? `${summary} ${TRUNCATION_NOTE}` : summary,
covers,
matched,
total,
totalLabel,
listed,
filters,
truncated,
};
}
/** The denominator every commitment figure in this file is drawn from. */
const COMMITMENTS_LABEL = 'live capacity commitment(s) on the book';
/** The denominators the two pipelines are drawn from. */
const DEMAND_DEALS_LABEL = 'demand deal(s) on the book';
const SUPPLY_DEALS_LABEL = 'supply deal(s) on the book';
/** One whole-table count, for use as a denominator. */
function rowCount(rows: readonly { value: number }[]): number {
return rows[0]?.value ?? 0;
}
/**
@@ -306,9 +405,24 @@ async function readMarginSummary(db: Database): Promise<unknown> {
headline:
`Revenue ${formatCents(totals.revenueCents)} against cost ${formatCents(totals.costCents)}; ` +
`gross margin ${formatCents(totals.grossMarginCents)} (${percent(totals.grossMarginPct)}) ` +
`at ${percent(totals.utilisation)} utilisation across ` +
`${atLeast(blocks.length, truncated)} live commitment(s).` +
`at ${percent(totals.utilisation)} utilisation across all ` +
`${atLeast(blocks.length, truncated)} ${COMMITMENTS_LABEL} — the whole book, unfiltered. ` +
`The ${largest.length} largest by cost are listed; the book holds ` +
`${atLeast(blocks.length, truncated)}.` +
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
/**
* Unfiltered, and the only tool here that is: `matched` equals `total`, so
* `liveCommitments` below is a real answer to "how many are on the book".
* `largestBlocks` is still a slice, which is what `listed` is for.
*/
scope: resultScope({
covers: 'are live',
matched: blocks.length,
total: blocks.length,
totalLabel: COMMITMENTS_LABEL,
listed: largest.length,
truncated,
}),
truncated,
totals: {
revenueCents: totals.revenueCents,
@@ -332,13 +446,27 @@ async function readMarginSummary(db: Database): Promise<unknown> {
};
}
/**
* The threshold and horizon this tool filters on.
*
* The same defaults the API and MCP use — and NOT the same as the workspace
* summary's worst-idle list, which takes any block with idle hours at all.
* Both are correct for what they answer and they return different counts, so
* each states its own threshold in `filters` rather than leaving the reader to
* reconcile two figures that were never the same figure.
*/
const IDLE_THRESHOLD_PCT = 0.25;
const IDLE_WITHIN_DAYS = 30;
/** Idle blocks, on the same defaults the API and MCP use: 25% within 30 days. */
async function readIdleCapacity(db: Database): Promise<unknown> {
const now = new Date();
const horizon = new Date(now.getTime() + 30 * 86_400_000);
const horizon = new Date(now.getTime() + IDLE_WITHIN_DAYS * 86_400_000);
const { blocks, truncated } = await readLiveBlocks(db, now);
const idle = blocks
.filter((block) => block.startsAt <= horizon && 1 - block.margin.utilisation >= 0.25)
.filter(
(block) => block.startsAt <= horizon && 1 - block.margin.utilisation >= IDLE_THRESHOLD_PCT,
)
.map((block) => ({
block,
idleGpuHours: block.margin.idleGpuHours,
@@ -348,19 +476,42 @@ async function readIdleCapacity(db: Database): Promise<unknown> {
.sort((a, b) => b.idleCostCents - a.idleCostCents);
const totalIdleCostCents = idle.reduce((sum, row) => sum + row.idleCostCents, 0);
const listed = idle.slice(0, EXEMPLARS);
const filter =
`are at least ${IDLE_THRESHOLD_PCT * 100}% unsold and start within ` +
`${IDLE_WITHIN_DAYS} day(s)`;
return {
/**
* The sentence the production defect was answered from, so it carries the
* denominator first and the filtered figure second. "3" alone was true of
* the filter and false of the book; "3 of 5" cannot be misread as 5.
*/
headline:
(idle.length === 0
? 'No live block is more than 25% unsold within the next 30 days.'
: `${atLeast(idle.length, truncated)} block(s) at least 25% unsold within 30 days, ` +
`${formatCents(totalIdleCostCents)} of capacity bought and not yet earning.`) +
? `None of the ${atLeast(blocks.length, truncated)} ${COMMITMENTS_LABEL} ${filter}.`
: `${idle.length} of ${atLeast(blocks.length, truncated)} ${COMMITMENTS_LABEL} ${filter}, ` +
`${formatCents(totalIdleCostCents)} of capacity bought and not yet earning. ` +
`${idle.length} is a filtered count — the book holds ` +
`${atLeast(blocks.length, truncated)} live commitment(s) in total.`) +
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
scope: resultScope({
covers: filter,
matched: idle.length,
total: blocks.length,
totalLabel: COMMITMENTS_LABEL,
listed: listed.length,
filters: { idleThresholdPct: IDLE_THRESHOLD_PCT, withinDays: IDLE_WITHIN_DAYS },
truncated,
}),
truncated,
thresholdPct: 0.25,
withinDays: 30,
/** The denominator, repeated as a bare field: this is the size of the book. */
liveCommitments: blocks.length,
thresholdPct: IDLE_THRESHOLD_PCT,
withinDays: IDLE_WITHIN_DAYS,
idleBlocks: idle.length,
totalIdleCostCents,
blocks: idle.slice(0, EXEMPLARS).map((row) => ({
blocks: listed.map((row) => ({
name: row.block.name,
gpuType: row.block.gpuType,
gpuCount: row.block.gpuCount,
@@ -379,7 +530,10 @@ async function readIdleCapacity(db: Database): Promise<unknown> {
// ---------------------------------------------------------------------------
async function readPipeline(db: Database): Promise<unknown> {
const [demandRead, supplyRead] = await Promise.all([
// The two whole-table counts are the denominators. Without them "12 open
// demand deals" is a filtered count with nothing to be filtered from, and
// "how many deals do we have" is answered with the number of open ones.
const [demandRead, supplyRead, demandAll, supplyAll] = await Promise.all([
db
.select()
.from(demandDeals)
@@ -390,10 +544,17 @@ async function readPipeline(db: Database): Promise<unknown> {
.from(supplyDeals)
.where(inArray(supplyDeals.stage, [...SUPPLY_OPEN_STAGES]))
.limit(SCAN_LIMIT + 1),
db.select({ value: count() }).from(demandDeals),
db.select({ value: count() }).from(supplyDeals),
]);
const { rows: demand, truncated: demandTruncated } = bounded(demandRead);
const { rows: supply, truncated: supplyTruncated } = bounded(supplyRead);
const truncated = demandTruncated || supplyTruncated;
const demandTotal = rowCount(demandAll);
const supplyTotal = rowCount(supplyAll);
const demandListed = Math.min(demand.length, EXEMPLARS);
const supplyListed = Math.min(supply.length, EXEMPLARS);
const openStages = 'are at an open stage (not closed, not lost)';
// Total contract value where it is known, annual value otherwise: a deal
// valued only by ACV is still worth counting, and treating it as zero would
@@ -403,13 +564,35 @@ async function readPipeline(db: Database): Promise<unknown> {
return {
headline:
`${atLeast(demand.length, demandTruncated)} open demand deal(s) worth ` +
`${formatCents(demandValueCents)} and ${atLeast(supply.length, supplyTruncated)} ` +
'open supply deal(s).' +
`${demand.length} of ${atLeast(demandTotal, demandTruncated)} ${DEMAND_DEALS_LABEL} are open, ` +
`worth ${formatCents(demandValueCents)}, and ${supply.length} of ` +
`${atLeast(supplyTotal, supplyTruncated)} ${SUPPLY_DEALS_LABEL} are open — ` +
`${demand.length + supply.length} of ${atLeast(demandTotal + supplyTotal, truncated)} ` +
'deal(s) on the book in all. Those are open-stage counts, not the size of either pipeline.' +
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
// Both sides together, so a question about "deals" has a denominator too.
scope: resultScope({
covers: openStages,
matched: demand.length + supply.length,
total: demandTotal + supplyTotal,
totalLabel: 'deal(s) on the book, demand and supply together',
listed: demandListed + supplyListed,
filters: { stages: 'open only' },
truncated,
}),
truncated: { demandDeals: demandTruncated, supplyDeals: supplyTruncated },
demand: {
scope: resultScope({
covers: openStages,
matched: demand.length,
total: demandTotal,
totalLabel: DEMAND_DEALS_LABEL,
listed: demandListed,
filters: { stages: [...DEMAND_OPEN_STAGES].join(', ') },
truncated: demandTruncated,
}),
openDeals: demand.length,
totalDeals: demandTotal,
valueCents: demandValueCents,
byStage: countByStage(demand.map((deal) => deal.stage)),
largest: [...demand]
@@ -423,7 +606,17 @@ async function readPipeline(db: Database): Promise<unknown> {
})),
},
supply: {
scope: resultScope({
covers: openStages,
matched: supply.length,
total: supplyTotal,
totalLabel: SUPPLY_DEALS_LABEL,
listed: supplyListed,
filters: { stages: [...SUPPLY_OPEN_STAGES].join(', ') },
truncated: supplyTruncated,
}),
openDeals: supply.length,
totalDeals: supplyTotal,
byStage: countByStage(supply.map((deal) => deal.stage)),
largest: [...supply]
.sort((a, b) => (b.gpuCount ?? 0) - (a.gpuCount ?? 0))
@@ -510,10 +703,22 @@ async function readCalendarAhead(db: Database, withinDays: number): Promise<unkn
const overdue = behind.events.filter((event) => event.state === 'overdue');
const truncated = ahead.truncated || behind.truncated;
const upcomingByKind = countByKind(upcoming);
const upcomingListed = Math.min(upcoming.length, EXEMPLARS * 2);
const overdueListed = Math.min(overdue.length, EXEMPLARS);
/**
* Both denominators are windows, not the book, and the labels say so. Nothing
* here can answer "how many obligations are there" — only how many fall in
* these dates — so a label that read "obligations on the book" would be the
* same lie in a different tool.
*/
const windowLabel = `dated item(s) falling in the next ${withinDays} day(s), done or not`;
const lookbackLabel = `dated item(s) in the last ${OVERDUE_LOOKBACK_DAYS} day(s) that can fall late`;
return {
headline:
`Next ${withinDays} day(s): ${atLeast(upcoming.length, ahead.truncated)} dated item(s) ` +
`Next ${withinDays} day(s) only, not the whole book: ` +
`${atLeast(upcoming.length, ahead.truncated)} of ` +
`${atLeast(ahead.events.length, ahead.truncated)} dated item(s) ` +
`across ${Object.keys(upcomingByKind).length} kind(s), of which ` +
`${ahead.totals.obligationCount} obligation(s) due, ${ahead.totals.closingCount} demand ` +
`deal(s) expected to close worth ${formatCents(ahead.totals.weightedPipelineCents)} ` +
@@ -522,6 +727,15 @@ async function readCalendarAhead(db: Database, withinDays: number): Promise<unkn
`${atLeast(overdue.length, behind.truncated)} item(s) overdue in the last ` +
`${OVERDUE_LOOKBACK_DAYS} day(s).` +
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
scope: resultScope({
covers: 'are still outstanding',
matched: upcoming.length,
total: ahead.events.length,
totalLabel: windowLabel,
listed: upcomingListed,
filters: { withinDays, state: 'excludes done' },
truncated: ahead.truncated,
}),
withinDays,
truncated,
/**
@@ -536,8 +750,11 @@ async function readCalendarAhead(db: Database, withinDays: number): Promise<unkn
renewalNotices: ahead.totals.renewalCount,
expiringExportAuthorizations: ahead.totals.expiringAuthorizationCount,
},
// The top-level `scope` is this list's: upcoming work is what the tool is
// for, and a second copy of the same eight fields is eight fields of budget.
upcoming: {
count: upcoming.length,
inWindow: ahead.events.length,
truncated: ahead.truncated,
byKind: upcomingByKind,
byState: countByState(upcoming),
@@ -545,6 +762,18 @@ async function readCalendarAhead(db: Database, withinDays: number): Promise<unkn
events: upcoming.slice(0, EXEMPLARS * 2).map(exemplar),
},
overdue: {
scope: resultScope({
covers: 'have lapsed without being completed',
matched: overdue.length,
total: behind.events.length,
totalLabel: lookbackLabel,
listed: overdueListed,
filters: {
lookbackDays: OVERDUE_LOOKBACK_DAYS,
kinds: [...OVERDUE_KINDS].join(', '),
},
truncated: behind.truncated,
}),
count: overdue.length,
truncated: behind.truncated,
lookbackDays: OVERDUE_LOOKBACK_DAYS,
@@ -597,7 +826,7 @@ function countByState(events: readonly CalendarEvent[]): Record<string, number>
* answered from the fragment it happened to receive.
*/
async function readWorkspaceSummary(db: Database): Promise<unknown> {
const [book, demandRead, supplyRead] = await Promise.all([
const [book, demandRead, supplyRead, demandAll, supplyAll] = await Promise.all([
readLiveBlocks(db),
db
.select({ id: demandDeals.id })
@@ -609,8 +838,12 @@ async function readWorkspaceSummary(db: Database): Promise<unknown> {
.from(supplyDeals)
.where(inArray(supplyDeals.stage, [...SUPPLY_OPEN_STAGES]))
.limit(SCAN_LIMIT + 1),
db.select({ value: count() }).from(demandDeals),
db.select({ value: count() }).from(supplyDeals),
]);
const { blocks } = book;
const demandTotal = rowCount(demandAll);
const supplyTotal = rowCount(supplyAll);
const { rows: demand, truncated: demandTruncated } = bounded(demandRead);
const { rows: supply, truncated: supplyTruncated } = bounded(supplyRead);
const truncated = {
@@ -620,23 +853,42 @@ async function readWorkspaceSummary(db: Database): Promise<unknown> {
};
const anyTruncated = Object.values(truncated).some(Boolean);
const totals = bookTotals(blocks);
const worstIdle = [...blocks]
/**
* Any idle at all, which is a THIRD threshold — `pig_get_idle_capacity` uses
* 25% and the web uses its own. Three surfaces have quoted three different
* idle counts for one book because of exactly this, so the scope below names
* the threshold rather than leaving the reader to guess which one produced
* the number in front of them.
*/
const withIdle = [...blocks]
.filter((block) => block.margin.idleGpuHours > 0)
.sort(
(a, b) =>
b.margin.idleGpuHours * b.costPerGpuHourCents -
a.margin.idleGpuHours * a.costPerGpuHourCents,
)
.slice(0, 3);
);
const worstIdle = withIdle.slice(0, 3);
return {
headline:
`${atLeast(blocks.length, truncated.commitments)} live commitment(s) at ` +
`All ${atLeast(blocks.length, truncated.commitments)} ${COMMITMENTS_LABEL} at ` +
`${percent(totals.utilisation)} utilisation; ` +
`gross margin ${formatCents(totals.grossMarginCents)}; ` +
`${atLeast(demand.length, demandTruncated)} open demand and ` +
`${atLeast(supply.length, supplyTruncated)} open supply deal(s).` +
`${demand.length} of ${atLeast(demandTotal, demandTruncated)} ${DEMAND_DEALS_LABEL} ` +
`and ${supply.length} of ${atLeast(supplyTotal, supplyTruncated)} ${SUPPLY_DEALS_LABEL} ` +
`are open. The ${worstIdle.length} block(s) listed below are the worst idle of ` +
`${withIdle.length} with any idle hours, not the whole book.` +
(anyTruncated ? ` ${TRUNCATION_NOTE}` : ''),
// The book, unfiltered: `matched` equals `total`, so this is the figure to
// quote when someone asks how large the book is.
scope: resultScope({
covers: 'are live',
matched: blocks.length,
total: blocks.length,
totalLabel: COMMITMENTS_LABEL,
listed: 0,
truncated: truncated.commitments,
}),
truncated,
book: {
liveCommitments: blocks.length,
@@ -646,14 +898,50 @@ async function readWorkspaceSummary(db: Database): Promise<unknown> {
utilisation: totals.utilisation,
idleGpuHours: Math.round(totals.idleGpuHours),
},
// Two filtered counts, each next to the denominator it came from. Without
// `totalDemandDeals` beside it, `openDemandDeals` is the only deal figure
// in the payload and becomes the answer to "how many deals do we have".
openDemandDeals: demand.length,
totalDemandDeals: demandTotal,
openDemandDealsScope: resultScope({
covers: 'are at an open stage (not closed, not lost)',
matched: demand.length,
total: demandTotal,
totalLabel: DEMAND_DEALS_LABEL,
listed: 0,
filters: { stages: [...DEMAND_OPEN_STAGES].join(', ') },
truncated: demandTruncated,
}),
openSupplyDeals: supply.length,
worstIdleBlocks: worstIdle.map((block) => ({
name: block.name,
gpuType: block.gpuType,
idleGpuHours: Math.round(block.margin.idleGpuHours),
idleCostCents: Math.round(block.margin.idleGpuHours * block.costPerGpuHourCents),
})),
totalSupplyDeals: supplyTotal,
openSupplyDealsScope: resultScope({
covers: 'are at an open stage (not closed, not lost)',
matched: supply.length,
total: supplyTotal,
totalLabel: SUPPLY_DEALS_LABEL,
listed: 0,
filters: { stages: [...SUPPLY_OPEN_STAGES].join(', ') },
truncated: supplyTruncated,
}),
worstIdle: {
scope: resultScope({
covers: 'have any unsold hours at all',
matched: withIdle.length,
total: blocks.length,
totalLabel: COMMITMENTS_LABEL,
listed: worstIdle.length,
// Not 0.25. This list and pig_get_idle_capacity answer different
// questions and will disagree; the thresholds say which is which.
filters: { idleThresholdPct: 0 },
truncated: truncated.commitments,
}),
blocks: worstIdle.map((block) => ({
name: block.name,
gpuType: block.gpuType,
idleGpuHours: Math.round(block.margin.idleGpuHours),
idleCostCents: Math.round(block.margin.idleGpuHours * block.costPerGpuHourCents),
})),
},
};
}