Rebuild the shell, add Calendar and Learn, and govern reads
CI / verify (push) Successful in 3m45s
CI / publish (push) Has been skipped

Seven parallel agents and an adversarial verification pass. The three things
worth knowing before reading the diff:

RBAC WAS ALREADY BUILT. docs/build-plan.md marks F2 and F3 outstanding and is
stale — packages/core/src/permissions.ts and lib/mutation.ts shipped long ago.
So this does not rebuild them; it closes the gaps an audit found. The big one
is that reads were entirely ungoverned: every GET was "any authenticated
member", so a junior demand rep and a research contractor could both pull
per-block supplier cost and break-even prices from /api/capacity/margin, and
every contract's negotiated terms. For a company whose margin is the business,
that was the hole that mattered. Adds book:read / economics:read / team:read,
a readGuard middleware, and a `viewer` role below member.

THE BUTTON AND THE 403 DISAGREED — the exact thing F3 said must never happen.
Contracts.tsx never called can() at all, so its save button was always enabled
against a server requiring contract:sign; Capacity.tsx gated commitment
creation on deal:write/demand while the server wanted commitment:write/supply.

POST /api/activities was the one write bypassing executeMutation: no capability
check, and any member could mutate accounts.lastActivityAt as a side effect.
It is now a proper mutation() behind activity:write.

The shell becomes three panes — a collapsible shadcn sidebar with an account
switcher on the Piggy accent, a header with real search, and Piggy docked to
the right, page-aware and persistent across navigation. The phone keeps its
bottom tab bar, which is the thing this product already beat trycompai/crm on,
and gains the sidebar as a sheet.

Calendar is a projection over thirteen dated sources rather than a new table,
because a table would duplicate dates that already live on contracts, deals and
commitments and would drift — and one ledger answering the question is the
whole argument. It surfaces export_authorizations and compliance_artifacts,
which had indexed expires_at columns, schema comments saying they must be
alerted on, and no read endpoint or UI anywhere.

Learn carries two tracks. Concepts are members-only; the platform track can be
opened with a share code by someone with no account. The code mints a scoped
learn-only token and never a Principal — every route here resolves a principal
and then checks capabilities, so a principal-minting code would be one missing
check away from leaking the book. "Only platform-track rows may be code-visible"
is a database CHECK constraint as well as a write-path rule, and a test asserts
a valid learn token still gets 401 on /api/dashboard, /api/accounts and
/api/contracts — the same invariant scripts/deploy.sh refuses to ship without.

CD becomes tag-to-ship. CI publishes an image to the Gitea registry on a
release-* tag and cloud-2 pulls it, so no credential on the shared runner can
execute anything on production — by construction rather than by policy. Both
halves of deploy.sh's original rule survive: nothing on the runner reaches the
host, and a human still decides when it ships. deploy.sh gains a rollback and a
public-origin check, and PIG_IMAGE now reaches compose through `sudo env`,
without which sudo's env_reset silently resolved every release to pig:local.

Tests 141 -> 261.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 15:02:48 -07:00
parent 6cf80747cc
commit 13dec6b4b8
102 changed files with 28638 additions and 913 deletions
+974
View File
@@ -0,0 +1,974 @@
/**
* The quarterly calendar — a projection, not a table.
*
* Everything with a date on it already lives somewhere: contracts expire,
* obligations fall due, commitments open and close, holds lapse, export
* authorisations run out. This service reads those columns where they are and
* emits one common shape. Nothing here is stored, and nothing here can drift
* from the record it describes.
*
* Three things shape the implementation.
*
* **One query per source, each with its own date predicate and its own
* limit.** The convention elsewhere in this API is a flat `.limit(300)`
* ordered by `updated_at`, with the caller filtering by date in the browser —
* which means the deals actually closing this quarter are not guaranteed to be
* in the response at all. That is precisely the bug this endpoint exists to
* fix, so every predicate is server-side and every source is bounded
* independently rather than competing for one budget.
*
* **Totals are separate aggregate queries.** If the header counted the rows in
* the list it would under-report the moment any source truncated, and a
* quarterly figure that silently shrinks is worse than no figure. The counts
* are exact even when the list is cut short.
*
* **Renewal comes from `renewalAlarm()`.** The rule — expiry minus notice
* days, only when auto-renewal is on — is defined once, in the contracts
* service. The SQL below narrows candidates with the same arithmetic so the
* scan stays bounded, but every date and every state on an emitted event comes
* from calling that function. If the rule changes, it changes there.
*/
import {
and,
asc,
count,
eq,
gt,
gte,
isNotNull,
isNull,
lt,
or,
sql,
} from 'drizzle-orm';
import {
calendarEventId,
completableSpanState,
eventState,
quarterOf,
spanState,
type CalendarEvent,
type CalendarEventKind,
type Quarter,
} from '@pig/core';
import {
accounts,
allocations,
calendarEntries,
capacityCommitments,
complianceArtifacts,
contractObligations,
contracts,
demandDeals,
exportAuthorizations,
supplyDeals,
users,
type Database,
} from '@pig/db';
import { renewalAlarm } from './contracts';
/** Per-source ceiling. Generous enough that a real quarter never reaches it. */
const DEFAULT_SOURCE_LIMIT = 500;
export interface CalendarQuery {
from: Date;
/** Exclusive. Quarters are half-open so consecutive ones do not double-count. */
to: Date;
kinds?: readonly CalendarEventKind[];
accountId?: string;
ownerUserId?: string;
fiscalYearStartMonth?: number;
timeZone?: string;
sourceLimit?: number;
}
export interface CalendarTotals {
/**
* Σ acv × probability for deals whose expected close date falls in range.
* The number a GTM lead reads first, and nothing in PIG computed it before.
*/
weightedPipelineCents: number;
closingCount: number;
renewalCount: number;
obligationCount: number;
expiringAuthorizationCount: number;
}
export interface CalendarProjection {
from: string;
to: string;
quarter: Quarter;
events: CalendarEvent[];
/** True when any single source hit its limit; the totals are still exact. */
truncated: boolean;
totals: CalendarTotals;
}
/**
* Where the front end should go when an event is clicked.
*
* There is no record-detail route convention in this app yet — every page is
* flat — so the page is the load-bearing half and the query parameter is a
* hint the detail sheet can honour once one exists.
*/
function href(page: string, param: string, id: string): string {
return `/${page}?${param}=${id}`;
}
/** Drizzle returns numeric columns as strings; `probability` is one of them. */
function numeric(value: string | null): number | null {
if (value === null) return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
export class CalendarService {
constructor(
private readonly db: Database,
private readonly clock: () => Date = () => new Date(),
) {}
/**
* The reader's own quarter boundary.
*
* `users.timezone` is settable through PATCH /api/me/preferences and until
* now was read by nothing at all. A quarter is a local-midnight question, so
* this is the first place it genuinely matters — and UTC remains the honest
* fallback for a user who has never set one.
*/
async timeZoneFor(userId: string): Promise<string> {
const [row] = await this.db
.select({ timezone: users.timezone })
.from(users)
.where(eq(users.id, userId))
.limit(1);
return row?.timezone ?? 'UTC';
}
async project(query: CalendarQuery): Promise<CalendarProjection> {
const now = this.clock();
const timeZone = query.timeZone ?? 'UTC';
const fiscalYearStartMonth = query.fiscalYearStartMonth ?? 0;
const limit = query.sourceLimit ?? DEFAULT_SOURCE_LIMIT;
const wanted = query.kinds?.length ? new Set(query.kinds) : null;
const wants = (kind: CalendarEventKind): boolean => !wanted || wanted.has(kind);
const collected: { events: CalendarEvent[]; truncated: boolean }[] = await Promise.all([
wants('expected_close') ? this.expectedClose(query, now, limit) : empty(),
wants('contract_effective')
? this.contractDate(query, now, limit, 'contract_effective')
: empty(),
wants('contract_expiry')
? this.contractDate(query, now, limit, 'contract_expiry')
: empty(),
wants('contract_executed')
? this.contractDate(query, now, limit, 'contract_executed')
: empty(),
wants('renewal_notice') ? this.renewalNotices(query, now, limit) : empty(),
wants('obligation_due') ? this.obligations(query, now, limit) : empty(),
wants('capacity_window') ? this.capacityWindows(query, now, limit) : empty(),
wants('allocation_window') ? this.allocationWindows(query, now, limit) : empty(),
wants('hold_expiry') ? this.holdExpiries(query, now, limit) : empty(),
wants('supply_available_from') ? this.supplyAvailability(query, now, limit) : empty(),
wants('authorization_expiry') ? this.authorizationExpiries(query, now, limit) : empty(),
wants('artifact_expiry') ? this.artifactExpiries(query, now, limit) : empty(),
wants('calendar_entry') ? this.entries(query, now, limit) : empty(),
]);
const events = collected
.flatMap((source) => source.events)
.sort((a, b) => a.startsAt.localeCompare(b.startsAt) || a.id.localeCompare(b.id));
return {
from: query.from.toISOString(),
to: query.to.toISOString(),
quarter: quarterOf(query.from, fiscalYearStartMonth, timeZone),
events,
truncated: collected.some((source) => source.truncated),
totals: await this.totals(query),
};
}
// ------------------------------------------------------------------ totals
/**
* Counted in SQL rather than off the event list, so a truncated source
* cannot quietly shrink a quarterly figure. The kind filter is deliberately
* ignored here: narrowing the list to one kind should not blank the header
* the reader is narrowing against.
*/
private async totals(query: CalendarQuery): Promise<CalendarTotals> {
const { from, to, accountId, ownerUserId } = query;
const [pipeline, renewals, obligations, authorizations] = await Promise.all([
this.db
.select({
/**
* A closed-won deal forecasts at certainty and a closed-lost one at
* nothing, whatever `probability` still says; an open deal with no
* forecast contributes nothing rather than its full value, because
* an unfilled field is not a prediction of 100%.
*/
weightedCents: sql<string>`coalesce(sum(round(${demandDeals.acvCents} * (case
when ${demandDeals.stage} = 'closed_won' then 1
when ${demandDeals.stage} = 'closed_lost' then 0
else coalesce(${demandDeals.probability}, 0) end))), 0)`,
closing: sql<number>`count(*) filter (where ${demandDeals.stage} <> 'closed_lost')::int`,
})
.from(demandDeals)
.where(
and(
gte(demandDeals.expectedCloseDate, from),
lt(demandDeals.expectedCloseDate, to),
accountId ? eq(demandDeals.accountId, accountId) : undefined,
ownerUserId ? eq(demandDeals.ownerUserId, ownerUserId) : undefined,
),
),
this.db
.select({ value: count() })
.from(contracts)
.where(this.renewalPredicate(query)),
this.db
.select({ value: count() })
.from(contractObligations)
.innerJoin(contracts, eq(contracts.id, contractObligations.contractId))
.where(
and(
gte(contractObligations.dueAt, from),
lt(contractObligations.dueAt, to),
// Outstanding only. A count that includes work already done reads
// as a backlog that is not there.
isNull(contractObligations.completedAt),
accountId ? eq(contracts.accountId, accountId) : undefined,
ownerUserId ? eq(contractObligations.ownerUserId, ownerUserId) : undefined,
),
),
// An expiring authorisation has no owner column, so an owner filter can
// only ever exclude it — reporting zero rather than the whole book.
ownerUserId
? Promise.resolve([{ value: 0 }])
: this.db
.select({ value: count() })
.from(exportAuthorizations)
.where(
and(
gte(exportAuthorizations.expiresAt, from),
lt(exportAuthorizations.expiresAt, to),
accountId ? eq(exportAuthorizations.accountId, accountId) : undefined,
),
),
]);
return {
weightedPipelineCents: Math.round(Number(pipeline[0]?.weightedCents ?? 0)),
closingCount: pipeline[0]?.closing ?? 0,
renewalCount: renewals[0]?.value ?? 0,
obligationCount: obligations[0]?.value ?? 0,
expiringAuthorizationCount: authorizations[0]?.value ?? 0,
};
}
// ----------------------------------------------------------------- sources
private async expectedClose(query: CalendarQuery, now: Date, limit: number) {
const rows = await this.db
.select({ deal: demandDeals, accountName: accounts.name })
.from(demandDeals)
.leftJoin(accounts, eq(accounts.id, demandDeals.accountId))
.where(
and(
gte(demandDeals.expectedCloseDate, query.from),
lt(demandDeals.expectedCloseDate, query.to),
query.accountId ? eq(demandDeals.accountId, query.accountId) : undefined,
query.ownerUserId ? eq(demandDeals.ownerUserId, query.ownerUserId) : undefined,
),
)
.orderBy(asc(demandDeals.expectedCloseDate))
.limit(limit + 1);
return bounded(rows, limit, ({ deal, accountName }) => {
const at = deal.expectedCloseDate!;
const probability = numeric(deal.probability);
return {
id: calendarEventId('demand_deal', deal.id, 'expectedCloseDate'),
kind: 'expected_close' as const,
title: deal.name,
startsAt: at.toISOString(),
endsAt: null,
isSpan: false,
state: eventState({ at, now, completedAt: deal.closedAt }),
accountId: deal.accountId,
accountName,
ownerUserId: deal.ownerUserId,
amountCents: deal.acvCents,
currency: deal.currency,
recordType: 'demand_deal',
recordId: deal.id,
href: href('demand', 'deal', deal.id),
meta: {
stage: deal.stage,
probability,
productLine: deal.productLine,
weightedCents:
deal.acvCents !== null && probability !== null
? Math.round(deal.acvCents * probability)
: null,
},
};
});
}
private async contractDate(
query: CalendarQuery,
now: Date,
limit: number,
kind: 'contract_effective' | 'contract_expiry' | 'contract_executed',
) {
const column =
kind === 'contract_effective'
? contracts.effectiveAt
: kind === 'contract_expiry'
? contracts.expiresAt
: contracts.executedAt;
const field =
kind === 'contract_effective'
? 'effectiveAt'
: kind === 'contract_expiry'
? 'expiresAt'
: 'executedAt';
const label =
kind === 'contract_effective'
? 'takes effect'
: kind === 'contract_expiry'
? 'expires'
: 'executed';
const rows = await this.db
.select({ contract: contracts, accountName: accounts.name })
.from(contracts)
.leftJoin(accounts, eq(accounts.id, contracts.accountId))
.where(
and(
gte(column, query.from),
lt(column, query.to),
query.accountId ? eq(contracts.accountId, query.accountId) : undefined,
query.ownerUserId ? eq(contracts.ownerUserId, query.ownerUserId) : undefined,
),
)
.orderBy(asc(column))
.limit(limit + 1);
return bounded(rows, limit, ({ contract, accountName }) => {
const at = contract[field]!;
return {
id: calendarEventId('contract', contract.id, field),
kind,
title: `${contract.title} ${label}`,
startsAt: at.toISOString(),
endsAt: null,
isSpan: false,
// An executed date is a fact about the past, not an errand: it is
// recorded as done so it does not sit in the overdue list forever.
state:
kind === 'contract_executed'
? ('done' as const)
: eventState({ at, now, completedAt: contract.terminatedAt }),
accountId: contract.accountId,
accountName,
ownerUserId: contract.ownerUserId,
amountCents: contract.valueCents,
currency: contract.currency,
recordType: 'contract',
recordId: contract.id,
href: href('contracts', 'contract', contract.id),
meta: {
contractType: contract.type,
status: contract.status,
side: contract.side,
terminatedAt: contract.terminatedAt?.toISOString() ?? null,
},
};
});
}
/**
* The SQL narrows; `renewalAlarm()` decides.
*
* The predicate repeats the expiry-minus-notice arithmetic only to keep the
* scan bounded — the alternative is loading every auto-renewing contract in
* the book. Every date and state that reaches a caller comes from the shared
* function, so there is still exactly one definition of the rule.
*/
private renewalPredicate(query: CalendarQuery) {
return and(
eq(contracts.isAutoRenew, true),
isNotNull(contracts.noticeDays),
isNotNull(contracts.expiresAt),
// A terminated contract will not renew, so its notice date is not a
// deadline anyone should be chased about.
isNull(contracts.terminatedAt),
// The bounds are bound as ISO text and cast, not as `Date`: drizzle types
// parameters from the column in a comparison, and a raw template has no
// column to learn from, so postgres-js receives a Date it cannot encode
// and the whole request 500s. Found by calling the endpoint.
sql`${contracts.expiresAt} - make_interval(days => ${contracts.noticeDays}) >= ${query.from.toISOString()}::timestamptz`,
sql`${contracts.expiresAt} - make_interval(days => ${contracts.noticeDays}) < ${query.to.toISOString()}::timestamptz`,
query.accountId ? eq(contracts.accountId, query.accountId) : undefined,
query.ownerUserId ? eq(contracts.ownerUserId, query.ownerUserId) : undefined,
);
}
private async renewalNotices(query: CalendarQuery, now: Date, limit: number) {
const rows = await this.db
.select({ contract: contracts, accountName: accounts.name })
.from(contracts)
.leftJoin(accounts, eq(accounts.id, contracts.accountId))
.where(this.renewalPredicate(query))
.orderBy(asc(contracts.expiresAt))
.limit(limit + 1);
return bounded(rows, limit, ({ contract, accountName }) => {
const alarm = renewalAlarm(contract, now);
const at = alarm.renewalNoticeAt!;
return {
id: calendarEventId('contract', contract.id, 'renewalNoticeAt'),
kind: 'renewal_notice' as const,
title: `Renewal notice — ${contract.title}`,
startsAt: at.toISOString(),
endsAt: null,
isSpan: false,
// 'expired' means the window to give notice has gone; the notice date
// itself is simply late until then.
state:
alarm.renewalState === 'expired'
? ('overdue' as const)
: eventState({ at, now }),
accountId: contract.accountId,
accountName,
ownerUserId: contract.ownerUserId,
amountCents: contract.valueCents,
currency: contract.currency,
recordType: 'contract',
recordId: contract.id,
href: href('contracts', 'contract', contract.id),
meta: {
renewalState: alarm.renewalState,
expiresAt: contract.expiresAt?.toISOString() ?? null,
noticeDays: contract.noticeDays,
side: contract.side,
},
};
});
}
/**
* Every obligation on every contract, in one query.
*
* Obligations were reachable only inside GET /api/contracts/:id, so a
* quarter of them meant one request per contract. They are the dated things
* most likely to be missed, which makes that the wrong place for them to be.
*/
private async obligations(query: CalendarQuery, now: Date, limit: number) {
const rows = await this.db
.select({
obligation: contractObligations,
contract: contracts,
accountName: accounts.name,
})
.from(contractObligations)
.innerJoin(contracts, eq(contracts.id, contractObligations.contractId))
.leftJoin(accounts, eq(accounts.id, contracts.accountId))
.where(
and(
gte(contractObligations.dueAt, query.from),
lt(contractObligations.dueAt, query.to),
query.accountId ? eq(contracts.accountId, query.accountId) : undefined,
query.ownerUserId
? eq(contractObligations.ownerUserId, query.ownerUserId)
: undefined,
),
)
.orderBy(asc(contractObligations.dueAt))
.limit(limit + 1);
return bounded(rows, limit, ({ obligation, contract, accountName }) => ({
id: calendarEventId('contract_obligation', obligation.id, 'dueAt'),
kind: 'obligation_due' as const,
title: obligation.title,
startsAt: obligation.dueAt.toISOString(),
endsAt: null,
isSpan: false,
state: eventState({
at: obligation.dueAt,
now,
completedAt: obligation.completedAt,
}),
accountId: contract.accountId,
accountName,
ownerUserId: obligation.ownerUserId,
amountCents: null,
currency: null,
recordType: 'contract_obligation',
recordId: obligation.id,
href: href('contracts', 'contract', contract.id),
meta: {
obligationKind: obligation.kind,
contractId: contract.id,
contractTitle: contract.title,
completedAt: obligation.completedAt?.toISOString() ?? null,
},
}));
}
/**
* Commitment windows, split on the capacity shape where one is present.
*
* A commitment ramps and steps — it is not a rectangle — and `shape` is
* authoritative over `startsAt`/`endsAt` when set. Drawing one bar across
* the whole term shows a seller capacity in a month it does not exist in,
* which is exactly the mistake the shape column was added to prevent.
*/
private async capacityWindows(query: CalendarQuery, now: Date, limit: number) {
// No owner column anywhere on the supply chain of custody, so an owner
// filter cannot be satisfied and must exclude the source outright.
if (query.ownerUserId) return { events: [], truncated: false };
const rows = await this.db
.select({ commitment: capacityCommitments, accountName: accounts.name })
.from(capacityCommitments)
.leftJoin(accounts, eq(accounts.id, capacityCommitments.accountId))
.where(
and(
lt(capacityCommitments.startsAt, query.to),
gt(capacityCommitments.endsAt, query.from),
query.accountId ? eq(capacityCommitments.accountId, query.accountId) : undefined,
),
)
.orderBy(asc(capacityCommitments.startsAt))
.limit(limit + 1);
const truncated = rows.length > limit;
if (truncated) rows.length = limit;
const events: CalendarEvent[] = [];
for (const { commitment, accountName } of rows) {
const base = {
kind: 'capacity_window' as const,
isSpan: true,
accountId: commitment.accountId,
accountName,
ownerUserId: null,
amountCents: null,
currency: commitment.currency,
recordType: 'capacity_commitment',
recordId: commitment.id,
href: href('capacity', 'commitment', commitment.id),
};
const shape = commitment.shape;
const subSpans =
shape && shape.intervals.length >= 2 && shape.quantities.length >= 1
? shape.intervals.slice(0, -1).map((boundary, index) => ({
index,
startsAt: new Date(boundary),
endsAt: new Date(shape.intervals[index + 1]!),
gpuCount: shape.quantities[index] ?? commitment.gpuCount,
}))
: [
{
index: null,
startsAt: commitment.startsAt,
endsAt: commitment.endsAt,
gpuCount: commitment.gpuCount,
},
];
for (const span of subSpans) {
if (Number.isNaN(span.startsAt.getTime()) || Number.isNaN(span.endsAt.getTime())) {
continue;
}
if (span.startsAt >= query.to || span.endsAt <= query.from) continue;
events.push({
...base,
id: calendarEventId(
'capacity_commitment',
commitment.id,
span.index === null ? 'window' : `shape.${span.index}`,
),
title:
span.index === null
? commitment.name
: `${commitment.name}${span.gpuCount}× ${commitment.gpuType}`,
startsAt: span.startsAt.toISOString(),
endsAt: span.endsAt.toISOString(),
state: commitment.terminatedAt
? ('done' as const)
: spanState({ startsAt: span.startsAt, endsAt: span.endsAt, now }),
meta: {
gpuType: commitment.gpuType,
gpuCount: span.gpuCount,
envelopeGpuCount: commitment.gpuCount,
shaped: span.index !== null,
costPerGpuHourCents: commitment.costPerGpuHourCents,
terminatedAt: commitment.terminatedAt?.toISOString() ?? null,
},
});
}
}
return { events, truncated };
}
private async allocationWindows(query: CalendarQuery, now: Date, limit: number) {
if (query.ownerUserId) return { events: [], truncated: false };
const rows = await this.db
.select({
allocation: allocations,
commitmentName: capacityCommitments.name,
dealName: demandDeals.name,
accountId: demandDeals.accountId,
accountName: accounts.name,
})
.from(allocations)
.leftJoin(
capacityCommitments,
eq(capacityCommitments.id, allocations.capacityCommitmentId),
)
.leftJoin(demandDeals, eq(demandDeals.id, allocations.demandDealId))
.leftJoin(accounts, eq(accounts.id, demandDeals.accountId))
.where(
and(
lt(allocations.startsAt, query.to),
gt(allocations.endsAt, query.from),
query.accountId ? eq(demandDeals.accountId, query.accountId) : undefined,
),
)
.orderBy(asc(allocations.startsAt))
.limit(limit + 1);
return bounded(rows, limit, (row) => {
const { allocation } = row;
const gpuHours = numeric(allocation.gpuHours) ?? 0;
return {
id: calendarEventId('allocation', allocation.id, 'window'),
kind: 'allocation_window' as const,
title:
row.dealName ??
(allocation.internalTeam
? `Internal — ${allocation.internalTeam}`
: (row.commitmentName ?? 'Allocation')),
startsAt: allocation.startsAt.toISOString(),
endsAt: allocation.endsAt.toISOString(),
isSpan: true,
state:
allocation.releasedAt !== null
? ('done' as const)
: spanState({
startsAt: allocation.startsAt,
endsAt: allocation.endsAt,
now,
}),
accountId: row.accountId ?? null,
accountName: row.accountName ?? null,
ownerUserId: null,
// Revenue over the window, in cents — hours are fractional, money is not.
amountCents: Math.round(gpuHours * allocation.pricePerGpuHourCents),
currency: allocation.currency,
recordType: 'allocation',
recordId: allocation.id,
href: href('capacity', 'allocation', allocation.id),
meta: {
status: allocation.status,
guaranteeType: allocation.guaranteeType,
gpuHours,
internalTeam: allocation.internalTeam,
commitmentId: allocation.capacityCommitmentId,
releasedAt: allocation.releasedAt?.toISOString() ?? null,
},
};
});
}
/**
* A hold expiring is the one date on this calendar that changes what can be
* sold: the moment it passes, the capacity returns to everyone else's
* availability. It has never been visible anywhere.
*/
private async holdExpiries(query: CalendarQuery, now: Date, limit: number) {
if (query.ownerUserId) return { events: [], truncated: false };
const rows = await this.db
.select({
allocation: allocations,
dealName: demandDeals.name,
accountId: demandDeals.accountId,
accountName: accounts.name,
})
.from(allocations)
.leftJoin(demandDeals, eq(demandDeals.id, allocations.demandDealId))
.leftJoin(accounts, eq(accounts.id, demandDeals.accountId))
.where(
and(
gte(allocations.holdExpiresAt, query.from),
lt(allocations.holdExpiresAt, query.to),
query.accountId ? eq(demandDeals.accountId, query.accountId) : undefined,
),
)
.orderBy(asc(allocations.holdExpiresAt))
.limit(limit + 1);
return bounded(rows, limit, (row) => {
const at = row.allocation.holdExpiresAt!;
return {
id: calendarEventId('allocation', row.allocation.id, 'holdExpiresAt'),
kind: 'hold_expiry' as const,
title: `Hold expires — ${row.dealName ?? 'unassigned capacity'}`,
startsAt: at.toISOString(),
endsAt: null,
isSpan: false,
state: eventState({ at, now, completedAt: row.allocation.releasedAt }),
accountId: row.accountId ?? null,
accountName: row.accountName ?? null,
ownerUserId: null,
// What was turned away to keep the hold. Makes the deadline honest.
amountCents: row.allocation.holdOpportunityCostCents,
currency: row.allocation.currency,
recordType: 'allocation',
recordId: row.allocation.id,
href: href('capacity', 'allocation', row.allocation.id),
meta: {
status: row.allocation.status,
gpuHours: numeric(row.allocation.gpuHours),
commitmentId: row.allocation.capacityCommitmentId,
},
};
});
}
private async supplyAvailability(query: CalendarQuery, now: Date, limit: number) {
const rows = await this.db
.select({ deal: supplyDeals, accountName: accounts.name })
.from(supplyDeals)
.leftJoin(accounts, eq(accounts.id, supplyDeals.accountId))
.where(
and(
gte(supplyDeals.availableFrom, query.from),
lt(supplyDeals.availableFrom, query.to),
query.accountId ? eq(supplyDeals.accountId, query.accountId) : undefined,
query.ownerUserId ? eq(supplyDeals.ownerUserId, query.ownerUserId) : undefined,
),
)
.orderBy(asc(supplyDeals.availableFrom))
.limit(limit + 1);
return bounded(rows, limit, ({ deal, accountName }) => {
const at = deal.availableFrom!;
return {
id: calendarEventId('supply_deal', deal.id, 'availableFrom'),
kind: 'supply_available_from' as const,
title: `Capacity available — ${deal.name}`,
startsAt: at.toISOString(),
endsAt: null,
isSpan: false,
state: eventState({ at, now, completedAt: deal.closedAt }),
accountId: deal.accountId,
accountName,
ownerUserId: deal.ownerUserId,
amountCents: null,
currency: null,
recordType: 'supply_deal',
recordId: deal.id,
href: href('supply', 'deal', deal.id),
meta: {
stage: deal.stage,
gpuType: deal.gpuType,
gpuCount: deal.gpuCount,
targetCostPerGpuHourCents: deal.targetCostPerGpuHourCents,
},
};
});
}
/**
* An expired export authorisation silently converts lawful business into
* unlawful business. The schema says so and indexes the column for it, and
* until this endpoint nothing in PIG read it — no endpoint, no screen.
*/
private async authorizationExpiries(query: CalendarQuery, now: Date, limit: number) {
if (query.ownerUserId) return { events: [], truncated: false };
const rows = await this.db
.select({ authorization: exportAuthorizations, accountName: accounts.name })
.from(exportAuthorizations)
.leftJoin(accounts, eq(accounts.id, exportAuthorizations.accountId))
.where(
and(
gte(exportAuthorizations.expiresAt, query.from),
lt(exportAuthorizations.expiresAt, query.to),
query.accountId ? eq(exportAuthorizations.accountId, query.accountId) : undefined,
),
)
.orderBy(asc(exportAuthorizations.expiresAt))
.limit(limit + 1);
return bounded(rows, limit, ({ authorization, accountName }) => {
const at = authorization.expiresAt!;
return {
id: calendarEventId('export_authorization', authorization.id, 'expiresAt'),
kind: 'authorization_expiry' as const,
title: `Export authorisation expires — ${accountName ?? 'account'}`,
startsAt: at.toISOString(),
endsAt: null,
isSpan: false,
// Never 'done': an authorisation is not something anyone completes,
// and marking a lapsed one finished is the failure mode itself.
state: eventState({ at, now }),
accountId: authorization.accountId,
accountName,
ownerUserId: null,
amountCents: null,
currency: null,
recordType: 'export_authorization',
recordId: authorization.id,
href: href('accounts', 'account', authorization.accountId),
meta: {
authorizationType: authorization.authorizationType,
reference: authorization.reference,
// Rules in flux for this counterparty: re-verify, do not trust the date.
volatile: authorization.volatile,
evidenceUrl: authorization.evidenceUrl,
verifiedByUserId: authorization.verifiedByUserId,
},
};
});
}
private async artifactExpiries(query: CalendarQuery, now: Date, limit: number) {
if (query.ownerUserId) return { events: [], truncated: false };
const rows = await this.db
.select({ artifact: complianceArtifacts, accountName: accounts.name })
.from(complianceArtifacts)
.leftJoin(accounts, eq(accounts.id, complianceArtifacts.accountId))
.where(
and(
gte(complianceArtifacts.expiresAt, query.from),
lt(complianceArtifacts.expiresAt, query.to),
query.accountId ? eq(complianceArtifacts.accountId, query.accountId) : undefined,
),
)
.orderBy(asc(complianceArtifacts.expiresAt))
.limit(limit + 1);
return bounded(rows, limit, ({ artifact, accountName }) => {
const at = artifact.expiresAt!;
return {
id: calendarEventId('compliance_artifact', artifact.id, 'expiresAt'),
kind: 'artifact_expiry' as const,
title: `${artifact.claim} expires — ${accountName ?? 'account'}`,
startsAt: at.toISOString(),
endsAt: null,
isSpan: false,
state: eventState({ at, now }),
accountId: artifact.accountId,
accountName,
ownerUserId: null,
amountCents: null,
currency: null,
recordType: 'compliance_artifact',
recordId: artifact.id,
href: href('accounts', 'account', artifact.accountId),
meta: {
claim: artifact.claim,
scope: artifact.scope,
// Certification versus self-declared alignment decides procurement,
// so it travels with the deadline rather than being looked up later.
isCertified: artifact.isCertified,
soc2Type: artifact.soc2Type,
verifiedByUserId: artifact.verifiedByUserId,
},
};
});
}
private async entries(query: CalendarQuery, now: Date, limit: number) {
const rows = await this.db
.select({ entry: calendarEntries, accountName: accounts.name })
.from(calendarEntries)
.leftJoin(accounts, eq(accounts.id, calendarEntries.accountId))
.where(
and(
// A dated entry with no end is a point; one with an end is a span,
// and a span overlaps the window whenever it has not already closed.
lt(calendarEntries.startsAt, query.to),
or(
and(isNull(calendarEntries.endsAt), gte(calendarEntries.startsAt, query.from)),
and(isNotNull(calendarEntries.endsAt), gt(calendarEntries.endsAt, query.from)),
),
query.accountId ? eq(calendarEntries.accountId, query.accountId) : undefined,
query.ownerUserId ? eq(calendarEntries.ownerUserId, query.ownerUserId) : undefined,
),
)
.orderBy(asc(calendarEntries.startsAt))
.limit(limit + 1);
return bounded(rows, limit, ({ entry, accountName }) => ({
id: calendarEventId('calendar_entry', entry.id, 'startsAt'),
kind: 'calendar_entry' as const,
title: entry.title,
startsAt: entry.startsAt.toISOString(),
endsAt: entry.endsAt?.toISOString() ?? null,
isSpan: entry.endsAt !== null,
// Not `spanState`: this is the one projected row type with a completion
// column, so a closed window is overdue until `completed_at` says
// otherwise. Whether a missed QBR is flagged must not depend on whether
// its author happened to type an end time.
state: entry.endsAt
? completableSpanState({
startsAt: entry.startsAt,
endsAt: entry.endsAt,
now,
completedAt: entry.completedAt,
})
: eventState({ at: entry.startsAt, now, completedAt: entry.completedAt }),
accountId: entry.accountId,
accountName,
ownerUserId: entry.ownerUserId,
amountCents: null,
currency: null,
recordType: 'calendar_entry',
recordId: entry.id,
href: href('calendar', 'entry', entry.id),
meta: {
entryKind: entry.kind,
allDay: entry.allDay,
description: entry.description,
demandDealId: entry.demandDealId,
supplyDealId: entry.supplyDealId,
completedAt: entry.completedAt?.toISOString() ?? null,
},
}));
}
}
// ------------------------------------------------------------------- helpers
async function empty(): Promise<{ events: CalendarEvent[]; truncated: boolean }> {
return { events: [], truncated: false };
}
/**
* Each source asks for one row more than its budget. Detecting truncation any
* other way means either a second count query per source or silently returning
* a partial quarter as if it were whole.
*/
function bounded<Row>(
rows: Row[],
limit: number,
toEvent: (row: Row) => CalendarEvent,
): { events: CalendarEvent[]; truncated: boolean } {
const truncated = rows.length > limit;
if (truncated) rows.length = limit;
return { events: rows.map(toEvent), truncated };
}