Files
pig/apps/piggy/src/page-tools.ts
T
karti 15c72ade1c
CI / verify (push) Successful in 4m47s
CI / publish (push) Failing after 3s
Fix twenty findings from the Motion review
Each was raised by a reviewer and then survived an independent attempt to
refute it. The four that mattered most:

- A third of the starter library was invisible. Three templates authored
  `fields` shapes no renderer read — decisions, blockingSet, checks,
  steps and the rest — so about forty records rendered as no DOM at all,
  in the library and again on the engagement that instantiated them.
  Nothing failed: a renderer returns null for a key set it does not
  recognise, and a header-plus-body page looks like a template written
  that way. FieldsView now reads every key the seeds carry.
- "Add a framework" opened a picker that could never match, because the
  dialog was seeded with both the forced kind and the deal's stage, and
  qualification serves only the qualification stage. The stage is now
  dropped when MOTION_KIND_STAGES says the pair is incoherent.
- Piggy reported the promotion count as an exact figure capped at 8,
  against a tile showing the true count beside it. It is now counted in
  SQL, and all three motion tools carry a ResultScope whose denominator
  is shared lineages — never rows, never private drafts.
- No Motion test went through createApp, so the whole feature could be
  unmounted with a green suite. That is the AGENTS.md §5 trap that
  already cost this project read-guards.ts and learn.ts.

Also: both sides of the instantiate/edit race now lock, so a template
cannot be rewritten under an artefact that has copied it; concurrent
engagement opens queue on the deal row and get the 409 the handler
already promised rather than a 500; latestScore uses DISTINCT ON instead
of losing engagements past a 200-row cap; the migration adds the
scored_by_user_id foreign key the schema declares; and the demo clear
refunds usage_count for engagements it reaches by cascade, which
otherwise left starter templates permanently un-editable.

Verified on a fresh database: 16 migrations apply and re-apply as a
no-op, both seeds idempotent, usage_count back to zero after --clear.
564 unit tests pass. Every Motion route measures zero horizontal
overflow at 393 and 1440 in both themes, and all twelve seeded field
trees are asserted onto the screen by scripts/motion-fields-check.mjs.

One thing left open deliberately: the shipped qualification scorecard's
five bands and MOTION_BANDS' four are calibrated differently. The
framework's table is now titled as its own guidance rather than the
product's verdict, which removes the contradiction on screen. Making the
framework's calibration authoritative over the persisted band column is
a product decision nobody has made.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:08:55 -07:00

1626 lines
65 KiB
TypeScript

/**
* The page-scoped read tools Piggy gets while docked.
*
* The equivalent answers already exist in the MCP server, but every one of
* those tools is an authenticated HTTP call carrying a `pig_…` API key. Piggy
* has no way to mint one and calling the API back through the network to read
* a database it already holds a handle to would be a round trip for nothing —
* so the queries are ported here as direct Drizzle reads.
*
* The calendar is the exception, and deliberately so. Its projection spans
* thirteen kinds across nine tables and it is the answer a user is looking at
* on /calendar; a second implementation here would not merely duplicate it,
* it would disagree with it, and Piggy contradicting the page it has just been
* told it is reading is worse than Piggy having no calendar tool. So @pig/piggy
* depends on @pig/api and calls `CalendarService` in-process — the service
* layer takes a `Database`, not a request, precisely so it can be called this
* way. Lifting it into @pig/core instead would drag nine table imports into a
* package the browser bundles.
*
* The hard constraint is size, not capability. Interactive chat runs at
* `max_tokens` 1024 across at most four turns, so a tool that returns rows
* spends the whole budget on transcription and truncates mid-answer. Every
* result here is aggregated first and capped at a handful of exemplar rows:
* the model is given the conclusion and enough evidence to quote, never the
* ledger. The bounded reads that feed them are wide, but that width never
* leaves this process.
*/
import {
ACCOUNT_SIDES,
CONSUMING_ALLOCATION_STATUSES,
DEMAND_OPEN_STAGES,
DEMAND_STAGES,
MOTION_KINDS,
RESERVING_ALLOCATION_STATUSES,
SUPPLY_OPEN_STAGES,
aggregateMargin,
breakEvenPricePerGpuHourCents,
computeMargin,
formatCents,
type AllocationInput,
type CalendarEvent,
type CalendarEventKind,
type DemandStage,
type MarginResult,
type MotionKind,
type PiggyPageRoute,
} from '@pig/core';
import {
accounts,
allocations,
capacityCommitments,
contacts,
demandDeals,
engagementArtifacts,
engagements,
motionTemplates,
qualificationScores,
supplyDeals,
type Database,
} from '@pig/db';
import { CalendarService } from '@pig/api/src/services/calendar';
import {
and,
count,
countDistinct,
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';
import { defineTool, type AgentTool } from './provider';
const noInput = z.object({}).strict();
/** How many exemplar rows a result may carry. Everything else is a total. */
const EXEMPLARS = 8;
/** Bound on the internal read. Wide enough for a real book, still finite. */
const SCAN_LIMIT = 500;
/**
* A bounded read that knows whether it was bounded.
*
* Every list here is capped, and a cap the caller cannot see is how a
* book-level figure ends up asserted over an arbitrary slice: the model is
* told these results are already aggregated and quotes them verbatim. So each
* read asks for one row more than its budget — the same trick the calendar
* service uses — and every result that could have been cut carries the flag.
*/
function bounded<Row>(rows: Row[], limit = SCAN_LIMIT): { rows: Row[]; truncated: boolean } {
const truncated = rows.length > limit;
return { rows: truncated ? rows.slice(0, limit) : rows, truncated };
}
/**
* Written into the headline because that is the field the model quotes. A
* `truncated: true` sitting further down the payload is routinely ignored.
*/
const TRUNCATION_NOTE =
'One or more reads hit their row cap, so these figures cover part of a larger ' +
'book — present them as a lower bound, not as the whole.';
/** Prefixes a count the model must not read as exact. */
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';
/**
* The denominators the motion tools are drawn from.
*
* The library one says `shared` and says `lineage` because both are real
* restrictions on the figure: private drafts are outside it by design, and a
* lineage is one piece of practice however many versions it has carried.
*/
const MOTION_LIBRARY_LABEL = 'shared template lineage(s) in the motion library';
const ENGAGEMENTS_LABEL = 'engagement(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;
}
/**
* One tool per page. The dock is present everywhere, so the model sees this
* list on every message — a second tool would be a second thing to choose
* wrongly, and choosing wrongly costs one of four turns.
*/
export function createPagePigTools(db: Database, route: PiggyPageRoute): AgentTool[] {
return [pageTool(db, piggyPageGuide(route).tool)];
}
function pageTool(db: Database, name: PiggyPageToolName): AgentTool {
switch (name) {
case 'pig_get_margin_summary':
return defineTool({
name,
description:
'Read book-level margin across every live capacity commitment: revenue, cost, ' +
'gross margin, utilisation and idle hours, plus the largest blocks. Cost is charged ' +
'against the full commitment, not only the hours that sold.',
inputSchema: noInput,
execute: async () => readMarginSummary(db),
});
case 'pig_get_idle_capacity':
return defineTool({
name,
description:
'Read committed capacity that is bought and unsold, ranked by what the idle hours ' +
'cost, with the break-even price for the remainder of each block.',
inputSchema: noInput,
execute: async () => readIdleCapacity(db),
});
case 'pig_get_pipeline':
return defineTool({
name,
description:
'Read the open demand and supply pipelines: how many deals sit at each stage, what ' +
'they are worth, and the largest few on each side.',
inputSchema: noInput,
execute: async () => readPipeline(db),
});
case 'pig_get_calendar_ahead':
return defineTool({
name,
description:
'Read the same calendar projection the /calendar page renders: everything dated in ' +
'the near future — deals expected to close, contract effective, expiry and execution ' +
'dates, renewal notices, obligations due, capacity and allocation windows, hold ' +
'expiries, supply availability, export authorisation and compliance artefact ' +
'expiries, and calendar entries — plus what is already overdue.',
inputSchema: z
.object({
/**
* `.nullish()` rather than `.optional()`, and `.describe()` before
* it rather than after.
*
* `zodToJsonSchema(..., { target: 'openAi' })` emits an optional
* field as required-and-nullable, so a model that follows the
* schema it was handed sends `{"withinDays": null}` — which
* `.optional()` rejects, spending one of four turns on a tool
* result that reads as a failure. Described after the wrapper, the
* sentence is dropped from the emitted schema entirely and the
* default is never communicated.
*/
withinDays: z
.number()
.int()
.min(1)
.max(365)
.describe('Horizon in days. null uses the default of 30.')
.nullish(),
})
.strict(),
execute: async ({ withinDays }) => readCalendarAhead(db, withinDays ?? 30),
});
case 'pig_get_motion_summary':
return defineTool({
name,
description:
'Read whether the go-to-market motion is repeating: which demand stages the shared ' +
'library covers and which it does not, how many live engagements sit at each stage, ' +
'the shared library by kind, and the artifacts most recently promoted back into it. ' +
'Private drafts are not visible to this tool.',
inputSchema: noInput,
execute: async () => readMotionSummary(db),
});
case 'pig_search_motion_library':
return defineTool({
name,
description:
'Search the shared motion library — discovery guides, qualification frameworks, POC ' +
'structures, proposal blocks, pricing inputs, reference architectures, case studies, ' +
'technical narratives and deployment playbooks. Returns the newest version of each ' +
'template. Private drafts are never searched, whoever is asking.',
inputSchema: z
.object({
kind: z
.enum(MOTION_KINDS)
.describe('Restrict to one kind of template. null for every kind.')
.nullish(),
stage: z
.enum(DEMAND_STAGES)
.describe('Restrict to templates serving one demand stage. null for every stage.')
.nullish(),
query: z
.string()
.trim()
.min(2)
.max(64)
.describe(
'Word or phrase matched against the title, summary and slug. null returns the ' +
'whole shared library.',
)
.nullish(),
})
.strict(),
execute: async (filter) => readMotionLibrary(db, filter),
});
case 'pig_get_engagement':
return defineTool({
name,
description:
'Read the engagements running against demand deals: which stage the deal sits at, ' +
'how many artifacts have been produced and how many are final, and the latest ' +
'qualification score with its band.',
inputSchema: z
.object({
query: z
.string()
.trim()
.min(2)
.max(64)
.describe(
'Word or phrase matched against the deal name and the engagement summary. ' +
'null returns the most recently opened engagements.',
)
.nullish(),
})
.strict(),
execute: async ({ query }) => readEngagements(db, query ?? null),
});
case 'pig_get_workspace_summary':
return defineTool({
name,
description:
'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),
});
}
}
// ---------------------------------------------------------------------------
// The book
// ---------------------------------------------------------------------------
interface LiveBlock {
name: string;
gpuType: string;
gpuCount: number;
startsAt: Date;
endsAt: Date;
totalGpuHours: number;
soldGpuHours: number;
/** Held by a live hold: removed from availability, but not revenue. */
heldGpuHours: number;
costPerGpuHourCents: number;
/** The sold slices, kept so book totals can sum cents rather than ratios. */
sold: readonly AllocationInput[];
margin: MarginResult;
breakEvenPriceCents: number | null;
}
interface LiveBook {
blocks: LiveBlock[];
/** True when the book is wider than SCAN_LIMIT, so the totals are partial. */
truncated: boolean;
}
/**
* Live commitments with sold and held hours counted separately.
*
* A port of `CapacityService.availability`, minus the shape integration and
* matching the API does not need here. Sold and held stay distinct because a
* pipeline of optimistic holds must never be able to make the book look full.
* Expired holds are ignored rather than swept, so the figures are right even
* when the cleanup job is behind.
*
* The cap is reported rather than hidden: revenue, cost and gross margin here
* are sums over whatever came back, and past 500 live commitments that is an
* arbitrary slice of the book being stated as the book.
*/
async function readLiveBlocks(db: Database, now = new Date()): Promise<LiveBook> {
const { rows: commitments, truncated } = bounded(
await db
.select()
.from(capacityCommitments)
.where(and(isNull(capacityCommitments.terminatedAt), gte(capacityCommitments.endsAt, now)))
.limit(SCAN_LIMIT + 1),
);
if (commitments.length === 0) return { blocks: [], truncated };
const reservations = await db
.select()
.from(allocations)
.where(
and(
inArray(
allocations.capacityCommitmentId,
commitments.map((commitment) => commitment.id),
),
inArray(allocations.status, [...RESERVING_ALLOCATION_STATUSES]),
),
);
const blocks = commitments.map((commitment) => {
const mine = reservations.filter((row) => row.capacityCommitmentId === commitment.id);
let soldGpuHours = 0;
let heldGpuHours = 0;
for (const row of mine) {
// numeric columns arrive as strings; adding them unconverted concatenates.
const hours = Number(row.gpuHours);
if ((CONSUMING_ALLOCATION_STATUSES as readonly string[]).includes(row.status)) {
soldGpuHours += hours;
} else if (!row.holdExpiresAt || row.holdExpiresAt > now) {
heldGpuHours += hours;
}
}
const book = {
gpuHours: Number(commitment.totalGpuHours),
costPerGpuHourCents: commitment.costPerGpuHourCents,
};
const sold = mine
.filter((row) => (CONSUMING_ALLOCATION_STATUSES as readonly string[]).includes(row.status))
.map((row) => ({
gpuHours: Number(row.gpuHours),
pricePerGpuHourCents: row.pricePerGpuHourCents,
}));
return {
name: commitment.name,
gpuType: commitment.gpuType,
gpuCount: commitment.gpuCount,
startsAt: commitment.startsAt,
endsAt: commitment.endsAt,
totalGpuHours: book.gpuHours,
soldGpuHours,
heldGpuHours,
costPerGpuHourCents: commitment.costPerGpuHourCents,
sold,
margin: computeMargin(book, sold),
breakEvenPriceCents: breakEvenPricePerGpuHourCents(book, sold),
};
});
return { blocks, truncated };
}
function bookTotals(blocks: readonly LiveBlock[]): MarginResult {
// Sum cents, never average per-block percentages: an average of ratios
// weights a tiny block equally with a huge one.
return aggregateMargin(
blocks.map((block) => ({
commitment: {
gpuHours: block.totalGpuHours,
costPerGpuHourCents: block.costPerGpuHourCents,
},
allocations: block.sold,
})),
);
}
async function readMarginSummary(db: Database): Promise<unknown> {
const { blocks, truncated } = await readLiveBlocks(db);
const totals = bookTotals(blocks);
const largest = [...blocks]
.sort((a, b) => b.margin.costCents - a.margin.costCents)
.slice(0, EXEMPLARS);
return {
headline:
`Revenue ${formatCents(totals.revenueCents)} against cost ${formatCents(totals.costCents)}; ` +
`gross margin ${formatCents(totals.grossMarginCents)} (${percent(totals.grossMarginPct)}) ` +
`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,
costCents: totals.costCents,
grossMarginCents: totals.grossMarginCents,
grossMarginPct: totals.grossMarginPct,
utilisation: totals.utilisation,
idleGpuHours: Math.round(totals.idleGpuHours),
marginPerAllocatedGpuHourCents: totals.marginPerAllocatedGpuHourCents,
},
liveCommitments: blocks.length,
largestBlocks: largest.map((block) => ({
name: block.name,
gpuType: block.gpuType,
utilisation: block.margin.utilisation,
soldGpuHours: Math.round(block.soldGpuHours),
totalGpuHours: Math.round(block.totalGpuHours),
costPerGpuHourCents: block.costPerGpuHourCents,
grossMarginCents: block.margin.grossMarginCents,
})),
};
}
/**
* 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() + 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 >= IDLE_THRESHOLD_PCT,
)
.map((block) => ({
block,
idleGpuHours: block.margin.idleGpuHours,
// The number that makes the case: what the unsold hours already cost us.
idleCostCents: Math.round(block.margin.idleGpuHours * block.costPerGpuHourCents),
}))
.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
? `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,
/** 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: listed.map((row) => ({
name: row.block.name,
gpuType: row.block.gpuType,
gpuCount: row.block.gpuCount,
utilisation: row.block.margin.utilisation,
idleGpuHours: Math.round(row.idleGpuHours),
idleCostCents: row.idleCostCents,
// What the rest of the block must fetch to come out even.
breakEvenPricePerGpuHourCents: row.block.breakEvenPriceCents,
endsAt: row.block.endsAt.toISOString(),
})),
};
}
// ---------------------------------------------------------------------------
// The two pipelines
// ---------------------------------------------------------------------------
async function readPipeline(db: Database): Promise<unknown> {
// 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)
.where(inArray(demandDeals.stage, [...DEMAND_OPEN_STAGES]))
.limit(SCAN_LIMIT + 1),
db
.select()
.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
// understate the pipeline rather than admit the gap.
const valueOf = (deal: (typeof demand)[number]) => deal.tcvCents ?? deal.acvCents ?? 0;
const demandValueCents = demand.reduce((sum, deal) => sum + valueOf(deal), 0);
return {
headline:
`${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]
.sort((a, b) => valueOf(b) - valueOf(a))
.slice(0, EXEMPLARS)
.map((deal) => ({
name: deal.name,
stage: deal.stage,
valueCents: valueOf(deal),
expectedCloseDate: deal.expectedCloseDate?.toISOString() ?? null,
})),
},
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))
.slice(0, EXEMPLARS)
.map((deal) => ({
name: deal.name,
stage: deal.stage,
gpuType: deal.gpuType,
gpuCount: deal.gpuCount,
targetCostPerGpuHourCents: deal.targetCostPerGpuHourCents,
})),
},
};
}
function countByStage(stages: readonly string[]): Record<string, number> {
const counts: Record<string, number> = {};
for (const stage of stages) counts[stage] = (counts[stage] ?? 0) + 1;
return counts;
}
// ---------------------------------------------------------------------------
// Dates
// ---------------------------------------------------------------------------
/**
* How far back a lapsed item still counts as this week's problem.
*
* Unbounded, the overdue arm surfaced whatever was oldest — a stale obligation
* from two years ago crowding out a renewal notice that lapsed on Friday. Past
* a quarter it is a data-hygiene job, not an operational one, so the window
* stops there and the exemplars run most-recent-first within it.
*/
const OVERDUE_LOOKBACK_DAYS = 90;
/**
* The kinds that can honestly be late.
*
* Lateness needs a completion column: an obligation, a renewal notice and a
* deal's expected close all have somewhere to record that the thing happened.
* A capacity window that has ended is finished, not overdue, and an expiry
* that has passed is a state of the world rather than an errand — listing
* either as overdue work invents a backlog.
*/
const OVERDUE_KINDS = [
'obligation_due',
'renewal_notice',
'expected_close',
] as const satisfies readonly CalendarEventKind[];
/**
* What is dated in the near future — the same projection /calendar renders.
*
* This used to reimplement the projection over two tables. The page shows
* thirteen kinds, so Piggy asserted a total that was missing contract
* expiries, renewal notices, hold expiries, capacity and allocation windows,
* authorisation and artefact expiries, and every human-owned calendar entry.
* Being confidently wrong about the screen in front of the reader is the one
* failure that costs the tool its credibility, so it calls the service.
*
* Two projections, not one: overdue work sits BEFORE `now` and the horizon
* starts at it, and a single wide window would let a quarter of stale rows
* consume the per-source budget that the coming month needs.
*
* Counts are taken over the full projected set and only then sliced for
* exemplars — the previous version interpolated the capped list lengths, so a
* book with two hundred overdue obligations reported eight, and the system
* prompt tells the model to quote these figures rather than recompute them.
*/
async function readCalendarAhead(db: Database, withinDays: number): Promise<unknown> {
const now = new Date();
const horizon = new Date(now.getTime() + withinDays * 86_400_000);
const lookback = new Date(now.getTime() - OVERDUE_LOOKBACK_DAYS * 86_400_000);
const calendar = new CalendarService(db, () => now);
const [ahead, behind] = await Promise.all([
calendar.project({ from: now, to: horizon }),
calendar.project({ from: lookback, to: now, kinds: OVERDUE_KINDS }),
]);
// A done event is a dated fact, not something anyone must act on; the page
// shows it greyed out and a count that includes it reads as a workload.
const upcoming = ahead.events.filter((event) => event.state !== 'done');
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) 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)} ` +
`weighted, ${ahead.totals.renewalCount} renewal notice(s) and ` +
`${ahead.totals.expiringAuthorizationCount} export authorisation(s) expiring; ` +
`${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,
/**
* Counted in SQL by the service, so these five stay exact even when a
* source truncates. Everything else on this payload is counted off the
* event list and moves with `truncated`.
*/
exactTotals: {
obligationsDue: ahead.totals.obligationCount,
dealsExpectedToClose: ahead.totals.closingCount,
weightedPipelineCents: ahead.totals.weightedPipelineCents,
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),
// Soonest first: the near edge of the horizon is what gets acted on.
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,
byKind: countByKind(overdue),
events: [...overdue]
.sort((a, b) => b.startsAt.localeCompare(a.startsAt))
.slice(0, EXEMPLARS)
.map(exemplar),
},
};
}
/**
* One event, small enough to quote. `meta`, `id` and the ids are dropped: the
* model cannot navigate and a uuid in a 1024-token answer is pure cost.
*/
function exemplar(event: CalendarEvent): Record<string, unknown> {
return {
kind: event.kind,
title: event.title,
startsAt: event.startsAt,
endsAt: event.endsAt,
state: event.state,
accountName: event.accountName,
amountCents: event.amountCents,
};
}
function countByKind(events: readonly CalendarEvent[]): Record<string, number> {
const counts: Record<string, number> = {};
for (const event of events) counts[event.kind] = (counts[event.kind] ?? 0) + 1;
return counts;
}
function countByState(events: readonly CalendarEvent[]): Record<string, number> {
const counts: Record<string, number> = {};
for (const event of events) counts[event.state] = (counts[event.state] ?? 0) + 1;
return counts;
}
// ---------------------------------------------------------------------------
// 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
// ---------------------------------------------------------------------------
export interface MotionLibraryFilter {
kind?: MotionKind | null;
stage?: DemandStage | null;
query?: string | null;
}
/**
* Shared templates only, unconditionally — not "unless the asker owns it".
*
* Motion is the one place in PIG with a row-level access rule: a `private`
* template is readable by its owner and by a platform admin, and by nobody
* else. Piggy has no reliable notion of who is asking. The dock publishes a
* route, the relay checks a capability, and the tool then runs as the process;
* nothing reaches this query that identifies a person strongly enough to widen
* it on. So it is not widened. A model that can be argued into reading a
* colleague's private draft is a leak with the extra step of asking politely,
* and the argument would arrive as ordinary conversation the guard never sees.
*
* This is the only clause here that must not become a parameter. If private
* drafts ever need an answer, the identity has to arrive with the request and
* be enforced in `services/motion.ts` where the API already enforces it — not
* by relaxing this. `motion-tools.test.ts` fails if it is.
*/
export function motionLibraryWhere(filter: MotionLibraryFilter): SQL {
const conditions: SQL[] = [
eq(motionTemplates.visibility, 'shared'),
isNull(motionTemplates.archivedAt),
];
if (filter.kind) conditions.push(eq(motionTemplates.kind, filter.kind));
if (filter.stage) conditions.push(eq(motionTemplates.stage, filter.stage));
if (filter.query) {
const fragment = likeFragment(filter.query);
conditions.push(
or(
ilike(motionTemplates.title, fragment),
ilike(motionTemplates.summary, fragment),
ilike(motionTemplates.slug, fragment),
)!,
);
}
return and(...conditions)!;
}
interface LineageRow {
slug: string;
kind: MotionKind;
stage: DemandStage;
version: number;
title: string;
summary: string;
usageCount: number;
}
/**
* One row per lineage, newest version winning.
*
* The library counts lineages rather than rows because a template promoted
* three times is one piece of practice with a history, and counting its
* versions reports a library four times the size of the one anybody can choose
* from. The slug is the identity of the lineage — see the schema header.
*/
function newestPerSlug<Row extends { slug: string; version: number }>(rows: readonly Row[]): Row[] {
const newest = new Map<string, Row>();
for (const row of rows) {
const held = newest.get(row.slug);
if (!held || row.version > held.version) newest.set(row.slug, row);
}
return [...newest.values()];
}
function countBy<Row>(rows: readonly Row[], key: (row: Row) => string): Record<string, number> {
const counts: Record<string, number> = {};
for (const row of rows) counts[key(row)] = (counts[key(row)] ?? 0) + 1;
return counts;
}
/**
* The question the /motion page exists to answer: is the motion repeating?
*
* Coverage is reported as the stages the shared library does NOT reach, not
* only as a count, because "6 of 8 stages covered" is a figure nobody acts on
* and "nothing covers procurement or deployment" is a piece of work. The two
* closed stages are excluded throughout — a won deal has left the motion.
*/
async function readMotionSummary(db: Database): Promise<unknown> {
// One predicate for the exemplars and for the count below them, so the list
// and the figure cannot come to describe different sets. It is also the
// predicate the /motion tile counts: `motionLibraryWhere({})` already excludes
// archived rows, and promotion always writes `visibility: 'shared'`.
const promoted = and(motionLibraryWhere({}), isNotNull(motionTemplates.originArtifactId))!;
const [libraryRead, engagementRead, promotions, promotionTotal] = await Promise.all([
db
.select({
slug: motionTemplates.slug,
kind: motionTemplates.kind,
stage: motionTemplates.stage,
version: motionTemplates.version,
title: motionTemplates.title,
summary: motionTemplates.summary,
usageCount: motionTemplates.usageCount,
})
.from(motionTemplates)
.where(motionLibraryWhere({}))
.limit(SCAN_LIMIT + 1),
db
.select({ stage: demandDeals.stage, dealName: demandDeals.name })
.from(engagements)
.innerJoin(demandDeals, eq(engagements.demandDealId, demandDeals.id))
.where(eq(engagements.status, 'open'))
.limit(SCAN_LIMIT + 1),
db
.select({
title: motionTemplates.title,
kind: motionTemplates.kind,
version: motionTemplates.version,
createdAt: motionTemplates.createdAt,
})
.from(motionTemplates)
.where(promoted)
.orderBy(desc(motionTemplates.createdAt))
.limit(EXEMPLARS),
// Counted in SQL rather than read off the list above it, which is capped at
// EXEMPLARS. Promotions is the one figure whose job is to show the loop
// closing, so it is the one figure that must not stop moving: read off the
// list it would say 8 the moment the loop started working, for ever, beside
// a tile counting 12 exactly. Versions rather than lineages, like the tile —
// a second promotion into one lineage is a second time the loop closed.
db.select({ value: count() }).from(motionTemplates).where(promoted),
]);
const { rows: templateRows, truncated: libraryTruncated } = bounded(libraryRead);
const { rows: openEngagements, truncated: engagementsTruncated } = bounded(engagementRead);
const truncated = libraryTruncated || engagementsTruncated;
const promotionCount = rowCount(promotionTotal);
const lineages = newestPerSlug(templateRows);
const engagementsByStage = countBy(openEngagements, (row) => row.stage);
const uncovered = DEMAND_OPEN_STAGES.filter(
(stage) => !lineages.some((template) => template.stage === stage),
);
return {
headline:
`${atLeast(lineages.length, libraryTruncated)} shared template(s) across ` +
`${Object.keys(countBy(lineages, (row) => row.kind)).length} of ${MOTION_KINDS.length} ` +
`kind(s), and ${atLeast(openEngagements.length, engagementsTruncated)} open engagement(s). ` +
(libraryTruncated
? 'Which stages the library misses cannot be told from this scan, because it hit its ' +
'row cap. '
: uncovered.length === 0
? 'Every live demand stage has at least one shared template. '
: `No shared template covers ${uncovered.join(', ')}. `) +
`${promotionCount} artifact(s) promoted back into the library in all` +
(promotions.length ? `, newest ${promotions.length} listed.` : '.') +
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
/** Unfiltered within the shared library, so this read IS its own denominator. */
scope: resultScope({
covers: 'are shared and not archived',
matched: lineages.length,
total: lineages.length,
totalLabel: MOTION_LIBRARY_LABEL,
listed: 0,
truncated: libraryTruncated,
}),
truncated,
sharedTemplates: lineages.length,
openEngagements: openEngagements.length,
/**
* Null rather than a list when the scan was capped. "No shared template
* covers procurement" is a definite negative, and a definite negative drawn
* from part of the library is a claim the read cannot support — the stage
* may well be covered by a lineage beyond the cap. An exact answer under
* truncation needs a per-slug newest-version aggregate in SQL, which is a
* larger change than this figure is worth.
*/
uncoveredStages: libraryTruncated ? null : uncovered,
stages: DEMAND_OPEN_STAGES.map((stage) => ({
stage,
openEngagements: engagementsByStage[stage] ?? 0,
sharedTemplates: lineages.filter((template) => template.stage === stage).length,
})),
libraryByKind: countBy(lineages, (row) => row.kind),
promotions: promotionCount,
/**
* The exact figure sits beside the list on purpose. A model handed eight
* rows and no total reads the length of the list as the count, which is the
* mistake `ResultScope` exists for.
*/
recentPromotionsScope: resultScope({
covers: 'were promoted back into the library from an engagement artefact',
matched: promotionCount,
total: promotionCount,
totalLabel: 'promoted template version(s) in the motion library',
listed: promotions.length,
}),
// The loop made visible: what the last few engagements gave back.
recentPromotions: promotions.map((row) => ({
title: row.title,
kind: row.kind,
version: row.version,
promotedAt: row.createdAt.toISOString(),
})),
};
}
async function readMotionLibrary(db: Database, filter: MotionLibraryFilter): Promise<unknown> {
const [libraryRead, libraryTotal] = await Promise.all([
db
.select({
slug: motionTemplates.slug,
kind: motionTemplates.kind,
stage: motionTemplates.stage,
version: motionTemplates.version,
title: motionTemplates.title,
summary: motionTemplates.summary,
usageCount: motionTemplates.usageCount,
})
.from(motionTemplates)
.where(motionLibraryWhere(filter))
.orderBy(desc(motionTemplates.updatedAt))
.limit(SCAN_LIMIT + 1),
/**
* The denominator, and both halves of it are decisions.
*
* `motionLibraryWhere({})` rather than a bare `count()` over the table: a
* total that included private rows would publish the existence and the size
* of colleagues' drafts through the back door, which is the one thing the
* library rule exists to withhold — a denominator leaks as readily as a
* list. And `countDistinct(slug)` rather than `count()`, because the slug is
* the identity of a lineage: counting versions reports a library several
* times the size of the one anybody can choose from, which is the same
* mistake `newestPerSlug` exists to avoid on the matched side. Counted in
* SQL, so the total stays exact when the read beside it hits its cap.
*/
db
.select({ value: countDistinct(motionTemplates.slug) })
.from(motionTemplates)
.where(motionLibraryWhere({})),
]);
const { rows, truncated } = bounded(libraryRead);
const lineages = newestPerSlug(rows as LineageRow[]);
const sharedTemplates = rowCount(libraryTotal);
const described = [
filter.kind ? `kind ${filter.kind}` : null,
filter.stage ? `stage ${filter.stage}` : null,
filter.query ? `"${filter.query}"` : null,
].filter((part): part is string => part !== null);
const matching = described.length ? ` matching ${described.join(', ')}` : '';
return {
headline:
(lineages.length === 0
? `None of the ${sharedTemplates} ${MOTION_LIBRARY_LABEL}${matching}. Private drafts are ` +
'not searched, so a template may exist and not be visible here.'
: `${atLeast(lineages.length, truncated)} of ${sharedTemplates} ` +
`${MOTION_LIBRARY_LABEL}${matching}, newest version of each.`) +
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
scope: resultScope({
covers: described.length ? `match ${described.join(', ')}` : 'are shared and not archived',
matched: lineages.length,
total: sharedTemplates,
totalLabel: MOTION_LIBRARY_LABEL,
listed: Math.min(lineages.length, EXEMPLARS),
filters: {
...(filter.kind ? { kind: filter.kind } : {}),
...(filter.stage ? { stage: filter.stage } : {}),
...(filter.query ? { query: filter.query } : {}),
},
truncated,
}),
truncated,
count: lineages.length,
/** The denominator as a bare field: this is how big the shared library is. */
sharedTemplates,
byKind: countBy(lineages, (row) => row.kind),
// Most recently updated first: the practice people are actually amending.
templates: lineages.slice(0, EXEMPLARS).map((template) => ({
slug: template.slug,
kind: template.kind,
stage: template.stage,
version: template.version,
title: template.title,
summary: template.summary,
// How many engagements have instantiated it — the only evidence here of
// whether a template is practice or an unread document.
usageCount: template.usageCount,
})),
};
}
/**
* Engagements, with the two figures anyone asks for: how much has been produced
* and where qualification landed.
*
* Artifacts and scores are read only for the exemplars, so the second and third
* queries stay small however wide the book is. The engagement's playbook
* template is deliberately not joined: it may be a private draft, and naming it
* would walk round the library rule by another door.
*/
async function readEngagements(db: Database, query: string | null): Promise<unknown> {
const fragment = query ? likeFragment(query) : null;
const [engagementRead, engagementTotal] = await Promise.all([
db
.select({
id: engagements.id,
status: engagements.status,
summary: engagements.summary,
openedAt: engagements.openedAt,
stage: demandDeals.stage,
dealName: demandDeals.name,
accountName: accounts.name,
})
.from(engagements)
.innerJoin(demandDeals, eq(engagements.demandDealId, demandDeals.id))
.leftJoin(accounts, eq(demandDeals.accountId, accounts.id))
.where(
fragment
? or(ilike(demandDeals.name, fragment), ilike(engagements.summary, fragment))
: undefined,
)
.orderBy(desc(engagements.openedAt))
.limit(SCAN_LIMIT + 1),
// Every engagement, open or closed and whatever the query. Without it a
// search that matches two is the only figure in the payload, and "we have
// two engagements" is the answer that comes back.
db.select({ value: count() }).from(engagements),
]);
const { rows, truncated } = bounded(engagementRead);
const totalEngagements = rowCount(engagementTotal);
const exemplars = rows.slice(0, EXEMPLARS);
const ids = exemplars.map((row) => row.id);
const [artifacts, scores] = ids.length
? await Promise.all([
db
.select({ engagementId: engagementArtifacts.engagementId, status: engagementArtifacts.status })
.from(engagementArtifacts)
.where(
and(
inArray(engagementArtifacts.engagementId, ids),
isNull(engagementArtifacts.archivedAt),
),
)
.limit(SCAN_LIMIT),
db
.select({
engagementId: qualificationScores.engagementId,
basisPoints: qualificationScores.basisPoints,
band: qualificationScores.band,
scoredAt: qualificationScores.scoredAt,
})
.from(qualificationScores)
.where(inArray(qualificationScores.engagementId, ids))
.orderBy(desc(qualificationScores.scoredAt))
.limit(SCAN_LIMIT),
])
: [[], []];
// Newest first out of the query, so the first score seen for an engagement is
// its latest; a later one must not overwrite it.
const latestScore = new Map<string, (typeof scores)[number]>();
for (const score of scores) if (!latestScore.has(score.engagementId)) latestScore.set(score.engagementId, score);
return {
headline:
(rows.length === 0
? query
? `None of the ${totalEngagements} ${ENGAGEMENTS_LABEL} match "${query}".`
: 'No demand deal has an engagement running against it yet.'
: `${atLeast(rows.length, truncated)} of ${totalEngagements} ${ENGAGEMENTS_LABEL}` +
`${query ? ` match "${query}"` : ''}, of which ` +
`${rows.filter((row) => row.status === 'open').length} open.`) +
(truncated ? ` ${TRUNCATION_NOTE}` : ''),
scope: resultScope({
covers: query ? `match "${query}"` : 'are on the book',
matched: rows.length,
total: totalEngagements,
totalLabel: ENGAGEMENTS_LABEL,
listed: exemplars.length,
filters: query ? { query } : {},
truncated,
}),
truncated,
count: rows.length,
/** The denominator as a bare field: engagements exist that this did not match. */
totalEngagements,
byStatus: countBy(rows, (row) => row.status),
byStage: countBy(rows, (row) => row.stage),
engagements: exemplars.map((row) => {
const mine = artifacts.filter((artifact) => artifact.engagementId === row.id);
const score = latestScore.get(row.id);
return {
dealName: row.dealName,
accountName: row.accountName,
stage: row.stage,
status: row.status,
summary: row.summary,
openedAt: row.openedAt.toISOString(),
artifacts: mine.length,
finalArtifacts: mine.filter((artifact) => artifact.status === 'final').length,
latestScore: score
? {
// Basis points of the maximum, so a tenth of a per cent — the
// band is the part a seller acts on.
basisPoints: score.basisPoints,
percent: `${(score.basisPoints / 100).toFixed(1)}%`,
band: score.band,
scoredAt: score.scoredAt.toISOString(),
}
: null,
};
}),
};
}
// ---------------------------------------------------------------------------
// The fallback
// ---------------------------------------------------------------------------
/**
* The default when no page names a better tool.
*
* This replaced a dump of up to 600 rows. That version could not survive one
* turn of a 1024-token budget, so the model saw a truncated ledger and
* answered from the fragment it happened to receive.
*/
async function readWorkspaceSummary(db: Database): Promise<unknown> {
const [book, parties, demandRead, supplyRead, demandAll, supplyAll] = await Promise.all([
readLiveBlocks(db),
readParties(db),
db
.select({ id: demandDeals.id })
.from(demandDeals)
.where(inArray(demandDeals.stage, [...DEMAND_OPEN_STAGES]))
.limit(SCAN_LIMIT + 1),
db
.select({ id: supplyDeals.id })
.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 = {
commitments: book.truncated,
demandDeals: demandTruncated,
supplyDeals: supplyTruncated,
};
const anyTruncated = Object.values(truncated).some(Boolean);
const totals = bookTotals(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,
);
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:
`${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)}; ` +
`${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,
costCents: totals.costCents,
grossMarginCents: totals.grossMarginCents,
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,
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),
})),
},
};
}
/**
* One decimal, matching the web's own `percent` for these two quantities.
*
* Both call sites report a blended figure the reader has on screen beside
* them — Overview and Margin render utilisation and gross margin to a tenth —
* and rounding to a whole number here had Piggy answer "5% margin at 87%
* utilisation" about a book the page was calling 5.3% and 87.3%. On a book
* clearing five per cent, a tenth is a twentieth of the whole margin, so this
* is a different number rather than a shorter one.
*/
function percent(value: number | null): string {
return value == null ? 'n/a' : `${(value * 100).toFixed(1)}%`;
}