Merge gitea/main into the Motion branch
Motion was written against a base five commits behind main, so the integration is the interesting part of this commit: - The migration is renumbered 0014 -> 0015. Main shipped 0014_piggy_conversations, and two migrations sharing an index is a journal that applies one of them. - The seed-idempotency gate keeps main's all-tables diff rather than the motion_templates counter this branch added; the general check subsumes the specific one. - Nav gains a Motion group alongside main's new Workspace group, and Piggy keeps the mark main gave it. - Stat keeps main's container-scaled figure, which already carries the min-w-0 this branch added for the same reason. - Piggy's page labels keep main's refusal wording for the four pages with no tool of their own, and gain the three Motion routes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+481
-34
@@ -26,6 +26,7 @@
|
||||
* leaves this process.
|
||||
*/
|
||||
import {
|
||||
ACCOUNT_SIDES,
|
||||
CONSUMING_ALLOCATION_STATUSES,
|
||||
DEMAND_OPEN_STAGES,
|
||||
DEMAND_STAGES,
|
||||
@@ -48,6 +49,7 @@ import {
|
||||
accounts,
|
||||
allocations,
|
||||
capacityCommitments,
|
||||
contacts,
|
||||
demandDeals,
|
||||
engagementArtifacts,
|
||||
engagements,
|
||||
@@ -57,7 +59,19 @@ import {
|
||||
type Database,
|
||||
} from '@pig/db';
|
||||
import { CalendarService } from '@pig/api/src/services/calendar';
|
||||
import { and, desc, eq, gte, ilike, inArray, isNotNull, isNull, or, type SQL } from 'drizzle-orm';
|
||||
import {
|
||||
and,
|
||||
count,
|
||||
desc,
|
||||
eq,
|
||||
gte,
|
||||
ilike,
|
||||
inArray,
|
||||
isNotNull,
|
||||
isNull,
|
||||
or,
|
||||
type SQL,
|
||||
} from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { likeFragment } from './chat-tools';
|
||||
import { piggyPageGuide, type PiggyPageToolName } from './page-routes';
|
||||
@@ -94,8 +108,111 @@ 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';
|
||||
|
||||
/** The denominators the two party tables are drawn from. */
|
||||
const ACCOUNTS_LABEL = 'account(s) on the book';
|
||||
const CONTACTS_LABEL = 'contact(s) in the CRM';
|
||||
|
||||
/** One whole-table count, for use as a denominator. */
|
||||
function rowCount(rows: readonly { value: number }[]): number {
|
||||
return rows[0]?.value ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,9 +358,11 @@ function pageTool(db: Database, name: PiggyPageToolName): AgentTool {
|
||||
return defineTool({
|
||||
name,
|
||||
description:
|
||||
'Read a bounded overview of the PIG workspace: book margin and utilisation, open ' +
|
||||
'deal counts on both sides, and the worst idle capacity. This cannot inspect the ' +
|
||||
'filesystem or external systems.',
|
||||
'Read a bounded overview of the PIG workspace: how many accounts (by side) and ' +
|
||||
'contacts are on the book, book margin and utilisation, how many deals exist and how ' +
|
||||
'many are open on each side, and the worst idle capacity. Counts only — it returns no ' +
|
||||
'account, contact or deal rows, and it cannot see the team, settings, imports or ' +
|
||||
'facts. This cannot inspect the filesystem or external systems.',
|
||||
inputSchema: noInput,
|
||||
execute: async () => readWorkspaceSummary(db),
|
||||
});
|
||||
@@ -382,9 +501,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,
|
||||
@@ -408,13 +542,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,
|
||||
@@ -424,19 +572,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,
|
||||
@@ -455,7 +626,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)
|
||||
@@ -466,10 +640,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
|
||||
@@ -479,13 +660,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]
|
||||
@@ -499,7 +702,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))
|
||||
@@ -586,10 +799,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)} ` +
|
||||
@@ -598,6 +823,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,
|
||||
/**
|
||||
@@ -612,8 +846,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),
|
||||
@@ -621,6 +858,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,
|
||||
@@ -662,6 +911,93 @@ function countByState(events: readonly CalendarEvent[]): Record<string, number>
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The parties
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface BookParties {
|
||||
/** What the /accounts list shows: every account that is not archived. */
|
||||
onBook: number;
|
||||
/** Archived accounts, excluded from `onBook` and counted so the gap is visible. */
|
||||
archived: number;
|
||||
/** The book partitioned by side. The three buckets sum to `onBook`. */
|
||||
bySide: Record<string, number>;
|
||||
/** Every contact row, which is what the contacts tab lists. */
|
||||
contacts: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many accounts and contacts the book holds.
|
||||
*
|
||||
* Measured in production on /accounts, minutes before this was written. Asked
|
||||
* "How many accounts are on the book in total? One sentence.", Piggy answered
|
||||
* "The book contains 7 demand deals (accounts) in total." The book held 17
|
||||
* accounts and 7 demand deals — so the figure was real, the payload had
|
||||
* correctly scoped it as deals, and the prose relabelled it as accounts.
|
||||
*
|
||||
* This tool is the fallback for /accounts and five other routes, and it carried
|
||||
* commitments, deals, margin and idle hours: no count of accounts or contacts
|
||||
* anywhere. Asked about accounts with no account figure in front of it, the
|
||||
* model reached for the nearest countable thing. That is the sibling of the
|
||||
* defect `ResultScope` was built for — one substitutes the size of a filter for
|
||||
* a total, this one substitutes another noun's total for a total that is simply
|
||||
* absent — and the cure for an absent number is not a firmer instruction. It is
|
||||
* the number.
|
||||
*
|
||||
* Counted in SQL rather than by measuring a list, so these figures are exact
|
||||
* and cannot truncate; every other count in this file rides on a capped read.
|
||||
*
|
||||
* Archived accounts are excluded because `/api/accounts` excludes them, and
|
||||
* Piggy contradicting the list the user is looking at is the failure that costs
|
||||
* the tool its credibility. They are counted rather than silently dropped, so a
|
||||
* figure that differs from a raw table count can still be reconciled. Contacts
|
||||
* are deliberately NOT filtered the same way: `/api/contacts` applies no archive
|
||||
* filter, so every contact row is the denominator that matches the screen.
|
||||
*/
|
||||
async function readParties(db: Database): Promise<BookParties> {
|
||||
const [sides, archived, contactRows] = await Promise.all([
|
||||
db
|
||||
.select({ side: accounts.side, value: count() })
|
||||
.from(accounts)
|
||||
.where(isNull(accounts.archivedAt))
|
||||
.groupBy(accounts.side),
|
||||
db.select({ value: count() }).from(accounts).where(isNotNull(accounts.archivedAt)),
|
||||
db.select({ value: count() }).from(contacts),
|
||||
]);
|
||||
|
||||
// Every side is present at zero rather than absent: a missing key reads as
|
||||
// "not known" to a model quoting the payload, and this breakdown is only
|
||||
// trustworthy if it visibly adds up.
|
||||
const bySide: Record<string, number> = Object.fromEntries(
|
||||
ACCOUNT_SIDES.map((side) => [side, 0]),
|
||||
);
|
||||
for (const row of sides) bySide[row.side] = row.value;
|
||||
// Summed from the same grouped read the breakdown is printed from. A total
|
||||
// read by a second query can disagree with its own parts under a concurrent
|
||||
// write, and a breakdown that does not add up invites the reader to pick.
|
||||
const onBook = Object.values(bySide).reduce((sum, value) => sum + value, 0);
|
||||
|
||||
return { onBook, archived: rowCount(archived), bySide, contacts: rowCount(contactRows) };
|
||||
}
|
||||
|
||||
/**
|
||||
* The side split as prose, for the headline.
|
||||
*
|
||||
* `supply`, `demand` and `both` partition the book, so these three figures sum
|
||||
* to the total and no account is counted twice. The /accounts side tabs do not
|
||||
* partition it — each tab matches `side = X or side = both`, so the two tabs
|
||||
* overlap — which is why the note below travels with the numbers rather than
|
||||
* being left for the reader to work out from a screen that disagrees.
|
||||
*/
|
||||
function sideClause(bySide: Record<string, number>): string {
|
||||
return ACCOUNT_SIDES.map((side) => `${bySide[side] ?? 0} ${side}`).join(', ');
|
||||
}
|
||||
|
||||
const BY_SIDE_NOTE =
|
||||
'supply, demand and both partition the book: these three figures sum to the total and ' +
|
||||
'no account is counted twice. An account whose side is both trades on each side of the ' +
|
||||
'market and is counted once, under both. The side tabs on the /accounts page instead show ' +
|
||||
'supply plus both, and demand plus both, so those two figures overlap and do not sum.';
|
||||
|
||||
// The motion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -992,8 +1328,9 @@ async function readEngagements(db: Database, query: string | null): Promise<unkn
|
||||
* answered from the fragment it happened to receive.
|
||||
*/
|
||||
async function readWorkspaceSummary(db: Database): Promise<unknown> {
|
||||
const [book, demandRead, supplyRead] = await Promise.all([
|
||||
const [book, parties, demandRead, supplyRead, demandAll, supplyAll] = await Promise.all([
|
||||
readLiveBlocks(db),
|
||||
readParties(db),
|
||||
db
|
||||
.select({ id: demandDeals.id })
|
||||
.from(demandDeals)
|
||||
@@ -1004,8 +1341,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 = {
|
||||
@@ -1015,24 +1356,94 @@ 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 {
|
||||
/**
|
||||
* The parties lead the headline, and that ordering is the fix.
|
||||
*
|
||||
* This sentence is what a small model quotes, and the production answer was
|
||||
* assembled by taking the first countable thing in it. Every count in it now
|
||||
* states the noun it counts immediately beside the figure, and the noun the
|
||||
* six fallback routes are most often asked about — accounts — is no longer
|
||||
* missing from it.
|
||||
*/
|
||||
headline:
|
||||
`${atLeast(blocks.length, truncated.commitments)} live commitment(s) at ` +
|
||||
`${parties.onBook} ${ACCOUNTS_LABEL} (${sideClause(parties.bySide)}) and ` +
|
||||
`${parties.contacts} ${CONTACTS_LABEL}` +
|
||||
(parties.archived > 0
|
||||
? `, with a further ${parties.archived} account(s) archived and off the book`
|
||||
: '') +
|
||||
`. 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,
|
||||
/**
|
||||
* The two figures whose absence produced the /accounts defect, first in the
|
||||
* payload as well as first in the headline, each with its own scope. Both
|
||||
* are exact: they are SQL counts, so neither can be a lower bound the way
|
||||
* the capped reads below can.
|
||||
*/
|
||||
accounts: {
|
||||
scope: resultScope({
|
||||
covers: 'are on the book and not archived',
|
||||
matched: parties.onBook,
|
||||
total: parties.onBook,
|
||||
totalLabel: ACCOUNTS_LABEL,
|
||||
listed: 0,
|
||||
}),
|
||||
onBook: parties.onBook,
|
||||
/** Excluded from `onBook`, and from the /accounts list, but not hidden. */
|
||||
archived: parties.archived,
|
||||
bySide: parties.bySide,
|
||||
bySideNote: BY_SIDE_NOTE,
|
||||
/**
|
||||
* Said in the payload because the route guide cannot say it often enough:
|
||||
* this tool counts accounts, it does not read them. A question about a
|
||||
* named account is a `pig_search_records` question.
|
||||
*/
|
||||
rows: 'not available from this tool — counts only, no account rows',
|
||||
},
|
||||
contacts: {
|
||||
scope: resultScope({
|
||||
covers: 'are in the CRM',
|
||||
matched: parties.contacts,
|
||||
total: parties.contacts,
|
||||
totalLabel: CONTACTS_LABEL,
|
||||
listed: 0,
|
||||
}),
|
||||
total: parties.contacts,
|
||||
rows: 'not available from this tool — counts only, no contact rows',
|
||||
},
|
||||
book: {
|
||||
liveCommitments: blocks.length,
|
||||
revenueCents: totals.revenueCents,
|
||||
@@ -1041,14 +1452,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),
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user