@tangle-network/agent-app 0.45.31 → 0.45.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DesignCanvas-RSWQCXM3.js → DesignCanvas-EODMDQMT.js} +2 -2
- package/dist/{DesignCanvasEditor-XZMGIST7.js → DesignCanvasEditor-DRRZ6ZD3.js} +4 -4
- package/dist/{InvitationsPanel-CJ7DR4F7.js → InvitationsPanel-BATXZHY4.js} +2 -2
- package/dist/{MembersPanel-BB4OD2KB.js → MembersPanel-XNLF3SA4.js} +2 -2
- package/dist/assistant/index.js +11 -10
- package/dist/assistant/index.js.map +1 -1
- package/dist/chat-react/index.js +1 -1
- package/dist/{chunk-YIQHOAHN.js → chunk-3UBAO3N5.js} +3 -3
- package/dist/{chunk-YIQHOAHN.js.map → chunk-3UBAO3N5.js.map} +1 -1
- package/dist/{chunk-PURHUAOR.js → chunk-556RB3XI.js} +5 -5
- package/dist/chunk-556RB3XI.js.map +1 -0
- package/dist/{chunk-NEJ7OMQS.js → chunk-5LC2VXH5.js} +102 -4
- package/dist/chunk-5LC2VXH5.js.map +1 -0
- package/dist/{chunk-HZ4WCA37.js → chunk-5MZXWPFN.js} +9 -9
- package/dist/chunk-5MZXWPFN.js.map +1 -0
- package/dist/{chunk-FNM7JYME.js → chunk-5N434BGF.js} +17 -17
- package/dist/chunk-5N434BGF.js.map +1 -0
- package/dist/{chunk-5SXS3YAB.js → chunk-7MV23XBW.js} +2 -2
- package/dist/chunk-7MV23XBW.js.map +1 -0
- package/dist/{chunk-S564OFTL.js → chunk-F4XLTVQ3.js} +3 -3
- package/dist/{chunk-S564OFTL.js.map → chunk-F4XLTVQ3.js.map} +1 -1
- package/dist/{chunk-LSQR6HM6.js → chunk-IN6LNZ7O.js} +35 -34
- package/dist/chunk-IN6LNZ7O.js.map +1 -0
- package/dist/{chunk-L7FHRORY.js → chunk-SFSR3CAH.js} +180 -40
- package/dist/chunk-SFSR3CAH.js.map +1 -0
- package/dist/{chunk-QGOU3VXH.js → chunk-UXRJAUJL.js} +3 -3
- package/dist/{chunk-IFYJDX3J.js → chunk-XHD4Y54K.js} +47 -46
- package/dist/chunk-XHD4Y54K.js.map +1 -0
- package/dist/design-canvas-react/index.js +8 -8
- package/dist/design-canvas-react/index.js.map +1 -1
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/spend/cli.js +1 -1
- package/dist/spend/index.d.ts +212 -7
- package/dist/spend/index.js +5 -1
- package/dist/spend/index.js.map +1 -1
- package/dist/studio-react/index.js +27 -27
- package/dist/studio-react/index.js.map +1 -1
- package/dist/teams-react/index.js +2 -2
- package/dist/teams-react/lazy.js +2 -2
- package/dist/theme/index.js +1 -1
- package/dist/theme/tailwind-preset.d.ts +5 -1
- package/dist/theme/tailwind-preset.js +11 -2
- package/dist/theme/tailwind-preset.js.map +1 -1
- package/dist/theme/tokens.css +126 -79
- package/dist/web-react/async/index.js +1 -1
- package/dist/web-react/index.d.ts +60 -2
- package/dist/web-react/index.js +13 -3
- package/dist/work-product-react/index.js +3 -3
- package/dist/work-product-react/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-5SXS3YAB.js.map +0 -1
- package/dist/chunk-FNM7JYME.js.map +0 -1
- package/dist/chunk-HZ4WCA37.js.map +0 -1
- package/dist/chunk-IFYJDX3J.js.map +0 -1
- package/dist/chunk-L7FHRORY.js.map +0 -1
- package/dist/chunk-LSQR6HM6.js.map +0 -1
- package/dist/chunk-NEJ7OMQS.js.map +0 -1
- package/dist/chunk-PURHUAOR.js.map +0 -1
- /package/dist/{DesignCanvas-RSWQCXM3.js.map → DesignCanvas-EODMDQMT.js.map} +0 -0
- /package/dist/{DesignCanvasEditor-XZMGIST7.js.map → DesignCanvasEditor-DRRZ6ZD3.js.map} +0 -0
- /package/dist/{InvitationsPanel-CJ7DR4F7.js.map → InvitationsPanel-BATXZHY4.js.map} +0 -0
- /package/dist/{MembersPanel-BB4OD2KB.js.map → MembersPanel-XNLF3SA4.js.map} +0 -0
- /package/dist/{chunk-QGOU3VXH.js.map → chunk-UXRJAUJL.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/spend/types.ts","../src/spend/ownership.ts","../src/spend/ceiling.ts","../src/spend/reference.ts","../src/spend/reconcile.ts","../src/spend/report.ts"],"sourcesContent":["/**\n * The vocabulary of consumer-side spend verification.\n *\n * Kept in its own module with ZERO imports so a product can type its storage\n * rows and its reconciliation config without pulling in `node:fs` through the\n * CLI half.\n *\n * The model in one paragraph: the platform's ledger is authoritative about what\n * was CHARGED. A product knows something the ledger does not — what it ASKED\n * for. Recording that second view, and diffing it against the first, is what\n * turns a platform billing defect from silent money into an alert. Nothing here\n * lets a product self-certify a charge away; the output is a discrepancy a human\n * disputes.\n */\n\n// ── the product's own view of a box's life ────────────────────────────────────\n\n/**\n * One box, as the PRODUCT understands it. Folded, not an append-only log: a\n * product runs one row per sandbox, and every field below is derived by a\n * monotonic fold (see `foldSpendBoxRecord`) so two concurrent writers cannot\n * produce a wrong answer, only a stale one.\n *\n * Timestamps are epoch ms throughout, matching the platform's own\n * `sandbox_meta.last_started_at`.\n */\nexport interface SpendBoxRecord {\n /** The platform's sandbox id — the join key to every settlement row. */\n readonly sandboxId: string\n /** The product's own tenancy unit, which the platform does not model. */\n readonly workspaceId: string\n /** First moment the product knew this box existed. */\n readonly createdAt: number\n /**\n * The idle timeout the product ASKED the platform for, seconds. This is the\n * width of the grace window between the last thing the product saw and the\n * moment the platform should have stopped billing.\n */\n readonly idleTimeoutSeconds: number\n /**\n * The maximum lifetime the product asked for, seconds, when it asked for one.\n * This is the strongest bound a product holds: the platform destroys the box\n * at `createdAt + maxLifetimeSeconds` regardless of what the product observed,\n * so it caps the ceiling even when nothing else can (see `computeExpectedCeiling`).\n */\n readonly maxLifetimeSeconds: number | null\n /** Latest moment the product OBSERVED the box doing work. */\n readonly lastActivityAt: number\n /**\n * Detached runs dispatched but never observed to finish, by run id.\n *\n * Non-empty means the product genuinely cannot bound this box from its own\n * observations: it handed the platform work and disconnected. The ceiling\n * degrades accordingly rather than pretending to a tightness it did not earn.\n */\n readonly openDetachedRunIds: readonly string[]\n /** When the product knows the box stopped. Cleared by later activity. */\n readonly stoppedAt: number | null\n /** When the product knows the box was deleted. Set once — a deleted id never returns. */\n readonly deletedAt: number | null\n /** Opaque product-column values, written verbatim and never read here. */\n readonly extras?: Record<string, unknown>\n}\n\n/**\n * A fold step. Every field states its own merge rule, so a SQL implementation\n * can apply it in one statement and reach the same record an in-memory\n * read-modify-write reaches.\n */\nexport interface SpendBoxPatch {\n /** Advance `lastActivityAt` to the max of stored and this. Never moves backward. */\n readonly observedActivityAt?: number\n /** Add a run id to `openDetachedRunIds` (set semantics — re-adding is a no-op). */\n readonly openDetachedRunAdd?: string\n /** Remove a run id from `openDetachedRunIds`. Removing an absent id is a no-op. */\n readonly openDetachedRunRemove?: string\n /**\n * Latest-wins. Activity observed AFTER a recorded stop clears it: a box that\n * worked after we thought it stopped is running again, and a stale stop would\n * make the ceiling too tight.\n */\n readonly stoppedAt?: number\n /** Set-once. A later delete observation does not move the first one. */\n readonly deletedAt?: number\n}\n\n// ── what bounds a box's billable time ─────────────────────────────────────────\n\n/** Which fact bounds a box's billable time, weakest last. */\nexport type CeilingBasis =\n /** The product observed deletion. Billing cannot run past a box that is gone. */\n | 'deleted'\n /** The product observed a stop. Billing should have closed there. */\n | 'stopped'\n /** No stop seen, but the platform destroys the box at its max lifetime. */\n | 'max-lifetime'\n /** No stop seen; the platform's idle timer is what should have closed billing. */\n | 'idle-timeout'\n /**\n * An unfinished detached run and no max lifetime — the product cannot bound\n * this box at all, so the ceiling degrades to the reconciliation instant.\n * A finding on this basis is weak evidence and says so.\n */\n | 'open-detached-run'\n\n/** The upper bound on one box's billable duration, and what earned it. */\nexport interface ExpectedCeiling {\n readonly sandboxId: string\n readonly basis: CeilingBasis\n /** The latest instant this box could still have been billable, epoch ms. */\n readonly horizonAt: number\n /** `horizonAt - createdAt + toleranceMs`. The upper bound on billable ms. */\n readonly ceilingMs: number\n readonly toleranceMs: number\n /**\n * False when the basis is `open-detached-run` — the ceiling then rests on the\n * reconciliation instant rather than on anything the product observed, so an\n * overage means the platform billed outside the box's own lifetime, not merely\n * longer than expected.\n */\n readonly bounded: boolean\n}\n\n// ── what the platform actually emitted ────────────────────────────────────────\n\n/**\n * One settled ledger row, in the shape the platform's `credit_transactions`\n * table stores it. The product supplies these through its own fetch (the\n * platform's credit-history API, an export, a mirror) — this package never\n * reaches for them, because the ledger is the counterparty's record and reading\n * it is the product's authenticated business.\n *\n * The rows a product can fetch are scoped to the BILLING OWNER, not to the\n * product: `product: 'sandbox'` is the platform's service taxonomy and every\n * consumer app's compute wears it. So a wallet running two of our products\n * returns both products' settlements, and telling them apart is\n * {@link SpendOwnershipRule}'s job.\n */\nexport interface SettlementRow {\n /** The ledger row id, for the dispute. */\n readonly id: string\n /**\n * `sandbox:<kind>:<sandboxId>:<intervalStartMs>` — the platform's idempotency\n * key, and the only place the billed interval's START is recorded.\n */\n readonly referenceId: string | null\n /**\n * Signed nanodollars, exactly as the ledger stores it: negative is a charge,\n * positive is a credit or refund.\n */\n readonly amountNanoUsd: number\n /** `compute` | `refund` | `inference` | … */\n readonly type: string\n /** `sandbox` | `router` | … */\n readonly product: string | null\n /** `sandbox:<sandboxId>` — the platform's aggregation unit. */\n readonly groupKey: string | null\n /** Settlement instant, epoch ms. The product normalizes the stored text. */\n readonly createdAt: number\n readonly description: string | null\n /** Provider at-cost basis, unsigned nanodollars. Null when unattributed. */\n readonly costBasisNanoUsd: number | null\n /**\n * The billed duration, when the product's ledger view exposes it directly.\n * Null is the common case: the platform does not store duration on the row.\n */\n readonly billedMs: number | null\n /**\n * The platform API key the charge was triggered by — `credit_transactions.key_id`,\n * stamped from the box's own creation metadata at settlement time and exposed\n * by `/v1/billing/transactions` (which also filters on it).\n *\n * This is the ONLY field on a settlement row that can be attributed back to a\n * PRODUCT rather than to a wallet or to the platform's service taxonomy, which\n * is why {@link SpendOwnershipRule}'s shipped constructor is built on it. The\n * sandbox id cannot do the job: the platform mints it as `sandbox-<12 hex>`\n * from a hash of (owner, idempotency key), so a product's own box naming never\n * reaches the ledger row.\n *\n * Optional, and `null` is a real answer: the platform leaves it null on legacy\n * rows and an export may not carry the column at all. A missing key is\n * `undecidable`, never `foreign` — see {@link SpendOwnershipVerdict}.\n */\n readonly keyId?: string | null\n}\n\n// ── whose box is this? ────────────────────────────────────────────────────────\n\n/**\n * What one settlement is attributable to, from inside ONE product.\n *\n * The distinction this type exists for: a sibling product's box and a charge\n * that is not ours at all look identical from inside a single product, because\n * both arrive as a settlement naming a sandbox this product's expectation ledger\n * has never heard of. Dropping both loses the check's whole purpose; reporting\n * both makes it noise. Only a platform-stamped attribution field separates them.\n */\nexport type SpendOwnershipVerdict =\n /** Attributable to THIS product. An unrecorded one is a phantom-charge candidate. */\n | 'mine'\n /** Attributable to a DIFFERENT product on the same wallet. Reported, never a finding. */\n | 'foreign'\n /**\n * The row carries nothing that decides it. Counted as `mine` — FAIL CLOSED.\n * An unattributable charge on a shared wallet is precisely the shape of the\n * thing this module exists to catch, so the ambiguous case costs a human five\n * minutes rather than costing the product the detection.\n */\n | 'undecidable'\n\n/** One settlement, presented to an ownership rule. */\nexport interface SpendOwnershipCandidate {\n readonly row: SettlementRow\n /** The sandbox the row is attributable to, or null for a row naming none. */\n readonly sandboxId: string | null\n}\n\n/**\n * The product's declaration of which settlements are its own.\n *\n * The one hard constraint on `decide`: it must answer from PROPERTIES OF THE\n * ROW. A rule that answers by looking the sandbox up in the product's own\n * expectation ledger deletes `unknown-box` entirely — every unrecorded box would\n * be `foreign` by construction, and \"we were billed for a box we never asked\n * for\" would become unrepresentable. The ledger already decides recorded-ness;\n * this decides ATTRIBUTION, and the two must stay independent.\n *\n * `decide` must not throw. If it does, the throw propagates and the whole pass\n * fails — a reconciliation whose ownership rule is broken has no verdict worth\n * printing, and swallowing it would turn a broken rule into a clean bill.\n */\nexport interface SpendOwnershipRule {\n /** Named in the report and on every finding, so a reader knows what was excluded. */\n readonly label: string\n decide(candidate: SpendOwnershipCandidate): SpendOwnershipVerdict\n}\n\n/**\n * What this pass scoped itself to — present on every report, including a clean\n * one, because \"nothing fired\" and \"nothing was looked at\" are different\n * answers and a report that cannot tell them apart is the failure this closes.\n */\nexport interface SpendOwnershipSummary {\n /**\n * False when the caller declared no rule. The pass then treats every box as\n * its own — today's behaviour, which over-reports rather than under-reports —\n * and every `unknown-box` finding says on its face that a sibling product's\n * box is indistinguishable from a charge that is not ours.\n */\n readonly declared: boolean\n /** The rule's label, or null when none was declared. */\n readonly label: string | null\n /** Boxes this pass treated as this product's, and what they were charged. */\n readonly ownedBoxes: number\n readonly ownedNanoUsd: number\n /**\n * Of those, the boxes no rule could decide. Counted as owned (fail-closed) and\n * reported separately so a product can see how much of its own verdict rests\n * on rows that carried no attribution.\n */\n readonly undecidableBoxes: number\n /** Boxes attributed to another product on the same wallet. Never findings. */\n readonly foreignBoxes: number\n readonly foreignNanoUsd: number\n /**\n * Their ids, in full — an exclusion a reader cannot audit is an exclusion\n * they have to trust, and this module's whole posture is that nothing about\n * money is taken on trust.\n */\n readonly foreignSandboxIds: readonly string[]\n}\n\n/** The parts of a settlement reference id, once parsed. */\nexport interface SettlementReference {\n /** `stop` | `compute` | `egress` | `gpu-lease` | anything the platform adds. */\n readonly kind: string\n /** For compute kinds, the sandbox id. For `gpu-lease`, the lease id. */\n readonly resourceId: string\n /** The interval's start, epoch ms. Null for kinds that carry no interval. */\n readonly intervalStartMs: number | null\n}\n\n/** How a settled duration was arrived at — every duration finding carries one. */\nexport type BilledDurationBasis =\n /** The ledger row carried the duration. Exact. */\n | 'reported'\n /** `amount ÷ the product's stated hourly rate`. Exact when the rate is right. */\n | 'rate'\n /**\n * `settledAt - intervalStart`. An UPPER bound, not the billed duration: a\n * correct settlement posted late by the platform's durable settlement queue\n * reads longer here than it billed. Findings on this basis say so.\n */\n | 'reference-span'\n /** No basis available — duration rules are skipped for this row. */\n | 'unknown'\n\n// ── findings ──────────────────────────────────────────────────────────────────\n\n/** The checks this reconciler runs. Each is individually skippable, by name. */\nexport type SpendCheckId =\n /** A settlement against a box the product has no record of ever asking for. */\n | 'unknown-box'\n /** A settled duration longer than the product's own upper bound allows. */\n | 'over-ceiling'\n /** A spend window far above the trailing median — the burst shape of a defect. */\n | 'velocity'\n /** The balance the product observes has gone below its floor. */\n | 'negative-balance'\n\nexport const SPEND_CHECKS: readonly SpendCheckId[] = [\n 'unknown-box',\n 'over-ceiling',\n 'velocity',\n 'negative-balance',\n]\n\n/**\n * One discrepancy, with every number the rule compared.\n *\n * Nullable fields are per-check and deliberately present-but-null rather than\n * absent: a reader scanning a JSON dump can tell \"this rule does not measure\n * that\" from \"that measurement is missing\".\n */\nexport interface SpendFinding {\n readonly check: SpendCheckId\n /** What is wrong, in one sentence, with the numbers in it. */\n readonly message: string\n /** What to do about it. A finding without a remedy is a complaint. */\n readonly remedy: string\n readonly sandboxId: string | null\n readonly workspaceId: string | null\n /** The ledger rows that evidence this finding — the dispute's exhibit list. */\n readonly referenceIds: readonly string[]\n /** Nanodollars this finding puts in question, unsigned. */\n readonly settledNanoUsd: number\n /** `over-ceiling` — the duration actually settled, and how that was derived. */\n readonly settledMs: number | null\n readonly durationBasis: BilledDurationBasis | null\n /** `over-ceiling` — the bound it broke, and what earned that bound. */\n readonly ceilingMs: number | null\n readonly overageMs: number | null\n readonly ceilingBasis: CeilingBasis | null\n /** `velocity` — the window, its trailing median, and the ratio between them. */\n readonly windowNanoUsd: number | null\n readonly trailingMedianNanoUsd: number | null\n readonly velocityRatio: number | null\n readonly windowStartAt: number | null\n /** `negative-balance` — the observed balance and the floor it broke. */\n readonly balanceNanoUsd: number | null\n readonly balanceFloorNanoUsd: number | null\n}\n\n/** What one reconciliation pass concluded. */\nexport interface SpendReport {\n /** True when nothing fired. `ok === findings.length === 0`. */\n readonly ok: boolean\n readonly findings: readonly SpendFinding[]\n readonly checksRun: readonly SpendCheckId[]\n /** Rows the pass read, including the ones no rule looked at. */\n readonly rowsExamined: number\n /** Distinct boxes those rows settled against. */\n readonly boxesExamined: number\n /** Total charged across every examined row, unsigned nanodollars. */\n readonly settledNanoUsd: number\n /** Total credited back across every examined row, unsigned nanodollars. */\n readonly creditedNanoUsd: number\n /** What this pass claimed as its own, and what it excluded as another product's. */\n readonly ownership: SpendOwnershipSummary\n /** The instant the pass treated as \"now\". */\n readonly asOf: number\n}\n","import type {\n SettlementRow,\n SpendOwnershipRule,\n SpendOwnershipVerdict,\n} from './types'\n\n/**\n * Which settlements belong to the product running the reconciliation.\n *\n * ## The problem this module is the answer to\n *\n * A product fetches its settled rows from `/v1/billing/transactions`, scoped to\n * the BILLING OWNER — the wallet whose key paid. `product: 'sandbox'` narrows\n * that to compute, but `sandbox` is the PLATFORM's service taxonomy: every\n * consumer app's box compute wears it. So a Tangle account running two of our\n * products hands each product's reconciliation the other product's boxes, and\n * `unknown-box` — correctly, by its own rule — reports one finding per sibling\n * box. Two products on one wallet make each other's spend check useless.\n *\n * ## Why the obvious fix is the wrong fix\n *\n * The obvious fix is to reconcile only the boxes already in the product's\n * expectation ledger. That removes the false findings and removes the check:\n * `unknown-box` exists to catch \"we were billed for a box we never asked for\",\n * which is the 2026-08-05 incident's day-one signature and the ONLY thing a\n * product with no lifecycle bookkeeping can catch at all. Filtering to the\n * ledger makes a phantom charge unrepresentable — the ledger's own contents\n * would define the answer.\n *\n * So the residue — settlements against boxes with no ledger record — has to be\n * SPLIT, not dropped. A sibling's box and a phantom charge are indistinguishable\n * unless something outside the product's own bookkeeping tells them apart.\n *\n * ## What can tell them apart\n *\n * Exactly one field on a settlement row, and it is not the sandbox id. The\n * platform mints that id as `sandbox-<12 hex>` — a hash of (owner, idempotency\n * key) — so a product's own box naming never reaches the ledger row, and the\n * charge's `description` carries only the resource spec. What does survive is\n * `credit_transactions.key_id`: the platform API key the box was created under,\n * stamped from the box's own creation metadata at settlement and filterable on\n * the transactions endpoint. Each product deploys with its own key, so the key\n * is the product's billing identity as the PLATFORM recorded it — not as the\n * product asserts it after the fact.\n *\n * That asymmetry is what makes the split safe. A product cannot widen its own\n * claim by claiming; it can only recognise a stamp the platform already wrote.\n *\n * ## The three rules that keep the detection intact\n *\n * 1. **A recorded box is never excluded.** Ownership is consulted only for\n * boxes with no ledger record. A rule that is wrong or over-narrow therefore\n * cannot hide an `over-ceiling` finding on a box the product recorded — the\n * incident's own 23 findings survive any rule at all.\n * 2. **Undecidable fails closed.** A row with no key attribution is `mine`, so\n * an unattributable charge on a shared wallet is reported. Silence is never\n * the answer to \"I don't know.\"\n * 3. **A box is claimed if ANY of its rows claims it.** The fold is\n * `mine > undecidable > foreign`, which is the module's standing asymmetry:\n * every derivation error pushes toward a false alarm, never toward a missed\n * charge.\n */\n\n/**\n * Claim every settlement the platform stamped with one of these API keys.\n *\n * The shipped rule, because it is the only one built on a field the PLATFORM\n * writes. Give each product its own platform key and this separates them\n * exactly; give two products the same key and no consumer-side rule can tell\n * them apart, because after settlement there is nothing left that differs — the\n * fix then is a second key, not a cleverer predicate.\n *\n * A row with no `keyId` (a legacy row, or an export that drops the column) is\n * `undecidable`, never `foreign`: the absence of an attribution is not evidence\n * that the charge is someone else's.\n *\n * @param keyIds The product's own platform API key ids. Must be non-empty — a\n * rule that owns nothing would classify every settlement as another product's\n * and report a clean bill for an account nobody is checking.\n */\nexport function ownedByBillingKeys(keyIds: readonly string[]): SpendOwnershipRule {\n const owned = new Set(keyIds.map((id) => id.trim()).filter(Boolean))\n if (owned.size === 0) {\n throw new Error(\n 'ownedByBillingKeys needs at least one key id: a rule that owns no key classifies every ' +\n 'settlement as another product\\'s and reports a clean bill for an unchecked account.',\n )\n }\n const label = `billing key ${[...owned].join(', ')}`\n return {\n label,\n decide({ row }) {\n const keyId = row.keyId?.trim()\n if (!keyId) return 'undecidable'\n return owned.has(keyId) ? 'mine' : 'foreign'\n },\n }\n}\n\n/**\n * One box's verdict, folded over every row that settled against it.\n *\n * `mine` wins over `undecidable`, which wins over `foreign`: it takes one row\n * attributable to this product to make the box this product's problem, and one\n * unattributable row to stop the box being excluded. Both directions push\n * toward reporting, which is the only direction that cannot lose money.\n *\n * Exported because a product auditing its own scoping wants the same answer the\n * reconciler reached, not a re-derivation of it.\n */\nexport function decideBoxOwnership(\n rule: SpendOwnershipRule,\n sandboxId: string,\n rows: readonly SettlementRow[],\n): SpendOwnershipVerdict {\n let verdict: SpendOwnershipVerdict = 'foreign'\n for (const row of rows) {\n const rowVerdict = rule.decide({ row, sandboxId })\n if (rowVerdict === 'mine') return 'mine'\n if (rowVerdict === 'undecidable') verdict = 'undecidable'\n }\n return verdict\n}\n","import type { CeilingBasis, ExpectedCeiling, SpendBoxRecord } from './types'\n\n/**\n * Slack allowed between the product's bound and what the platform settled,\n * before an overage is called a discrepancy. 15 minutes.\n *\n * Not a guess: it is the platform's OWN staleness threshold for compute\n * settlement. Its runbook clears an incident when\n * `/health computeSettlement.oldestAgeSeconds` is \"back under 900\" — so 900 s is\n * the age the platform itself treats as normal settlement lag, and anything\n * inside it is drift the platform has already declared acceptable. Below that a\n * product would alert on the platform's ordinary queue behaviour; far above it\n * the tolerance starts eating the signal, because the idle window it must stay\n * well under is 3600 s in every shipped product.\n *\n * It is a caller parameter because a product that asks for a shorter idle\n * timeout must shrink this with it.\n */\nexport const DEFAULT_CEILING_TOLERANCE_MS = 900_000\n\nexport interface ComputeExpectedCeilingOptions {\n /** The instant the reconciliation treats as \"now\", epoch ms. */\n readonly asOf: number\n /** Slack before an overage counts. Default {@link DEFAULT_CEILING_TOLERANCE_MS}. */\n readonly toleranceMs?: number\n}\n\n/**\n * The upper bound on how long one box could honestly have been billable.\n *\n * The whole design constraint is that this must stay an UPPER bound under\n * everything the product cannot see. Three such blind spots exist, and they\n * pull in different directions:\n *\n * - **Platform-side suspends.** The platform can park a box the product never\n * hears about. That only ever REDUCES real billable time, so an upper bound\n * is unaffected and nothing here widens for it.\n * - **Detached runs.** The product dispatches work and disconnects. The box\n * keeps working — and billing — after the last activity the product saw, so\n * `lastActivityAt` understates the truth. An unfinished detached run\n * therefore abandons the activity-based bound entirely rather than reporting\n * a bound it cannot support.\n * - **Reconnects.** A browser or worker re-attaches and work resumes. This\n * needs no special case: a reconnect is recorded as activity, the fold takes\n * the max, and the horizon moves out on its own.\n *\n * The bound that rescues the detached case is `maxLifetimeSeconds`. The platform\n * destroys the box at `createdAt + maxLifetimeSeconds` no matter what anyone\n * observed, so a product that asks for one holds a hard bound that survives\n * every blind spot above. Both shipped products ask for 86 400 s, which is why\n * the incident — 124 to 268 hours settled against boxes with a 24-hour\n * lifetime — is detectable with no lifecycle bookkeeping at all.\n */\nexport function computeExpectedCeiling(\n record: SpendBoxRecord,\n options: ComputeExpectedCeilingOptions,\n): ExpectedCeiling {\n const toleranceMs = options.toleranceMs ?? DEFAULT_CEILING_TOLERANCE_MS\n const { asOf } = options\n\n let basis: CeilingBasis\n let horizonAt: number\n\n if (record.deletedAt !== null) {\n basis = 'deleted'\n horizonAt = record.deletedAt\n } else if (record.stoppedAt !== null) {\n basis = 'stopped'\n horizonAt = record.stoppedAt\n } else if (record.openDetachedRunIds.length > 0) {\n // Nothing the product observed bounds this box. Fall back to the\n // reconciliation instant, and say so through `bounded: false`.\n basis = 'open-detached-run'\n horizonAt = asOf\n } else {\n basis = 'idle-timeout'\n horizonAt = record.lastActivityAt + record.idleTimeoutSeconds * 1000\n }\n\n // The hard platform bound wins whenever it is tighter — including over an\n // unfinished detached run, which is what turns an unbounded box back into a\n // bounded one.\n if (record.maxLifetimeSeconds !== null) {\n const lifetimeHorizon = record.createdAt + record.maxLifetimeSeconds * 1000\n if (lifetimeHorizon < horizonAt) {\n basis = 'max-lifetime'\n horizonAt = lifetimeHorizon\n }\n }\n\n // Time that has not elapsed cannot have been billed, so an idle window\n // reaching into the future does not widen the bound. This only ever tightens\n // the ceiling, so it cannot mask a charge.\n if (horizonAt > asOf) horizonAt = asOf\n // A horizon before creation would produce a negative ceiling; a box is at\n // minimum billable for the instant it existed.\n if (horizonAt < record.createdAt) horizonAt = record.createdAt\n\n return {\n sandboxId: record.sandboxId,\n basis,\n horizonAt,\n ceilingMs: horizonAt - record.createdAt + toleranceMs,\n toleranceMs,\n bounded: basis !== 'open-detached-run',\n }\n}\n","import type { SettlementReference, SettlementRow } from './types'\n\n/**\n * Parse the platform's settlement idempotency key.\n *\n * The platform mints it as `sandbox:<kind>:<resourceId>:<intervalStart>`\n * (`d1-usage-service.ts`), where `intervalStart` is the interval cursor in epoch\n * ms — the SAME `last_started_at` the settlement subtracts from to get its\n * billed duration. That makes this string the only place a consumer can read the\n * billed interval's start, because the ledger row itself stores no duration.\n *\n * Kinds seen in production: `stop` (an interval closing), `compute` (a heartbeat\n * claim), `egress`, `gpu-lease`. `stop` deliberately covers both a settle and a\n * late stop racing over the same claim, so the two derive one reference id and\n * the ledger's uniqueness constraint makes the overlap safe.\n *\n * Returns null for anything that is not a sandbox reference — a router\n * inference row, a grant, a refund — rather than guessing.\n */\nexport function parseSettlementReference(referenceId: string | null | undefined): SettlementReference | null {\n if (!referenceId) return null\n const parts = referenceId.split(':')\n if (parts.length < 3 || parts[0] !== 'sandbox') return null\n const kind = parts[1]\n if (!kind) return null\n\n // The trailing segment is the interval cursor ONLY when it reads as an epoch\n // instant. `sandbox:gpu-lease:<leaseId>` has no cursor, and a resource id that\n // happened to contain a colon must not have its tail eaten as one.\n const tail = parts[parts.length - 1] as string\n const tailMs = /^\\d+$/.test(tail) ? Number(tail) : Number.NaN\n const hasCursor = parts.length >= 4 && Number.isSafeInteger(tailMs) && tailMs > 0\n\n const resourceId = hasCursor ? parts.slice(2, -1).join(':') : parts.slice(2).join(':')\n if (!resourceId) return null\n\n return { kind, resourceId, intervalStartMs: hasCursor ? tailMs : null }\n}\n\n/**\n * Read the sandbox id out of the platform's aggregation key, `sandbox:<id>`.\n *\n * Distinct from the reference id: `groupKey` is the unit a billing statement\n * groups by and is deliberately NOT unique per row, while `referenceId` is\n * unique per interval. A null group key means \"do not aggregate\" (grants,\n * top-ups, refunds, transfers) and is not an error.\n */\nexport function parseSandboxGroupKey(groupKey: string | null | undefined): string | null {\n if (!groupKey) return null\n const parts = groupKey.split(':')\n if (parts.length < 2 || parts[0] !== 'sandbox') return null\n const id = parts.slice(1).join(':')\n return id || null\n}\n\n/**\n * The sandbox a settlement row is attributable to.\n *\n * The reference id wins over the group key because it is the field the platform\n * dedups on, so it is the one guaranteed present and correct on a compute\n * settlement; the group key is the fallback for rows written before a producer\n * stamped a reference, and for kinds whose reference names something else (a GPU\n * lease id, not a box).\n */\nexport function settlementSandboxId(row: SettlementRow): string | null {\n const reference = parseSettlementReference(row.referenceId)\n if (reference && reference.intervalStartMs !== null) return reference.resourceId\n return parseSandboxGroupKey(row.groupKey) ?? (reference ? reference.resourceId : null)\n}\n\n/** True when a row is a charge (the ledger stores charges as negative amounts). */\nexport function isCharge(row: SettlementRow): boolean {\n return row.amountNanoUsd < 0\n}\n\n/** A charge's magnitude in unsigned nanodollars; 0 for credits. */\nexport function chargeNanoUsd(row: SettlementRow): number {\n return row.amountNanoUsd < 0 ? -row.amountNanoUsd : 0\n}\n","import { DEFAULT_CEILING_TOLERANCE_MS, computeExpectedCeiling } from './ceiling'\nimport { decideBoxOwnership } from './ownership'\nimport { chargeNanoUsd, parseSettlementReference, settlementSandboxId } from './reference'\nimport type { SpendLedgerStorePort } from './store'\nimport {\n SPEND_CHECKS,\n type BilledDurationBasis,\n type CeilingBasis,\n type SettlementRow,\n type SpendBoxRecord,\n type SpendCheckId,\n type SpendFinding,\n type SpendOwnershipRule,\n type SpendOwnershipSummary,\n type SpendOwnershipVerdict,\n type SpendReport,\n} from './types'\n\n/** How many nanodollars in one US dollar. The ledger's unit. */\nconst NANO_PER_USD = 1_000_000_000\nconst MS_PER_HOUR = 3_600_000\n\nexport interface VelocityOptions {\n /** Bucket width for a spend window, ms. Default 24 h. */\n readonly windowMs?: number\n /** Fire when a window exceeds this multiple of the trailing median. Default 5. */\n readonly multiple?: number\n /**\n * Windows of history required before a median means anything. Default 3.\n * Below this the rule stays silent, so a product's genuine first days of\n * usage are not reported as an anomaly.\n */\n readonly minTrailingWindows?: number\n /**\n * A window under this never fires, whatever the ratio. Default $1.00.\n *\n * Without a floor the rule is useless: a trailing median of a tenth of a cent\n * makes every ordinary day a 5x outlier. $1.00 is set from the incident's own\n * distribution — the smallest of the eight affected wallets took $1.98, and\n * the two rows in the same window that were GENUINE were sub-cent. So the\n * floor sits above the noise and below every real finding.\n */\n readonly minAbsoluteNanoUsd?: number\n}\n\nconst DEFAULT_VELOCITY: Required<VelocityOptions> = {\n windowMs: 86_400_000,\n multiple: 5,\n minTrailingWindows: 3,\n minAbsoluteNanoUsd: NANO_PER_USD,\n}\n\n/** The balance the product observes, and the floor it must not cross. */\nexport interface ObservedBalance {\n /** Signed nanodollars, as the platform reports it. */\n readonly nanoUsd: number\n /** Below this is a finding. Default 0. */\n readonly floorNanoUsd?: number\n}\n\n/**\n * A box's price, nanodollars per hour, used to derive an EXACT billed duration\n * from a charge. Return null when the product does not know the box's rate; the\n * reconciler then falls back to the reference span.\n */\nexport type BoxRateResolver = (record: SpendBoxRecord | null, sandboxId: string) => number | null | undefined\n\nexport interface ReconcileSpendOptions {\n /**\n * Settled ledger rows, supplied by the product's own authenticated fetch.\n *\n * The fetch can only scope to a WALLET — `product: 'sandbox'` is the\n * platform's service taxonomy, not this product's — so on an account running\n * more than one of our products these rows carry the siblings' boxes too.\n * {@link ReconcileSpendOptions.ownership} is what separates them.\n */\n readonly rows: readonly SettlementRow[]\n /** The product's expectation ledger. */\n readonly store: SpendLedgerStorePort\n /**\n * Which of those rows are THIS product's — see {@link SpendOwnershipRule} and\n * the shipped `ownedByBillingKeys`.\n *\n * Omitting it is safe and changes nothing: the pass claims every box, which is\n * the behaviour that shipped, and the direction that over-reports rather than\n * under-reports. It is not silent about it — `report.ownership.declared` is\n * `false`, `formatSpendReport` says so above the findings, and every\n * `unknown-box` finding states on its face that a sibling product's box is\n * indistinguishable from a charge that is not ours.\n *\n * Declaring it never weakens the ledger-backed checks: ownership is consulted\n * ONLY for boxes with no expectation record, so `over-ceiling` on a recorded\n * box fires whatever the rule says.\n */\n readonly ownership?: SpendOwnershipRule\n /** Treated as \"now\". Default `Date.now()`. */\n readonly asOf?: number\n /** Ceiling slack. Default {@link DEFAULT_CEILING_TOLERANCE_MS}. */\n readonly toleranceMs?: number\n /** Box price, for the exact duration basis. A number applies to every box. */\n readonly nanoUsdPerHour?: number | BoxRateResolver\n /** Velocity tuning, or `false` to skip the rule. */\n readonly velocity?: VelocityOptions | false\n /** The workspace balance, when the product can see one. Omitted skips the rule. */\n readonly balance?: ObservedBalance\n /** Stamped onto findings so an alert names the tenant. */\n readonly workspaceId?: string\n /** Checks to leave out of this pass. */\n readonly skip?: readonly SpendCheckId[]\n}\n\nfunction usd(nano: number): string {\n return `$${(nano / NANO_PER_USD).toFixed(2)}`\n}\n\nfunction hours(ms: number): string {\n return `${(ms / MS_PER_HOUR).toFixed(1)}h`\n}\n\n/** Trust order, least trustworthy first — a mixed aggregate reports the weakest. */\nconst BASIS_TRUST: readonly BilledDurationBasis[] = ['unknown', 'reference-span', 'rate', 'reported']\n\nfunction weakestBasis(a: BilledDurationBasis, b: BilledDurationBasis): BilledDurationBasis {\n return BASIS_TRUST.indexOf(a) <= BASIS_TRUST.indexOf(b) ? a : b\n}\n\n/**\n * How long one settlement billed for, and how confidently we know it.\n *\n * The ledger row does not store a duration — this is the whole reason the check\n * is subtle. Three ways to recover it, best first:\n *\n * 1. `reported` — the product's ledger view exposed it. Exact.\n * 2. `rate` — `charge / pricePerHour`. Exact, because the platform computes the\n * charge as `(durationMs / 3_600_000) * costPerHour` and nothing else enters\n * it. Requires the product to know its box's price.\n * 3. `reference-span` — `settledAt - intervalStart`, both read off the row. An\n * UPPER bound, not the duration: a correct settlement delayed by the\n * platform's durable settlement queue reads longer here than it billed. It is\n * exact for the failure this module exists to catch, because billing \"up to\n * now\" makes the settlement instant and the interval end the same moment.\n */\nfunction resolveBilledMs(\n row: SettlementRow,\n ratePerHourNano: number | null,\n): { ms: number; basis: BilledDurationBasis } {\n if (row.billedMs !== null) return { ms: row.billedMs, basis: 'reported' }\n\n const charge = chargeNanoUsd(row)\n if (ratePerHourNano !== null && ratePerHourNano > 0 && charge > 0) {\n return { ms: (charge / ratePerHourNano) * MS_PER_HOUR, basis: 'rate' }\n }\n\n const reference = parseSettlementReference(row.referenceId)\n if (reference?.intervalStartMs != null && row.createdAt > reference.intervalStartMs) {\n return { ms: row.createdAt - reference.intervalStartMs, basis: 'reference-span' }\n }\n\n return { ms: 0, basis: 'unknown' }\n}\n\nfunction median(values: readonly number[]): number {\n if (values.length === 0) return 0\n const sorted = [...values].sort((a, b) => a - b)\n const mid = Math.floor(sorted.length / 2)\n if (sorted.length % 2 === 1) return sorted[mid] as number\n return (((sorted[mid - 1] as number) + (sorted[mid] as number)) / 2)\n}\n\n/**\n * What an `unknown-box` finding can honestly claim about WHOSE box it is.\n *\n * Three different statements, because the reader's next action differs: chase a\n * charge inside our own billing identity, chase a charge nothing attributes,\n * or first go and declare an ownership rule so the question can be answered at\n * all. A single message covering all three would be the vaguest of the three.\n */\nfunction unknownBoxAttribution(\n ownership: SpendOwnershipRule | null,\n verdict: SpendOwnershipVerdict,\n): string {\n if (!ownership) {\n return (\n ' No ownership rule was declared for this pass, so a sibling product\\'s box on the same ' +\n 'wallet reads exactly like a charge that is not ours — this finding could be either.'\n )\n }\n if (verdict === 'undecidable') {\n return (\n ` The ownership rule (${ownership.label}) could not decide it: the settlement carries no ` +\n 'billing-key attribution to exclude it by, so it is reported rather than dropped.'\n )\n }\n return ` The ownership rule (${ownership.label}) attributes it to THIS product.`\n}\n\nfunction unknownBoxRemedy(\n ownership: SpendOwnershipRule | null,\n verdict: SpendOwnershipVerdict,\n sandboxId: string,\n): string {\n const lookup = `Look up ${sandboxId} on the platform before disputing.`\n if (!ownership) {\n return (\n 'Declare `ownership` (see `ownedByBillingKeys`) so a sibling product\\'s box stops reading as ' +\n 'a discrepancy — without it this check cannot tell one from a charge that is not ours. ' +\n 'Until then, treat this as one of three things: a sibling product on the same wallet, a box ' +\n `provisioned outside the recorded seam, or a box that is not ours at all. ${lookup}`\n )\n }\n if (verdict === 'undecidable') {\n return (\n 'An unattributable charge on a shared wallet is exactly what a phantom charge looks like, so ' +\n 'it is reported by design rather than excluded. Confirm the row genuinely predates key ' +\n `attribution before dismissing it. ${lookup}`\n )\n }\n return (\n 'This box is inside this product\\'s own billing attribution and the product never recorded it, ' +\n 'so it is either a provision that bypassed the recorded seam or a charge that is not ours. ' +\n `${lookup}`\n )\n}\n\nfunction emptyFinding(check: SpendCheckId): Omit<SpendFinding, 'message' | 'remedy'> {\n return {\n check,\n sandboxId: null,\n workspaceId: null,\n referenceIds: [],\n settledNanoUsd: 0,\n settledMs: null,\n durationBasis: null,\n ceilingMs: null,\n overageMs: null,\n ceilingBasis: null,\n windowNanoUsd: null,\n trailingMedianNanoUsd: null,\n velocityRatio: null,\n windowStartAt: null,\n balanceNanoUsd: null,\n balanceFloorNanoUsd: null,\n }\n}\n\n/**\n * Diff what the platform charged against what the product believes it asked for.\n *\n * Never disputes anything and never writes: the output is a report a human acts\n * on. The platform's ledger stays authoritative — this only ever produces the\n * evidence for a conversation with it.\n */\nexport async function reconcileSpend(options: ReconcileSpendOptions): Promise<SpendReport> {\n const asOf = options.asOf ?? Date.now()\n const toleranceMs = options.toleranceMs ?? DEFAULT_CEILING_TOLERANCE_MS\n const workspaceId = options.workspaceId ?? null\n const skip = new Set(options.skip ?? [])\n const checksRun = SPEND_CHECKS.filter((check) => !skip.has(check))\n const runs = (check: SpendCheckId): boolean => !skip.has(check)\n\n const ownership = options.ownership ?? null\n\n const findings: SpendFinding[] = []\n let settledNanoUsd = 0\n let creditedNanoUsd = 0\n let ownedBoxes = 0\n let ownedNanoUsd = 0\n let undecidableBoxes = 0\n let foreignNanoUsd = 0\n const foreignSandboxIds: string[] = []\n\n // ── group charges by the box they are attributable to ──────────────────────\n const byBox = new Map<string, SettlementRow[]>()\n for (const row of options.rows) {\n if (row.amountNanoUsd < 0) settledNanoUsd += -row.amountNanoUsd\n else creditedNanoUsd += row.amountNanoUsd\n if (row.amountNanoUsd >= 0) continue\n const sandboxId = settlementSandboxId(row)\n if (!sandboxId) continue\n const bucket = byBox.get(sandboxId)\n if (bucket) bucket.push(row)\n else byBox.set(sandboxId, [row])\n }\n\n const rateOf = (record: SpendBoxRecord | null, sandboxId: string): number | null => {\n const rate = options.nanoUsdPerHour\n if (rate === undefined) return null\n if (typeof rate === 'number') return rate\n return rate(record, sandboxId) ?? null\n }\n\n for (const [sandboxId, rows] of byBox) {\n const record = await options.store.load(sandboxId)\n const referenceIds = rows.map((row) => row.referenceId ?? row.id)\n const charged = rows.reduce((sum, row) => sum + chargeNanoUsd(row), 0)\n\n // A box the product RECORDED is this product's by construction, and no\n // ownership rule may un-own it. That is what stops a wrong or over-narrow\n // rule from hiding an over-ceiling finding: the ledger decides recorded-ness,\n // the rule decides attribution, and the rule only ever gets a say about the\n // residue neither of them has claimed.\n const verdict: SpendOwnershipVerdict = record\n ? 'mine'\n : ownership\n ? decideBoxOwnership(ownership, sandboxId, rows)\n : 'mine'\n\n if (verdict === 'foreign') {\n foreignSandboxIds.push(sandboxId)\n foreignNanoUsd += charged\n continue\n }\n ownedBoxes += 1\n ownedNanoUsd += charged\n if (verdict === 'undecidable') undecidableBoxes += 1\n\n // ── unknown-box ──────────────────────────────────────────────────────────\n if (!record) {\n if (runs('unknown-box')) {\n findings.push({\n ...emptyFinding('unknown-box'),\n sandboxId,\n workspaceId,\n referenceIds,\n settledNanoUsd: charged,\n message:\n `${usd(charged)} settled across ${rows.length} row(s) against sandbox ${sandboxId}, ` +\n 'which this product has no record of ever asking for.' +\n unknownBoxAttribution(ownership, verdict),\n remedy: unknownBoxRemedy(ownership, verdict, sandboxId),\n })\n }\n continue\n }\n\n // ── over-ceiling ─────────────────────────────────────────────────────────\n if (!runs('over-ceiling')) continue\n\n const ceiling = computeExpectedCeiling(record, { asOf, toleranceMs })\n const ratePerHourNano = rateOf(record, sandboxId)\n\n let settledMs = 0\n let basis: BilledDurationBasis = 'reported'\n let anyMeasured = false\n for (const row of rows) {\n const resolved = resolveBilledMs(row, ratePerHourNano)\n if (resolved.basis === 'unknown') {\n basis = weakestBasis(basis, 'unknown')\n continue\n }\n anyMeasured = true\n settledMs += resolved.ms\n basis = weakestBasis(basis, resolved.basis)\n }\n if (!anyMeasured) continue\n if (settledMs <= ceiling.ceilingMs) continue\n\n const overageMs = settledMs - ceiling.ceilingMs\n const confidence = ceiling.bounded\n ? ''\n : ' The product could not bound this box from its own observations (an unfinished detached ' +\n 'run, and no max lifetime), so the ceiling rests on the reconciliation instant: this ' +\n 'settlement bills time outside the box\\'s own life, not merely more than expected.'\n const spanCaveat =\n basis === 'reference-span'\n ? ' Duration is derived from the settlement instant minus the interval start, which ' +\n 'overstates a settlement the platform merely posted late — confirm before disputing.'\n : ''\n\n findings.push({\n ...emptyFinding('over-ceiling'),\n sandboxId,\n workspaceId: record.workspaceId || workspaceId,\n referenceIds,\n settledNanoUsd: charged,\n settledMs,\n durationBasis: basis,\n ceilingMs: ceiling.ceilingMs,\n overageMs,\n ceilingBasis: ceiling.basis,\n message:\n `Sandbox ${sandboxId} settled ${hours(settledMs)} (${usd(charged)}) across ${rows.length} ` +\n `row(s), against an expected ceiling of ${hours(ceiling.ceilingMs)} — over by ` +\n `${hours(overageMs)}. Ceiling basis: ${ceiling.basis}; duration basis: ${basis}.` +\n confidence +\n spanCaveat,\n remedy:\n `Dispute ${referenceIds.join(', ')} against the platform ledger with both numbers. ` +\n (ceiling.basis === 'stopped' || ceiling.basis === 'deleted'\n ? 'The product recorded this box as no longer running before the billed time ended, so ' +\n 'either the stop did not take or the interval was settled at the wrong boundary.'\n : 'Freeze the open interval before anything deletes this box — deleting a box with an ' +\n 'open compute interval settles the whole gap at once.'),\n })\n }\n\n // ── velocity ───────────────────────────────────────────────────────────────\n if (runs('velocity') && options.velocity !== false) {\n const cfg = { ...DEFAULT_VELOCITY, ...(options.velocity ?? {}) }\n const buckets = new Map<number, { nano: number; references: string[] }>()\n for (const row of options.rows) {\n const charge = chargeNanoUsd(row)\n if (charge === 0) continue\n // Velocity is decided per ROW, not per box, because a row naming no\n // sandbox at all still counts toward what this product spent. A sibling's\n // burst is excluded rather than paged on: a product cannot dispute a\n // charge it did not incur, and leaving them in makes one product's\n // incident wake every product on the wallet.\n if (ownership && ownership.decide({ row, sandboxId: settlementSandboxId(row) }) === 'foreign') {\n continue\n }\n const bucketStart = Math.floor(row.createdAt / cfg.windowMs) * cfg.windowMs\n const bucket = buckets.get(bucketStart)\n if (bucket) {\n bucket.nano += charge\n bucket.references.push(row.referenceId ?? row.id)\n } else {\n buckets.set(bucketStart, { nano: charge, references: [row.referenceId ?? row.id] })\n }\n }\n\n const ordered = [...buckets.entries()].sort((a, b) => a[0] - b[0])\n for (let i = 0; i < ordered.length; i++) {\n const entry = ordered[i]\n if (!entry) continue\n const [windowStartAt, bucket] = entry\n if (i < cfg.minTrailingWindows) continue\n const trailing = ordered.slice(0, i).map(([, prior]) => prior.nano)\n const trailingMedian = median(trailing)\n const threshold = Math.max(trailingMedian * cfg.multiple, cfg.minAbsoluteNanoUsd)\n if (bucket.nano <= threshold) continue\n const ratio = trailingMedian > 0 ? bucket.nano / trailingMedian : Number.POSITIVE_INFINITY\n\n findings.push({\n ...emptyFinding('velocity'),\n workspaceId,\n referenceIds: bucket.references,\n settledNanoUsd: bucket.nano,\n windowNanoUsd: bucket.nano,\n trailingMedianNanoUsd: trailingMedian,\n velocityRatio: ratio,\n windowStartAt,\n message:\n `${usd(bucket.nano)} settled in the window starting ${new Date(windowStartAt).toISOString()} ` +\n `across ${bucket.references.length} row(s), against a trailing median of ` +\n `${usd(trailingMedian)} over ${trailing.length} prior window(s) — ` +\n `${Number.isFinite(ratio) ? `${ratio.toFixed(1)}x` : 'no prior spend to compare against'}, ` +\n `over the ${cfg.multiple}x threshold.` +\n (ownership\n ? ` Counted over this product's own rows only (${ownership.label}); the wallet total ` +\n 'for the window is higher when a sibling product settled into it.'\n : ' Counted over every row on the wallet, which on a shared account includes any ' +\n 'sibling product\\'s spend.'),\n remedy:\n 'A burst of this shape is what a settlement defect looks like from the consumer side: ' +\n 'long-dormant intervals cashed out at once. Check whether these rows carry interval ' +\n 'starts far older than the settlement instant before treating it as real usage.',\n })\n }\n }\n\n // ── negative-balance ───────────────────────────────────────────────────────\n if (runs('negative-balance') && options.balance) {\n const floor = options.balance.floorNanoUsd ?? 0\n if (options.balance.nanoUsd < floor) {\n findings.push({\n ...emptyFinding('negative-balance'),\n workspaceId,\n settledNanoUsd: Math.max(0, floor - options.balance.nanoUsd),\n balanceNanoUsd: options.balance.nanoUsd,\n balanceFloorNanoUsd: floor,\n message:\n `Observed balance ${usd(options.balance.nanoUsd)} is below the floor ${usd(floor)}.`,\n remedy:\n 'Stop provisioning new compute for this owner until the balance is explained. A negative ' +\n 'balance that nobody is watching is how a billing defect becomes settled money.',\n })\n }\n }\n\n const ownershipSummary: SpendOwnershipSummary = {\n declared: ownership !== null,\n label: ownership?.label ?? null,\n ownedBoxes,\n ownedNanoUsd,\n undecidableBoxes,\n foreignBoxes: foreignSandboxIds.length,\n foreignNanoUsd,\n foreignSandboxIds,\n }\n\n return {\n ok: findings.length === 0,\n findings,\n checksRun,\n rowsExamined: options.rows.length,\n boxesExamined: byBox.size,\n settledNanoUsd,\n creditedNanoUsd,\n ownership: ownershipSummary,\n asOf,\n }\n}\n","import type { SpendFinding, SpendOwnershipSummary, SpendReport } from './types'\n\nconst NANO_PER_USD = 1_000_000_000\n\nfunction usd(nano: number | null): string {\n return nano === null ? '—' : `$${(nano / NANO_PER_USD).toFixed(2)}`\n}\n\n/**\n * Render a reconciliation for a human deciding whether to open a dispute.\n *\n * Every finding prints its numbers, not a summary of them: the reader's next\n * action is a conversation with the platform about specific reference ids, and a\n * report that made them re-derive the durations would just be re-read alongside\n * the raw rows anyway.\n */\nexport function formatSpendReport(report: SpendReport): string {\n const lines: string[] = []\n lines.push(\n `spend reconciliation — ${report.rowsExamined} row(s), ${report.boxesExamined} box(es), ` +\n `${usd(report.settledNanoUsd)} charged, ${usd(report.creditedNanoUsd)} credited, ` +\n `as of ${new Date(report.asOf).toISOString()}`,\n )\n lines.push(`checks: ${report.checksRun.join(', ') || '(none)'}`)\n for (const line of ownershipLines(report.ownership)) lines.push(line)\n\n if (report.ok) {\n lines.push('')\n lines.push('OK — no discrepancy between the product\\'s expectations and the settled ledger.')\n return lines.join('\\n')\n }\n\n lines.push('')\n lines.push(`${report.findings.length} finding(s):`)\n for (const finding of report.findings) {\n lines.push('')\n lines.push(` [${finding.check}] ${finding.message}`)\n for (const [label, value] of measuredFields(finding)) lines.push(` ${label}: ${value}`)\n if (finding.referenceIds.length > 0) {\n lines.push(` rows: ${finding.referenceIds.join(', ')}`)\n }\n lines.push(` → ${finding.remedy}`)\n }\n return lines.join('\\n')\n}\n\n/**\n * What the pass claimed and what it set aside — printed on EVERY report,\n * clean ones included.\n *\n * A clean report that does not say what it looked at is the failure this\n * closes: an over-narrow ownership rule and a genuinely quiet account produce\n * the same \"OK\" line, and only the excluded numbers tell them apart. The\n * excluded box ids are printed in full for the same reason — an exclusion a\n * reader cannot audit is one they have to take on trust.\n */\nfunction ownershipLines(ownership: SpendOwnershipSummary): string[] {\n if (!ownership.declared) {\n return [\n `scope: NOT DECLARED — all ${ownership.ownedBoxes} settled box(es) claimed as this ` +\n 'product\\'s. A sibling product\\'s box on this wallet is reported as unknown-box; pass ' +\n '`ownership` (see `ownedByBillingKeys`) to tell the two apart.',\n ]\n }\n const lines = [\n `scope: ${ownership.label} — ${ownership.ownedBoxes} box(es) ${usd(ownership.ownedNanoUsd)} ` +\n `owned, ${ownership.foreignBoxes} box(es) ${usd(ownership.foreignNanoUsd)} excluded as ` +\n 'another product\\'s',\n ]\n if (ownership.undecidableBoxes > 0) {\n lines.push(\n ` ${ownership.undecidableBoxes} of the owned box(es) carried no billing-key ` +\n 'attribution and were claimed fail-closed',\n )\n }\n if (ownership.foreignSandboxIds.length > 0) {\n lines.push(` excluded: ${ownership.foreignSandboxIds.join(', ')}`)\n }\n return lines\n}\n\n/** Every measured field a finding carries, including the ones it left null. */\nfunction measuredFields(finding: SpendFinding): Array<[string, string]> {\n const ms = (value: number | null): string =>\n value === null ? '—' : `${(value / 3_600_000).toFixed(2)}h`\n return [\n ['sandbox', finding.sandboxId ?? '—'],\n ['workspace', finding.workspaceId ?? '—'],\n ['amount', usd(finding.settledNanoUsd)],\n ['settled', ms(finding.settledMs)],\n ['ceiling', ms(finding.ceilingMs)],\n ['overage', ms(finding.overageMs)],\n ['ceiling basis', finding.ceilingBasis ?? '—'],\n ['duration basis', finding.durationBasis ?? '—'],\n ['window', finding.windowStartAt === null ? '—' : new Date(finding.windowStartAt).toISOString()],\n ['window spend', usd(finding.windowNanoUsd)],\n ['trailing median', usd(finding.trailingMedianNanoUsd)],\n ['ratio', finding.velocityRatio === null ? '—' : `${finding.velocityRatio.toFixed(1)}x`],\n ['balance', usd(finding.balanceNanoUsd)],\n ['balance floor', usd(finding.balanceFloorNanoUsd)],\n ]\n}\n\n/** The report as a plain JSON value, for an alerting pipeline. */\nexport function spendReportToJson(report: SpendReport): string {\n return JSON.stringify(report, null, 2)\n}\n"],"mappings":";AAsTO,IAAM,eAAwC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;AC3OO,SAAS,mBAAmB,QAA+C;AAChF,QAAM,QAAQ,IAAI,IAAI,OAAO,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,EAAE,OAAO,OAAO,CAAC;AACnE,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,QAAM,QAAQ,eAAe,CAAC,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC;AAClD,SAAO;AAAA,IACL;AAAA,IACA,OAAO,EAAE,IAAI,GAAG;AACd,YAAM,QAAQ,IAAI,OAAO,KAAK;AAC9B,UAAI,CAAC,MAAO,QAAO;AACnB,aAAO,MAAM,IAAI,KAAK,IAAI,SAAS;AAAA,IACrC;AAAA,EACF;AACF;AAaO,SAAS,mBACd,MACA,WACA,MACuB;AACvB,MAAI,UAAiC;AACrC,aAAW,OAAO,MAAM;AACtB,UAAM,aAAa,KAAK,OAAO,EAAE,KAAK,UAAU,CAAC;AACjD,QAAI,eAAe,OAAQ,QAAO;AAClC,QAAI,eAAe,cAAe,WAAU;AAAA,EAC9C;AACA,SAAO;AACT;;;ACxGO,IAAM,+BAA+B;AAmCrC,SAAS,uBACd,QACA,SACiB;AACjB,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,EAAE,KAAK,IAAI;AAEjB,MAAI;AACJ,MAAI;AAEJ,MAAI,OAAO,cAAc,MAAM;AAC7B,YAAQ;AACR,gBAAY,OAAO;AAAA,EACrB,WAAW,OAAO,cAAc,MAAM;AACpC,YAAQ;AACR,gBAAY,OAAO;AAAA,EACrB,WAAW,OAAO,mBAAmB,SAAS,GAAG;AAG/C,YAAQ;AACR,gBAAY;AAAA,EACd,OAAO;AACL,YAAQ;AACR,gBAAY,OAAO,iBAAiB,OAAO,qBAAqB;AAAA,EAClE;AAKA,MAAI,OAAO,uBAAuB,MAAM;AACtC,UAAM,kBAAkB,OAAO,YAAY,OAAO,qBAAqB;AACvE,QAAI,kBAAkB,WAAW;AAC/B,cAAQ;AACR,kBAAY;AAAA,IACd;AAAA,EACF;AAKA,MAAI,YAAY,KAAM,aAAY;AAGlC,MAAI,YAAY,OAAO,UAAW,aAAY,OAAO;AAErD,SAAO;AAAA,IACL,WAAW,OAAO;AAAA,IAClB;AAAA,IACA;AAAA,IACA,WAAW,YAAY,OAAO,YAAY;AAAA,IAC1C;AAAA,IACA,SAAS,UAAU;AAAA,EACrB;AACF;;;ACvFO,SAAS,yBAAyB,aAAoE;AAC3G,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,QAAQ,YAAY,MAAM,GAAG;AACnC,MAAI,MAAM,SAAS,KAAK,MAAM,CAAC,MAAM,UAAW,QAAO;AACvD,QAAM,OAAO,MAAM,CAAC;AACpB,MAAI,CAAC,KAAM,QAAO;AAKlB,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,QAAM,SAAS,QAAQ,KAAK,IAAI,IAAI,OAAO,IAAI,IAAI,OAAO;AAC1D,QAAM,YAAY,MAAM,UAAU,KAAK,OAAO,cAAc,MAAM,KAAK,SAAS;AAEhF,QAAM,aAAa,YAAY,MAAM,MAAM,GAAG,EAAE,EAAE,KAAK,GAAG,IAAI,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG;AACrF,MAAI,CAAC,WAAY,QAAO;AAExB,SAAO,EAAE,MAAM,YAAY,iBAAiB,YAAY,SAAS,KAAK;AACxE;AAUO,SAAS,qBAAqB,UAAoD;AACvF,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,QAAQ,SAAS,MAAM,GAAG;AAChC,MAAI,MAAM,SAAS,KAAK,MAAM,CAAC,MAAM,UAAW,QAAO;AACvD,QAAM,KAAK,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG;AAClC,SAAO,MAAM;AACf;AAWO,SAAS,oBAAoB,KAAmC;AACrE,QAAM,YAAY,yBAAyB,IAAI,WAAW;AAC1D,MAAI,aAAa,UAAU,oBAAoB,KAAM,QAAO,UAAU;AACtE,SAAO,qBAAqB,IAAI,QAAQ,MAAM,YAAY,UAAU,aAAa;AACnF;AAGO,SAAS,SAAS,KAA6B;AACpD,SAAO,IAAI,gBAAgB;AAC7B;AAGO,SAAS,cAAc,KAA4B;AACxD,SAAO,IAAI,gBAAgB,IAAI,CAAC,IAAI,gBAAgB;AACtD;;;AC3DA,IAAM,eAAe;AACrB,IAAM,cAAc;AAyBpB,IAAM,mBAA8C;AAAA,EAClD,UAAU;AAAA,EACV,UAAU;AAAA,EACV,oBAAoB;AAAA,EACpB,oBAAoB;AACtB;AA6DA,SAAS,IAAI,MAAsB;AACjC,SAAO,KAAK,OAAO,cAAc,QAAQ,CAAC,CAAC;AAC7C;AAEA,SAAS,MAAM,IAAoB;AACjC,SAAO,IAAI,KAAK,aAAa,QAAQ,CAAC,CAAC;AACzC;AAGA,IAAM,cAA8C,CAAC,WAAW,kBAAkB,QAAQ,UAAU;AAEpG,SAAS,aAAa,GAAwB,GAA6C;AACzF,SAAO,YAAY,QAAQ,CAAC,KAAK,YAAY,QAAQ,CAAC,IAAI,IAAI;AAChE;AAkBA,SAAS,gBACP,KACA,iBAC4C;AAC5C,MAAI,IAAI,aAAa,KAAM,QAAO,EAAE,IAAI,IAAI,UAAU,OAAO,WAAW;AAExE,QAAM,SAAS,cAAc,GAAG;AAChC,MAAI,oBAAoB,QAAQ,kBAAkB,KAAK,SAAS,GAAG;AACjE,WAAO,EAAE,IAAK,SAAS,kBAAmB,aAAa,OAAO,OAAO;AAAA,EACvE;AAEA,QAAM,YAAY,yBAAyB,IAAI,WAAW;AAC1D,MAAI,WAAW,mBAAmB,QAAQ,IAAI,YAAY,UAAU,iBAAiB;AACnF,WAAO,EAAE,IAAI,IAAI,YAAY,UAAU,iBAAiB,OAAO,iBAAiB;AAAA,EAClF;AAEA,SAAO,EAAE,IAAI,GAAG,OAAO,UAAU;AACnC;AAEA,SAAS,OAAO,QAAmC;AACjD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,SAAS,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAC/C,QAAM,MAAM,KAAK,MAAM,OAAO,SAAS,CAAC;AACxC,MAAI,OAAO,SAAS,MAAM,EAAG,QAAO,OAAO,GAAG;AAC9C,UAAU,OAAO,MAAM,CAAC,IAAgB,OAAO,GAAG,KAAgB;AACpE;AAUA,SAAS,sBACP,WACA,SACQ;AACR,MAAI,CAAC,WAAW;AACd,WACE;AAAA,EAGJ;AACA,MAAI,YAAY,eAAe;AAC7B,WACE,wBAAwB,UAAU,KAAK;AAAA,EAG3C;AACA,SAAO,wBAAwB,UAAU,KAAK;AAChD;AAEA,SAAS,iBACP,WACA,SACA,WACQ;AACR,QAAM,SAAS,WAAW,SAAS;AACnC,MAAI,CAAC,WAAW;AACd,WACE,iWAG4E,MAAM;AAAA,EAEtF;AACA,MAAI,YAAY,eAAe;AAC7B,WACE,uNAEqC,MAAM;AAAA,EAE/C;AACA,SACE,0LAEG,MAAM;AAEb;AAEA,SAAS,aAAa,OAA+D;AACnF,SAAO;AAAA,IACL;AAAA,IACA,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc,CAAC;AAAA,IACf,gBAAgB;AAAA,IAChB,WAAW;AAAA,IACX,eAAe;AAAA,IACf,WAAW;AAAA,IACX,WAAW;AAAA,IACX,cAAc;AAAA,IACd,eAAe;AAAA,IACf,uBAAuB;AAAA,IACvB,eAAe;AAAA,IACf,eAAe;AAAA,IACf,gBAAgB;AAAA,IAChB,qBAAqB;AAAA,EACvB;AACF;AASA,eAAsB,eAAe,SAAsD;AACzF,QAAM,OAAO,QAAQ,QAAQ,KAAK,IAAI;AACtC,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,OAAO,IAAI,IAAI,QAAQ,QAAQ,CAAC,CAAC;AACvC,QAAM,YAAY,aAAa,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,KAAK,CAAC;AACjE,QAAM,OAAO,CAAC,UAAiC,CAAC,KAAK,IAAI,KAAK;AAE9D,QAAM,YAAY,QAAQ,aAAa;AAEvC,QAAM,WAA2B,CAAC;AAClC,MAAI,iBAAiB;AACrB,MAAI,kBAAkB;AACtB,MAAI,aAAa;AACjB,MAAI,eAAe;AACnB,MAAI,mBAAmB;AACvB,MAAI,iBAAiB;AACrB,QAAM,oBAA8B,CAAC;AAGrC,QAAM,QAAQ,oBAAI,IAA6B;AAC/C,aAAW,OAAO,QAAQ,MAAM;AAC9B,QAAI,IAAI,gBAAgB,EAAG,mBAAkB,CAAC,IAAI;AAAA,QAC7C,oBAAmB,IAAI;AAC5B,QAAI,IAAI,iBAAiB,EAAG;AAC5B,UAAM,YAAY,oBAAoB,GAAG;AACzC,QAAI,CAAC,UAAW;AAChB,UAAM,SAAS,MAAM,IAAI,SAAS;AAClC,QAAI,OAAQ,QAAO,KAAK,GAAG;AAAA,QACtB,OAAM,IAAI,WAAW,CAAC,GAAG,CAAC;AAAA,EACjC;AAEA,QAAM,SAAS,CAAC,QAA+B,cAAqC;AAClF,UAAM,OAAO,QAAQ;AACrB,QAAI,SAAS,OAAW,QAAO;AAC/B,QAAI,OAAO,SAAS,SAAU,QAAO;AACrC,WAAO,KAAK,QAAQ,SAAS,KAAK;AAAA,EACpC;AAEA,aAAW,CAAC,WAAW,IAAI,KAAK,OAAO;AACrC,UAAM,SAAS,MAAM,QAAQ,MAAM,KAAK,SAAS;AACjD,UAAM,eAAe,KAAK,IAAI,CAAC,QAAQ,IAAI,eAAe,IAAI,EAAE;AAChE,UAAM,UAAU,KAAK,OAAO,CAAC,KAAK,QAAQ,MAAM,cAAc,GAAG,GAAG,CAAC;AAOrE,UAAM,UAAiC,SACnC,SACA,YACE,mBAAmB,WAAW,WAAW,IAAI,IAC7C;AAEN,QAAI,YAAY,WAAW;AACzB,wBAAkB,KAAK,SAAS;AAChC,wBAAkB;AAClB;AAAA,IACF;AACA,kBAAc;AACd,oBAAgB;AAChB,QAAI,YAAY,cAAe,qBAAoB;AAGnD,QAAI,CAAC,QAAQ;AACX,UAAI,KAAK,aAAa,GAAG;AACvB,iBAAS,KAAK;AAAA,UACZ,GAAG,aAAa,aAAa;AAAA,UAC7B;AAAA,UACA;AAAA,UACA;AAAA,UACA,gBAAgB;AAAA,UAChB,SACE,GAAG,IAAI,OAAO,CAAC,mBAAmB,KAAK,MAAM,2BAA2B,SAAS,2DAEjF,sBAAsB,WAAW,OAAO;AAAA,UAC1C,QAAQ,iBAAiB,WAAW,SAAS,SAAS;AAAA,QACxD,CAAC;AAAA,MACH;AACA;AAAA,IACF;AAGA,QAAI,CAAC,KAAK,cAAc,EAAG;AAE3B,UAAM,UAAU,uBAAuB,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpE,UAAM,kBAAkB,OAAO,QAAQ,SAAS;AAEhD,QAAI,YAAY;AAChB,QAAI,QAA6B;AACjC,QAAI,cAAc;AAClB,eAAW,OAAO,MAAM;AACtB,YAAM,WAAW,gBAAgB,KAAK,eAAe;AACrD,UAAI,SAAS,UAAU,WAAW;AAChC,gBAAQ,aAAa,OAAO,SAAS;AACrC;AAAA,MACF;AACA,oBAAc;AACd,mBAAa,SAAS;AACtB,cAAQ,aAAa,OAAO,SAAS,KAAK;AAAA,IAC5C;AACA,QAAI,CAAC,YAAa;AAClB,QAAI,aAAa,QAAQ,UAAW;AAEpC,UAAM,YAAY,YAAY,QAAQ;AACtC,UAAM,aAAa,QAAQ,UACvB,KACA;AAGJ,UAAM,aACJ,UAAU,mBACN,8KAEA;AAEN,aAAS,KAAK;AAAA,MACZ,GAAG,aAAa,cAAc;AAAA,MAC9B;AAAA,MACA,aAAa,OAAO,eAAe;AAAA,MACnC;AAAA,MACA,gBAAgB;AAAA,MAChB;AAAA,MACA,eAAe;AAAA,MACf,WAAW,QAAQ;AAAA,MACnB;AAAA,MACA,cAAc,QAAQ;AAAA,MACtB,SACE,WAAW,SAAS,YAAY,MAAM,SAAS,CAAC,KAAK,IAAI,OAAO,CAAC,YAAY,KAAK,MAAM,2CAC9C,MAAM,QAAQ,SAAS,CAAC,mBAC/D,MAAM,SAAS,CAAC,oBAAoB,QAAQ,KAAK,qBAAqB,KAAK,MAC9E,aACA;AAAA,MACF,QACE,WAAW,aAAa,KAAK,IAAI,CAAC,sDACjC,QAAQ,UAAU,aAAa,QAAQ,UAAU,YAC9C,wKAEA;AAAA,IAER,CAAC;AAAA,EACH;AAGA,MAAI,KAAK,UAAU,KAAK,QAAQ,aAAa,OAAO;AAClD,UAAM,MAAM,EAAE,GAAG,kBAAkB,GAAI,QAAQ,YAAY,CAAC,EAAG;AAC/D,UAAM,UAAU,oBAAI,IAAoD;AACxE,eAAW,OAAO,QAAQ,MAAM;AAC9B,YAAM,SAAS,cAAc,GAAG;AAChC,UAAI,WAAW,EAAG;AAMlB,UAAI,aAAa,UAAU,OAAO,EAAE,KAAK,WAAW,oBAAoB,GAAG,EAAE,CAAC,MAAM,WAAW;AAC7F;AAAA,MACF;AACA,YAAM,cAAc,KAAK,MAAM,IAAI,YAAY,IAAI,QAAQ,IAAI,IAAI;AACnE,YAAM,SAAS,QAAQ,IAAI,WAAW;AACtC,UAAI,QAAQ;AACV,eAAO,QAAQ;AACf,eAAO,WAAW,KAAK,IAAI,eAAe,IAAI,EAAE;AAAA,MAClD,OAAO;AACL,gBAAQ,IAAI,aAAa,EAAE,MAAM,QAAQ,YAAY,CAAC,IAAI,eAAe,IAAI,EAAE,EAAE,CAAC;AAAA,MACpF;AAAA,IACF;AAEA,UAAM,UAAU,CAAC,GAAG,QAAQ,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AACjE,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,QAAQ,QAAQ,CAAC;AACvB,UAAI,CAAC,MAAO;AACZ,YAAM,CAAC,eAAe,MAAM,IAAI;AAChC,UAAI,IAAI,IAAI,mBAAoB;AAChC,YAAM,WAAW,QAAQ,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,KAAK,MAAM,MAAM,IAAI;AAClE,YAAM,iBAAiB,OAAO,QAAQ;AACtC,YAAM,YAAY,KAAK,IAAI,iBAAiB,IAAI,UAAU,IAAI,kBAAkB;AAChF,UAAI,OAAO,QAAQ,UAAW;AAC9B,YAAM,QAAQ,iBAAiB,IAAI,OAAO,OAAO,iBAAiB,OAAO;AAEzE,eAAS,KAAK;AAAA,QACZ,GAAG,aAAa,UAAU;AAAA,QAC1B;AAAA,QACA,cAAc,OAAO;AAAA,QACrB,gBAAgB,OAAO;AAAA,QACvB,eAAe,OAAO;AAAA,QACtB,uBAAuB;AAAA,QACvB,eAAe;AAAA,QACf;AAAA,QACA,SACE,GAAG,IAAI,OAAO,IAAI,CAAC,mCAAmC,IAAI,KAAK,aAAa,EAAE,YAAY,CAAC,WACjF,OAAO,WAAW,MAAM,yCAC/B,IAAI,cAAc,CAAC,SAAS,SAAS,MAAM,2BAC3C,OAAO,SAAS,KAAK,IAAI,GAAG,MAAM,QAAQ,CAAC,CAAC,MAAM,mCAAmC,cAC5E,IAAI,QAAQ,kBACvB,YACG,+CAA+C,UAAU,KAAK,yFAE9D;AAAA,QAEN,QACE;AAAA,MAGJ,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,KAAK,kBAAkB,KAAK,QAAQ,SAAS;AAC/C,UAAM,QAAQ,QAAQ,QAAQ,gBAAgB;AAC9C,QAAI,QAAQ,QAAQ,UAAU,OAAO;AACnC,eAAS,KAAK;AAAA,QACZ,GAAG,aAAa,kBAAkB;AAAA,QAClC;AAAA,QACA,gBAAgB,KAAK,IAAI,GAAG,QAAQ,QAAQ,QAAQ,OAAO;AAAA,QAC3D,gBAAgB,QAAQ,QAAQ;AAAA,QAChC,qBAAqB;AAAA,QACrB,SACE,oBAAoB,IAAI,QAAQ,QAAQ,OAAO,CAAC,uBAAuB,IAAI,KAAK,CAAC;AAAA,QACnF,QACE;AAAA,MAEJ,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,mBAA0C;AAAA,IAC9C,UAAU,cAAc;AAAA,IACxB,OAAO,WAAW,SAAS;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,kBAAkB;AAAA,IAChC;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,IAAI,SAAS,WAAW;AAAA,IACxB;AAAA,IACA;AAAA,IACA,cAAc,QAAQ,KAAK;AAAA,IAC3B,eAAe,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,EACF;AACF;;;ACpfA,IAAMA,gBAAe;AAErB,SAASC,KAAI,MAA6B;AACxC,SAAO,SAAS,OAAO,WAAM,KAAK,OAAOD,eAAc,QAAQ,CAAC,CAAC;AACnE;AAUO,SAAS,kBAAkB,QAA6B;AAC7D,QAAM,QAAkB,CAAC;AACzB,QAAM;AAAA,IACJ,+BAA0B,OAAO,YAAY,YAAY,OAAO,aAAa,aACxEC,KAAI,OAAO,cAAc,CAAC,aAAaA,KAAI,OAAO,eAAe,CAAC,oBAC5D,IAAI,KAAK,OAAO,IAAI,EAAE,YAAY,CAAC;AAAA,EAChD;AACA,QAAM,KAAK,WAAW,OAAO,UAAU,KAAK,IAAI,KAAK,QAAQ,EAAE;AAC/D,aAAW,QAAQ,eAAe,OAAO,SAAS,EAAG,OAAM,KAAK,IAAI;AAEpE,MAAI,OAAO,IAAI;AACb,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,qFAAiF;AAC5F,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AAEA,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,GAAG,OAAO,SAAS,MAAM,cAAc;AAClD,aAAW,WAAW,OAAO,UAAU;AACrC,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,MAAM,QAAQ,KAAK,KAAK,QAAQ,OAAO,EAAE;AACpD,eAAW,CAAC,OAAO,KAAK,KAAK,eAAe,OAAO,EAAG,OAAM,KAAK,OAAO,KAAK,KAAK,KAAK,EAAE;AACzF,QAAI,QAAQ,aAAa,SAAS,GAAG;AACnC,YAAM,KAAK,aAAa,QAAQ,aAAa,KAAK,IAAI,CAAC,EAAE;AAAA,IAC3D;AACA,UAAM,KAAK,cAAS,QAAQ,MAAM,EAAE;AAAA,EACtC;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAYA,SAAS,eAAe,WAA4C;AAClE,MAAI,CAAC,UAAU,UAAU;AACvB,WAAO;AAAA,MACL,kCAA6B,UAAU,UAAU;AAAA,IAGnD;AAAA,EACF;AACA,QAAM,QAAQ;AAAA,IACZ,UAAU,UAAU,KAAK,WAAM,UAAU,UAAU,YAAYA,KAAI,UAAU,YAAY,CAAC,WAC9E,UAAU,YAAY,YAAYA,KAAI,UAAU,cAAc,CAAC;AAAA,EAE7E;AACA,MAAI,UAAU,mBAAmB,GAAG;AAClC,UAAM;AAAA,MACJ,UAAU,UAAU,gBAAgB;AAAA,IAEtC;AAAA,EACF;AACA,MAAI,UAAU,kBAAkB,SAAS,GAAG;AAC1C,UAAM,KAAK,oBAAoB,UAAU,kBAAkB,KAAK,IAAI,CAAC,EAAE;AAAA,EACzE;AACA,SAAO;AACT;AAGA,SAAS,eAAe,SAAgD;AACtE,QAAM,KAAK,CAAC,UACV,UAAU,OAAO,WAAM,IAAI,QAAQ,MAAW,QAAQ,CAAC,CAAC;AAC1D,SAAO;AAAA,IACL,CAAC,WAAW,QAAQ,aAAa,QAAG;AAAA,IACpC,CAAC,aAAa,QAAQ,eAAe,QAAG;AAAA,IACxC,CAAC,UAAUA,KAAI,QAAQ,cAAc,CAAC;AAAA,IACtC,CAAC,WAAW,GAAG,QAAQ,SAAS,CAAC;AAAA,IACjC,CAAC,WAAW,GAAG,QAAQ,SAAS,CAAC;AAAA,IACjC,CAAC,WAAW,GAAG,QAAQ,SAAS,CAAC;AAAA,IACjC,CAAC,iBAAiB,QAAQ,gBAAgB,QAAG;AAAA,IAC7C,CAAC,kBAAkB,QAAQ,iBAAiB,QAAG;AAAA,IAC/C,CAAC,UAAU,QAAQ,kBAAkB,OAAO,WAAM,IAAI,KAAK,QAAQ,aAAa,EAAE,YAAY,CAAC;AAAA,IAC/F,CAAC,gBAAgBA,KAAI,QAAQ,aAAa,CAAC;AAAA,IAC3C,CAAC,mBAAmBA,KAAI,QAAQ,qBAAqB,CAAC;AAAA,IACtD,CAAC,SAAS,QAAQ,kBAAkB,OAAO,WAAM,GAAG,QAAQ,cAAc,QAAQ,CAAC,CAAC,GAAG;AAAA,IACvF,CAAC,WAAWA,KAAI,QAAQ,cAAc,CAAC;AAAA,IACvC,CAAC,iBAAiBA,KAAI,QAAQ,mBAAmB,CAAC;AAAA,EACpD;AACF;AAGO,SAAS,kBAAkB,QAA6B;AAC7D,SAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;AACvC;","names":["NANO_PER_USD","usd"]}
|
|
@@ -18,7 +18,7 @@ var STATE_LABELS = {
|
|
|
18
18
|
blocked: "Blocked"
|
|
19
19
|
};
|
|
20
20
|
var STATE_TONES = {
|
|
21
|
-
intake: "bg-
|
|
21
|
+
intake: "bg-secondary text-muted-foreground",
|
|
22
22
|
missing_info: "bg-warning/10 text-warning",
|
|
23
23
|
working: "bg-primary/10 text-primary",
|
|
24
24
|
ready_for_review: "bg-success/10 text-success",
|
|
@@ -40,7 +40,7 @@ var STATUS_TONES = {
|
|
|
40
40
|
ready: "bg-success/10 text-success",
|
|
41
41
|
changes_requested: "bg-warning/10 text-warning",
|
|
42
42
|
approved: "bg-success/10 text-success",
|
|
43
|
-
superseded: "bg-
|
|
43
|
+
superseded: "bg-secondary text-muted-foreground"
|
|
44
44
|
};
|
|
45
45
|
function reviewQueueStateLabel(state) {
|
|
46
46
|
return STATE_LABELS[state];
|
|
@@ -90,7 +90,7 @@ function WorkProductCard({ part, onOpen, className }) {
|
|
|
90
90
|
{
|
|
91
91
|
type: "button",
|
|
92
92
|
onClick: () => onOpen(part),
|
|
93
|
-
className: "rounded-md border border-border px-2.5 py-1 text-xs font-medium text-foreground transition hover:bg-accent
|
|
93
|
+
className: "rounded-md border border-border px-2.5 py-1 text-xs font-medium text-foreground transition hover:bg-accent",
|
|
94
94
|
children: "Review"
|
|
95
95
|
}
|
|
96
96
|
)
|
|
@@ -145,7 +145,7 @@ function ReviewQueuePanel({
|
|
|
145
145
|
onClick: () => void load(),
|
|
146
146
|
disabled: loading,
|
|
147
147
|
"aria-label": "Refresh",
|
|
148
|
-
className: "rounded-md p-1.5 text-muted-foreground transition hover:bg-accent
|
|
148
|
+
className: "rounded-md p-1.5 text-muted-foreground transition hover:bg-accent hover:text-foreground disabled:opacity-50",
|
|
149
149
|
children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", className: `h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
|
|
150
150
|
/* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-2.64-6.36" }),
|
|
151
151
|
/* @__PURE__ */ jsx("polyline", { points: "21 3 21 9 15 9" })
|
|
@@ -161,7 +161,7 @@ function ReviewQueuePanel({
|
|
|
161
161
|
{
|
|
162
162
|
type: "button",
|
|
163
163
|
onClick: () => onSelect?.(item),
|
|
164
|
-
className: "flex w-full items-center gap-3 rounded-lg border border-border bg-card px-3 py-2.5 text-left transition hover:border-primary/40 hover:bg-accent
|
|
164
|
+
className: "flex w-full items-center gap-3 rounded-lg border border-border bg-card px-3 py-2.5 text-left transition hover:border-primary/40 hover:bg-accent",
|
|
165
165
|
children: [
|
|
166
166
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
167
167
|
/* @__PURE__ */ jsxs("p", { className: "truncate text-sm font-medium text-foreground", children: [
|
|
@@ -198,7 +198,7 @@ function ReviewQueuePanel({
|
|
|
198
198
|
type: "button",
|
|
199
199
|
onClick: () => void load(cursor),
|
|
200
200
|
disabled: loading,
|
|
201
|
-
className: "w-full rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition hover:bg-accent
|
|
201
|
+
className: "w-full rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition hover:bg-accent disabled:opacity-50",
|
|
202
202
|
children: "Load more"
|
|
203
203
|
}
|
|
204
204
|
)
|
|
@@ -266,7 +266,7 @@ function EvidenceLineageTable({ evidence, resolveSourceUrl, className }) {
|
|
|
266
266
|
var SEVERITY_TONES = {
|
|
267
267
|
blocking: "bg-destructive/10 text-destructive",
|
|
268
268
|
material: "bg-warning/10 text-warning",
|
|
269
|
-
advisory: "bg-
|
|
269
|
+
advisory: "bg-secondary text-muted-foreground"
|
|
270
270
|
};
|
|
271
271
|
function ExceptionList({ exceptions, className }) {
|
|
272
272
|
if (exceptions.length === 0) {
|
|
@@ -316,7 +316,7 @@ function ProvenanceStamp({ provenance, backtest, className }) {
|
|
|
316
316
|
"run ",
|
|
317
317
|
provenance.runId ? provenance.runId.slice(0, 10) : "\u2014"
|
|
318
318
|
] }),
|
|
319
|
-
provenance.servingModels.length > 0 ? provenance.servingModels.map((model) => /* @__PURE__ */ jsx("span", { className: "rounded-full bg-
|
|
319
|
+
provenance.servingModels.length > 0 ? provenance.servingModels.map((model) => /* @__PURE__ */ jsx("span", { className: "rounded-full bg-secondary px-2 py-0.5 font-mono", children: model }, model)) : /* @__PURE__ */ jsx("span", { className: "italic", children: "serving model pending" }),
|
|
320
320
|
typeof provenance.costUsd === "number" && /* @__PURE__ */ jsxs("span", { children: [
|
|
321
321
|
"$",
|
|
322
322
|
provenance.costUsd.toFixed(provenance.costUsd < 0.01 ? 4 : 2)
|
|
@@ -342,4 +342,4 @@ export {
|
|
|
342
342
|
QualityCheckList,
|
|
343
343
|
ProvenanceStamp
|
|
344
344
|
};
|
|
345
|
-
//# sourceMappingURL=chunk-
|
|
345
|
+
//# sourceMappingURL=chunk-5MZXWPFN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/web-react/work-product.tsx"],"sourcesContent":["/**\n * Work-product review surfaces — the sandbox-ui-FREE half of the vertical:\n *\n * - {@link ReviewQueuePanel}: the practice/matters/campaign review queue over\n * a `fetchQueue(cursor)` data port (the `AgentActivityPanel` shape).\n * - {@link WorkProductCard}: the persisted `type:'work_product'` transcript\n * anchor rendered inside `ChatMessages` — chat stays the driver surface.\n * - {@link EvidenceLineageTable}: target → claim → source rows with\n * click-through via an injected `resolveSourceUrl` (the\n * `MessageAttachments.resolveFileUrl` pattern).\n * - {@link ExceptionList} / {@link QualityCheckList}: severity-badged\n * exception rows and pass/fail check rows.\n * - {@link ProvenanceStamp}: profileHash prefix + runId + serving-model\n * chips + the optional backtest slot (\"182 cases · composite 0.81 ·\n * trust PASS\" — or \"quality: unverified\" when the trust gate failed).\n *\n * Anything hosting sandbox-ui (DiffView, CodeView, PillTabs) lives in\n * `./work-product-react` instead — this module must stay importable without\n * the optional sandbox-ui peer.\n */\n\nimport { useCallback, useEffect, useState } from 'react'\n\nimport {\n parseReviewQueueItem,\n type ReviewQueueItem,\n type ReviewQueueState,\n} from '../work-product/queue'\nimport type {\n EvidenceEntry,\n ExceptionEntry,\n ProfileBacktestSummary,\n QualityCheck,\n WorkProductPersistedPart,\n WorkProductProvenance,\n WorkProductStatus,\n} from '../work-product/types'\nimport { persistedPartToWorkProduct } from '../work-product/types'\n\nexport { parseReviewQueueItem }\nexport type { ReviewQueueItem, ReviewQueueState }\n\n// ── shared presentation vocabulary ────────────────────────────────────────\n\nconst STATE_LABELS: Record<ReviewQueueState, string> = {\n intake: 'Intake',\n missing_info: 'Missing info',\n working: 'Working',\n ready_for_review: 'Ready for review',\n changes_requested: 'Changes requested',\n approved: 'Approved',\n blocked: 'Blocked',\n}\n\nconst STATE_TONES: Record<ReviewQueueState, string> = {\n intake: 'bg-secondary text-muted-foreground',\n missing_info: 'bg-warning/10 text-warning',\n working: 'bg-primary/10 text-primary',\n ready_for_review: 'bg-success/10 text-success',\n changes_requested: 'bg-warning/10 text-warning',\n approved: 'bg-success/10 text-success',\n blocked: 'bg-destructive/10 text-destructive',\n}\n\nconst STATUS_LABELS: Record<WorkProductStatus, string> = {\n draft: 'Draft',\n blocked: 'Blocked',\n ready: 'Ready for review',\n changes_requested: 'Changes requested',\n approved: 'Approved',\n superseded: 'Superseded',\n}\n\nconst STATUS_TONES: Record<WorkProductStatus, string> = {\n draft: 'bg-primary/10 text-primary',\n blocked: 'bg-destructive/10 text-destructive',\n ready: 'bg-success/10 text-success',\n changes_requested: 'bg-warning/10 text-warning',\n approved: 'bg-success/10 text-success',\n superseded: 'bg-secondary text-muted-foreground',\n}\n\n/** Human label for a queue state. */\nexport function reviewQueueStateLabel(state: ReviewQueueState): string {\n return STATE_LABELS[state]\n}\n\n/** Human label for a work-product status. */\nexport function workProductStatusLabel(status: WorkProductStatus): string {\n return STATUS_LABELS[status]\n}\n\nfunction StatePill({ state }: { state: ReviewQueueState }) {\n return (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${STATE_TONES[state]}`}>\n {STATE_LABELS[state]}\n </span>\n )\n}\n\nfunction StatusPill({ status }: { status: WorkProductStatus }) {\n return (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${STATUS_TONES[status]}`}>\n {STATUS_LABELS[status]}\n </span>\n )\n}\n\n// ── transcript anchor helpers + card ──────────────────────────────────────\n\n/** Every persisted work-product anchor on one message, re-validated. */\nexport function workProductPartsFromMessageParts(\n parts: ReadonlyArray<Record<string, unknown>> | null | undefined,\n): WorkProductPersistedPart[] {\n if (!parts) return []\n const out: WorkProductPersistedPart[] = []\n for (const part of parts) {\n const typed = persistedPartToWorkProduct(part)\n if (typed) out.push(typed)\n }\n return out\n}\n\n/** Properties for the transcript anchor card rendered in chat */\nexport interface WorkProductCardProps {\n part: WorkProductPersistedPart\n /** Open the work product (queue detail / pane) — the card's only action. */\n onOpen?: (part: WorkProductPersistedPart) => void\n className?: string\n}\n\n/** The chat transcript anchor card for one work-product version — a compact,\n * system-authored pointer, not a parallel review surface. */\nexport function WorkProductCard({ part, onOpen, className }: WorkProductCardProps) {\n return (\n <div\n className={`flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 ${className ?? ''}`}\n data-work-product-id={part.ref.id}\n >\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary\">\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" />\n <polyline points=\"14 2 14 8 20 8\" />\n <path d=\"m9 15 2 2 4-4\" />\n </svg>\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-semibold text-foreground\">{part.title}</p>\n <p className=\"text-[11px] text-muted-foreground\">\n {part.kind && <span className=\"font-mono\">{part.kind}</span>}\n {part.kind && ' · '}v{part.ref.version}\n </p>\n </div>\n <StatusPill status={part.status} />\n {onOpen && (\n <button\n type=\"button\"\n onClick={() => onOpen(part)}\n className=\"rounded-md border border-border px-2.5 py-1 text-xs font-medium text-foreground transition hover:bg-accent\"\n >\n Review\n </button>\n )}\n </div>\n )\n}\n\n// ── review queue panel ────────────────────────────────────────────────────\n\n/** One fetched page of queue items with an optional continuation cursor */\nexport interface ReviewQueuePage {\n items: ReviewQueueItem[]\n /** Opaque continuation token; absent ⇒ no further pages. */\n nextCursor?: string\n}\n\n/** Properties for the review queue panel over a fetch data port */\nexport interface ReviewQueuePanelProps {\n /** Data port — fetch one page of queue items (the projection's output,\n * re-validated here at the JSON boundary). */\n fetchQueue: (cursor?: string) => Promise<ReviewQueuePage>\n /** Open one queue item (navigate to its thread / detail pane). */\n onSelect?: (item: ReviewQueueItem) => void\n title?: string\n emptyLabel?: string\n className?: string\n}\n\n/** Merge a fetched page into held rows: dedupe by scopeKey, incoming wins,\n * newest updatedAt first. Exported for tests. */\nexport function mergeReviewQueuePages(existing: ReviewQueueItem[], incoming: ReviewQueueItem[]): ReviewQueueItem[] {\n const byScope = new Map<string, ReviewQueueItem>()\n for (const item of existing) byScope.set(item.scopeKey, item)\n for (const item of incoming) byScope.set(item.scopeKey, item)\n return [...byScope.values()].sort((a, b) => b.updatedAt - a.updatedAt)\n}\n\n/** The workspace review queue — cursor-paged, refreshable, selection via\n * callback. States, counts, and provenance render from the projection; the\n * panel holds no domain logic. */\nexport function ReviewQueuePanel({\n fetchQueue,\n onSelect,\n title = 'Review queue',\n emptyLabel = 'Nothing awaiting review.',\n className,\n}: ReviewQueuePanelProps) {\n const [items, setItems] = useState<ReviewQueueItem[]>([])\n const [cursor, setCursor] = useState<string | undefined>(undefined)\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const load = useCallback(\n async (from?: string) => {\n setLoading(true)\n setError(null)\n try {\n const page = await fetchQueue(from)\n const validated = page.items\n .map((item) => parseReviewQueueItem(item))\n .filter((item): item is ReviewQueueItem => item !== null)\n setItems((prev) => mergeReviewQueuePages(from === undefined ? [] : prev, validated))\n setCursor(page.nextCursor)\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e))\n } finally {\n setLoading(false)\n }\n },\n [fetchQueue],\n )\n\n useEffect(() => {\n void load()\n }, [load])\n\n return (\n <div className={`space-y-2 ${className ?? ''}`}>\n <div className=\"flex items-center gap-2\">\n <h2 className=\"flex-1 text-sm font-semibold\">{title}</h2>\n <button\n type=\"button\"\n onClick={() => void load()}\n disabled={loading}\n aria-label=\"Refresh\"\n className=\"rounded-md p-1.5 text-muted-foreground transition hover:bg-accent hover:text-foreground disabled:opacity-50\"\n >\n <svg viewBox=\"0 0 24 24\" className={`h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}`} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M21 12a9 9 0 1 1-2.64-6.36\" />\n <polyline points=\"21 3 21 9 15 9\" />\n </svg>\n </button>\n </div>\n {error && <p className=\"rounded-md bg-destructive/10 px-3 py-2 text-xs text-destructive\">{error}</p>}\n {!error && items.length === 0 && !loading && (\n <p className=\"rounded-md border border-dashed border-border px-3 py-4 text-center text-xs text-muted-foreground\">{emptyLabel}</p>\n )}\n {/* The queue renders neither rows nor the empty copy while `loading`, so\n the announcement is the only thing a screen reader has during the wait.\n Kept mounted with changing text: an inserted live region that already\n carries its message is not reliably announced. */}\n <span role=\"status\" aria-live=\"polite\" aria-busy={loading} className=\"sr-only\">\n {loading ? 'Loading review queue…' : ''}\n </span>\n <ul className=\"space-y-1.5\" aria-busy={loading}>\n {items.map((item) => (\n <li key={item.scopeKey}>\n <button\n type=\"button\"\n onClick={() => onSelect?.(item)}\n className=\"flex w-full items-center gap-3 rounded-lg border border-border bg-card px-3 py-2.5 text-left transition hover:border-primary/40 hover:bg-accent\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-foreground\">\n {item.workProduct?.title ?? item.scopeKey}\n {item.workProduct && <span className=\"ml-1.5 text-[11px] text-muted-foreground\">v{item.workProduct.version}</span>}\n </p>\n <p className=\"mt-0.5 flex flex-wrap items-center gap-x-2 text-[11px] text-muted-foreground\">\n {item.workProduct?.kind && <span className=\"font-mono\">{item.workProduct.kind}</span>}\n {item.pendingAsk && <span>asks: {item.pendingAsk.title}</span>}\n {item.blockingExceptions > 0 && (\n <span className=\"text-destructive\">{item.blockingExceptions} blocking</span>\n )}\n {item.failedChecks > 0 && <span className=\"text-warning\">{item.failedChecks} failed checks</span>}\n {item.provenance && item.provenance.profileHash && (\n <span className=\"font-mono\">{item.provenance.profileHash.slice(0, 8)}</span>\n )}\n </p>\n </div>\n <StatePill state={item.state} />\n </button>\n </li>\n ))}\n </ul>\n {cursor && (\n <button\n type=\"button\"\n onClick={() => void load(cursor)}\n disabled={loading}\n className=\"w-full rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition hover:bg-accent disabled:opacity-50\"\n >\n Load more\n </button>\n )}\n </div>\n )\n}\n\n// ── evidence lineage table ────────────────────────────────────────────────\n\n/** Properties for the evidence lineage table with source click-through */\nexport interface EvidenceLineageTableProps {\n evidence: readonly EvidenceEntry[]\n /** Resolve one entry's source document to an openable URL (signed\n * object-store/vault download) — the `resolveFileUrl` pattern. Absent →\n * sources render as plain refs without links. */\n resolveSourceUrl?: (entry: EvidenceEntry) => string\n className?: string\n}\n\nfunction locatorLabel(entry: EvidenceEntry): string | null {\n const parts: string[] = []\n if (entry.locator.page !== undefined) parts.push(`p.${entry.locator.page}`)\n if (entry.locator.range) parts.push(entry.locator.range)\n return parts.length ? parts.join(' · ') : null\n}\n\n/** Target → claim → source lineage rows: the \"every material value traceable\n * to source evidence\" surface. Each row names the artifact target, the\n * claim, and the source document location supporting it. */\nexport function EvidenceLineageTable({ evidence, resolveSourceUrl, className }: EvidenceLineageTableProps) {\n if (evidence.length === 0) {\n return <p className={`text-xs text-muted-foreground ${className ?? ''}`}>No evidence recorded.</p>\n }\n return (\n <div className={`overflow-x-auto ${className ?? ''}`}>\n <table className=\"w-full border-collapse text-left text-sm\">\n <thead>\n <tr className=\"border-b border-border text-[11px] uppercase tracking-wide text-muted-foreground\">\n <th className=\"py-1.5 pr-3 font-medium\">Target</th>\n <th className=\"py-1.5 pr-3 font-medium\">Claim</th>\n <th className=\"py-1.5 font-medium\">Source</th>\n </tr>\n </thead>\n <tbody>\n {evidence.map((entry) => {\n const url = resolveSourceUrl?.(entry)\n const locator = locatorLabel(entry)\n return (\n <tr key={entry.id} className=\"border-b border-border/50 align-top\">\n <td className=\"py-2 pr-3 font-mono text-xs text-foreground\">{entry.target}</td>\n <td className=\"py-2 pr-3 text-[13px] leading-snug text-foreground\">\n {entry.claim}\n {entry.locator.quote && (\n // The basis is what a reviewer weighs, so it has to be\n // visible per row, not only in the aggregate check detail.\n // A platform-sliced quote came out of the document's own\n // bytes; a model-typed one was proved to occur but was\n // transcribed. Same text, different strength of evidence.\n <span\n className={`mt-0.5 block border-l-2 pl-2 text-xs italic ${\n entry.locator.quoteBasis === 'span'\n ? 'border-primary/60 text-foreground'\n : 'border-border text-muted-foreground'\n }`}\n title={\n entry.locator.quoteBasis === 'span'\n ? 'Sliced from the source document by the platform'\n : entry.locator.quoteBasis === 'model'\n ? 'Quoted by the agent, verified to occur in the source'\n : 'Unverified quote'\n }\n >\n “{entry.locator.quote}”\n {entry.locator.quoteBasis && (\n <span className=\"ml-1.5 align-middle text-[10px] not-italic uppercase tracking-wide text-muted-foreground\">\n {entry.locator.quoteBasis === 'span' ? 'from source' : 'verified'}\n </span>\n )}\n </span>\n )}\n </td>\n <td className=\"py-2 text-xs\">\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"font-mono text-primary underline-offset-2 hover:underline\"\n >\n {entry.sourceRef}\n </a>\n ) : (\n <span className=\"font-mono text-muted-foreground\">{entry.sourceRef}</span>\n )}\n {locator && <span className=\"ml-1.5 text-muted-foreground\">{locator}</span>}\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n}\n\n// ── exceptions + checks ───────────────────────────────────────────────────\n\nconst SEVERITY_TONES: Record<ExceptionEntry['severity'], string> = {\n blocking: 'bg-destructive/10 text-destructive',\n material: 'bg-warning/10 text-warning',\n advisory: 'bg-secondary text-muted-foreground',\n}\n\n/** Properties for the severity-badged exception list */\nexport interface ExceptionListProps {\n exceptions: readonly ExceptionEntry[]\n className?: string\n}\n\n/** Severity-badged exception rows with resolution state. */\nexport function ExceptionList({ exceptions, className }: ExceptionListProps) {\n if (exceptions.length === 0) {\n return <p className={`text-xs text-muted-foreground ${className ?? ''}`}>No exceptions flagged.</p>\n }\n return (\n <ul className={`space-y-1.5 ${className ?? ''}`}>\n {exceptions.map((entry) => (\n <li key={entry.id} className=\"flex items-start gap-2.5 rounded-lg border border-border bg-card px-3 py-2\">\n <span className={`mt-0.5 inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${SEVERITY_TONES[entry.severity]}`}>\n {entry.severity}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className={`text-[13px] leading-snug ${entry.resolved ? 'text-muted-foreground line-through' : 'text-foreground'}`}>\n {entry.message}\n </p>\n <p className=\"mt-0.5 flex flex-wrap gap-x-2 text-[11px] text-muted-foreground\">\n <span className=\"font-mono\">{entry.kind}</span>\n {entry.targets && entry.targets.length > 0 && <span className=\"font-mono\">{entry.targets.join(', ')}</span>}\n {entry.resolved && <span>resolved{entry.resolvedBy ? ` by ${entry.resolvedBy}` : ''}</span>}\n {entry.resolutionNote && <span>{entry.resolutionNote}</span>}\n </p>\n </div>\n </li>\n ))}\n </ul>\n )\n}\n\n/** Properties for the pass/fail quality check list */\nexport interface QualityCheckListProps {\n checks: readonly QualityCheck[]\n className?: string\n}\n\n/** Pass/fail quality-check rows tagged with their source (agent self-report,\n * platform gate, judge ensemble). */\nexport function QualityCheckList({ checks, className }: QualityCheckListProps) {\n if (checks.length === 0) {\n return <p className={`text-xs text-muted-foreground ${className ?? ''}`}>No checks recorded.</p>\n }\n return (\n <ul className={`space-y-1 ${className ?? ''}`}>\n {checks.map((check) => (\n <li key={check.id} className=\"flex items-start gap-2 rounded-md px-1 py-1\">\n {check.passed ? (\n <svg viewBox=\"0 0 24 24\" className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-success\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n ) : (\n <svg viewBox=\"0 0 24 24\" className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-destructive\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-[13px] text-foreground\">\n {check.name}\n <span className=\"ml-1.5 text-[10px] uppercase tracking-wide text-muted-foreground\">{check.source}</span>\n </p>\n {check.detail && <p className=\"text-[11px] leading-snug text-muted-foreground\">{check.detail}</p>}\n </div>\n </li>\n ))}\n </ul>\n )\n}\n\n// ── provenance stamp ──────────────────────────────────────────────────────\n\n/** Properties for the provenance stamp with the optional backtest slot */\nexport interface ProvenanceStampProps {\n provenance: Pick<WorkProductProvenance, 'profileHash' | 'runId' | 'servingModels'> &\n Partial<Pick<WorkProductProvenance, 'costUsd' | 'producedAt'>>\n /** The product-resolved backtest summary for this profile hash. When its\n * trust gate failed, the composite renders as \"quality: unverified\" —\n * never a naked number. */\n backtest?: ProfileBacktestSummary\n className?: string\n}\n\n/** The audit line a reviewer approves against: which configuration produced\n * this document, what served it, and how that configuration measured on its\n * backtest. */\nexport function ProvenanceStamp({ provenance, backtest, className }: ProvenanceStampProps) {\n return (\n <div className={`flex flex-wrap items-center gap-x-2.5 gap-y-1 text-[11px] text-muted-foreground ${className ?? ''}`}>\n <span className=\"font-mono\" title={provenance.profileHash}>\n profile {provenance.profileHash ? provenance.profileHash.slice(0, 10) : '—'}\n </span>\n <span className=\"font-mono\" title={provenance.runId}>\n run {provenance.runId ? provenance.runId.slice(0, 10) : '—'}\n </span>\n {provenance.servingModels.length > 0 ? (\n provenance.servingModels.map((model) => (\n <span key={model} className=\"rounded-full bg-secondary px-2 py-0.5 font-mono\">\n {model}\n </span>\n ))\n ) : (\n <span className=\"italic\">serving model pending</span>\n )}\n {typeof provenance.costUsd === 'number' && <span>${provenance.costUsd.toFixed(provenance.costUsd < 0.01 ? 4 : 2)}</span>}\n {backtest &&\n (backtest.trust === 'pass' ? (\n <span className=\"text-success\">\n {backtest.cases} backtest cases · composite {backtest.composite.toFixed(2)} · trust PASS\n </span>\n ) : (\n <span className=\"text-warning\" title={backtest.trustReasons.join('; ')}>\n quality: unverified\n </span>\n ))}\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -410,7 +410,7 @@ function PagesStrip({
|
|
|
410
410
|
return /* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 flex-col bg-[var(--bg-input)]", children: [
|
|
411
411
|
label ? /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1.5 px-3 pt-1.5 text-[var(--text-muted)]", children: [
|
|
412
412
|
/* @__PURE__ */ jsx5(PageGlyph, { className: "h-3 w-3" }),
|
|
413
|
-
/* @__PURE__ */ jsx5("span", { className: "text-[
|
|
413
|
+
/* @__PURE__ */ jsx5("span", { className: "text-[11px] font-semibold uppercase tracking-[0.05em]", children: label })
|
|
414
414
|
] }) : null,
|
|
415
415
|
/* @__PURE__ */ jsxs3(
|
|
416
416
|
"div",
|
|
@@ -477,7 +477,7 @@ function PagesStrip({
|
|
|
477
477
|
) : /* @__PURE__ */ jsx5("div", { className: "flex h-full w-full items-center justify-center", children: /* @__PURE__ */ jsx5(PageGlyph, { className: "h-5 w-5 text-[var(--text-muted)]" }) })
|
|
478
478
|
}
|
|
479
479
|
),
|
|
480
|
-
/* @__PURE__ */ jsx5("span", { className: "max-w-[80px] truncate text-[
|
|
480
|
+
/* @__PURE__ */ jsx5("span", { className: "max-w-[80px] truncate text-[11px] text-[var(--text-secondary)]", children: page.name })
|
|
481
481
|
]
|
|
482
482
|
}
|
|
483
483
|
),
|
|
@@ -524,7 +524,7 @@ function PagesStrip({
|
|
|
524
524
|
className: "flex h-[72px] w-[80px] shrink-0 flex-col items-center justify-center gap-1 rounded border border-dashed border-[var(--border-default)] text-[var(--text-muted)] transition hover:border-[var(--brand-primary)] hover:text-[var(--brand-primary)]",
|
|
525
525
|
children: [
|
|
526
526
|
/* @__PURE__ */ jsx5(PlusGlyph, { className: "h-4 w-4" }),
|
|
527
|
-
/* @__PURE__ */ jsx5("span", { className: "text-[
|
|
527
|
+
/* @__PURE__ */ jsx5("span", { className: "text-[11px]", children: "Add page" })
|
|
528
528
|
]
|
|
529
529
|
}
|
|
530
530
|
) : null
|
|
@@ -881,7 +881,7 @@ function Popover({
|
|
|
881
881
|
open ? children : null
|
|
882
882
|
] });
|
|
883
883
|
}
|
|
884
|
-
var FIELD_LABEL = "text-[
|
|
884
|
+
var FIELD_LABEL = "text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--text-muted)]";
|
|
885
885
|
function NumberInput({
|
|
886
886
|
label,
|
|
887
887
|
value,
|
|
@@ -911,7 +911,7 @@ function NumberInput({
|
|
|
911
911
|
if (event.key === "Enter") commit(event.target.value);
|
|
912
912
|
if (event.key === "Escape") setRaw(null);
|
|
913
913
|
},
|
|
914
|
-
className: `${className} rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-center text-
|
|
914
|
+
className: `${className} rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-center text-xs text-[var(--text-primary)] focus:border-[var(--brand-primary)]`
|
|
915
915
|
}
|
|
916
916
|
)
|
|
917
917
|
] });
|
|
@@ -941,7 +941,7 @@ function SelectControl({
|
|
|
941
941
|
"aria-haspopup": "listbox",
|
|
942
942
|
"aria-expanded": open,
|
|
943
943
|
onClick: () => setOpen((v) => !v),
|
|
944
|
-
className: `${buttonClassName} flex items-center justify-between gap-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-0.5 text-left text-
|
|
944
|
+
className: `${buttonClassName} flex items-center justify-between gap-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-0.5 text-left text-xs text-[var(--text-primary)] hover:border-[var(--brand-primary)] disabled:cursor-default disabled:opacity-40`,
|
|
945
945
|
children: [
|
|
946
946
|
/* @__PURE__ */ jsx7("span", { className: "truncate", children: current?.label ?? value }),
|
|
947
947
|
/* @__PURE__ */ jsx7("span", { className: "text-[8px] text-[var(--text-muted)]", children: "\u25BE" })
|
|
@@ -958,7 +958,7 @@ function SelectControl({
|
|
|
958
958
|
onChange(opt.value);
|
|
959
959
|
setOpen(false);
|
|
960
960
|
},
|
|
961
|
-
className: `px-3 py-1 text-left text-
|
|
961
|
+
className: `px-3 py-1 text-left text-xs hover:bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)] ${opt.value === value ? "text-[var(--brand-primary)]" : "text-[var(--text-primary)]"}`,
|
|
962
962
|
children: opt.label
|
|
963
963
|
},
|
|
964
964
|
opt.value
|
|
@@ -995,7 +995,7 @@ function FontPicker({
|
|
|
995
995
|
"aria-expanded": open,
|
|
996
996
|
"aria-label": "Font family",
|
|
997
997
|
onClick: () => setOpen((v) => !v),
|
|
998
|
-
className: "flex w-28 items-center justify-between gap-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-0.5 text-left text-
|
|
998
|
+
className: "flex w-28 items-center justify-between gap-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-0.5 text-left text-xs text-[var(--text-primary)] hover:border-[var(--brand-primary)] disabled:cursor-default disabled:opacity-40",
|
|
999
999
|
style: { fontFamily: value },
|
|
1000
1000
|
children: [
|
|
1001
1001
|
/* @__PURE__ */ jsx7("span", { className: "truncate", children: value }),
|
|
@@ -1012,7 +1012,7 @@ function FontPicker({
|
|
|
1012
1012
|
onChange: (event) => setQuery(event.target.value),
|
|
1013
1013
|
placeholder: "Search fonts",
|
|
1014
1014
|
"aria-label": "Search fonts",
|
|
1015
|
-
className: "m-1 rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-
|
|
1015
|
+
className: "m-1 rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-xs text-[var(--text-primary)] focus:border-[var(--brand-primary)]"
|
|
1016
1016
|
}
|
|
1017
1017
|
),
|
|
1018
1018
|
/* @__PURE__ */ jsx7("div", { role: "listbox", className: "max-h-60 overflow-y-auto py-1", children: filtered.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "px-3 py-2 text-[11px] text-[var(--text-muted)]", children: "No matches" }) : filtered.map((family) => /* @__PURE__ */ jsx7(
|
|
@@ -1027,7 +1027,7 @@ function FontPicker({
|
|
|
1027
1027
|
setQuery("");
|
|
1028
1028
|
},
|
|
1029
1029
|
style: { fontFamily: family },
|
|
1030
|
-
className: `block w-full px-3 py-1 text-left text-
|
|
1030
|
+
className: `block w-full px-3 py-1 text-left text-sm hover:bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)] ${family === value ? "text-[var(--brand-primary)]" : "text-[var(--text-primary)]"}`,
|
|
1031
1031
|
children: family
|
|
1032
1032
|
},
|
|
1033
1033
|
family
|
|
@@ -1078,7 +1078,7 @@ function ColorSwatch({ label, value, onCommit, disabled, none = false }) {
|
|
|
1078
1078
|
"aria-label": `${label} hex value`,
|
|
1079
1079
|
value,
|
|
1080
1080
|
onChange: (event) => onCommit(event.target.value),
|
|
1081
|
-
className: "rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-
|
|
1081
|
+
className: "rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-xs text-[var(--text-primary)] focus:border-[var(--brand-primary)]"
|
|
1082
1082
|
}
|
|
1083
1083
|
)
|
|
1084
1084
|
] })
|
|
@@ -1282,7 +1282,7 @@ function SelectionControls({
|
|
|
1282
1282
|
value: slotInput,
|
|
1283
1283
|
onChange: (event) => setSlotInput(event.target.value),
|
|
1284
1284
|
placeholder: "slot-name",
|
|
1285
|
-
className: "rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-
|
|
1285
|
+
className: "rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-xs text-[var(--text-primary)] focus:border-[var(--brand-primary)]"
|
|
1286
1286
|
}
|
|
1287
1287
|
),
|
|
1288
1288
|
/* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [
|
|
@@ -1429,7 +1429,7 @@ function ImageControls({ element, canWrite, onPatch }) {
|
|
|
1429
1429
|
onChange: (event) => setSwapUrl(event.target.value),
|
|
1430
1430
|
placeholder: "https://\u2026 image URL",
|
|
1431
1431
|
"aria-label": "New image URL",
|
|
1432
|
-
className: "rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-
|
|
1432
|
+
className: "rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-xs text-[var(--text-primary)] focus:border-[var(--brand-primary)]"
|
|
1433
1433
|
}
|
|
1434
1434
|
),
|
|
1435
1435
|
/* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [
|
|
@@ -1484,7 +1484,7 @@ function PagePropsControls({ page, canWrite, onSetPageProps, onSetPageGuides, pa
|
|
|
1484
1484
|
value: page.name,
|
|
1485
1485
|
disabled: !canWrite,
|
|
1486
1486
|
onChange: (event) => onSetPageProps({ name: event.target.value }),
|
|
1487
|
-
className: "w-28 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-0.5 text-
|
|
1487
|
+
className: "w-28 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-0.5 text-xs text-[var(--text-primary)] focus:border-[var(--brand-primary)]"
|
|
1488
1488
|
}
|
|
1489
1489
|
),
|
|
1490
1490
|
SEP,
|
|
@@ -1517,7 +1517,7 @@ function PagePropsControls({ page, canWrite, onSetPageProps, onSetPageGuides, pa
|
|
|
1517
1517
|
if (event.key === "Enter") commitDimension("width", event.target.value);
|
|
1518
1518
|
if (event.key === "Escape") setCustomW(null);
|
|
1519
1519
|
},
|
|
1520
|
-
className: "w-16 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-center text-
|
|
1520
|
+
className: "w-16 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-center text-xs text-[var(--text-primary)] focus:border-[var(--brand-primary)]"
|
|
1521
1521
|
}
|
|
1522
1522
|
)
|
|
1523
1523
|
] }),
|
|
@@ -1537,7 +1537,7 @@ function PagePropsControls({ page, canWrite, onSetPageProps, onSetPageGuides, pa
|
|
|
1537
1537
|
if (event.key === "Enter") commitDimension("height", event.target.value);
|
|
1538
1538
|
if (event.key === "Escape") setCustomH(null);
|
|
1539
1539
|
},
|
|
1540
|
-
className: "w-16 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-center text-
|
|
1540
|
+
className: "w-16 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-center text-xs text-[var(--text-primary)] focus:border-[var(--brand-primary)]"
|
|
1541
1541
|
}
|
|
1542
1542
|
)
|
|
1543
1543
|
] }),
|
|
@@ -2259,4 +2259,4 @@ export {
|
|
|
2259
2259
|
ExportControl,
|
|
2260
2260
|
DesignCanvas
|
|
2261
2261
|
};
|
|
2262
|
-
//# sourceMappingURL=chunk-
|
|
2262
|
+
//# sourceMappingURL=chunk-5N434BGF.js.map
|