18d5f5bfc0
Piggy arrived as a chat panel bolted onto a CRM and then grew a workspace around it. The layout was already right — the audit found the approval card to be the best-designed object in the repo, and the account page's empty panels less finished than anything in the workspace. What was wrong was vocabulary: nobody had written the small things down, so both halves kept inventing them. Piggy was drawn with five different marks — a pig in the dock, a sparkle in the sidebar and again on the model picker, a speech bubble on the Ask buttons, and a stock robot glyph on every assistant message, which is the one people look at most. There is now one mark. The composer, which is the first control in the product since sign-in lands on /piggy, was the only un-adapted shadcn field left: 6px radius against a 12px Send button it sat 8px from. A stat tile had been reinvented six times at three numeral scales, and the same uppercase micro-label existed in five variants, two of them one tab apart in the same rail. There were 63 hand-written font sizes: not a scale, sixty-three opinions. Underneath that, the focus ring was invisible. The global rule used ring-accent, which Tailwind deliberately aliases onto the hover tint, so the ring measured 1.01:1 against the light canvas — no visible focus indicator anywhere in the product, for any accent, in either theme. It is ring-brand now and measures 17:1. The warning, positive and info tones were darkened until each clears 4.5:1 on a card, on inset and on its own chip, and the light canvas moved to 98% so a card lifts without leaning on its shadow. The mobile work is the part worth reading. A landscape phone gave the transcript 28% of the viewport and a keyboard-up phone 16%, against a 45% floor — and the fixed tab bar painted over the composer, covering the safety sentence and half the Send button, because two source comments asserted the bar stood down on short viewports and it never had. Both fixed and measured by hit-testing rather than by screenshot. The composer itself was 64px tall for a blank second line nobody typed, because the auto-resize effect sizes to scrollHeight and scrollHeight counts rows — a CSS height could not win against an inline style, so the attribute was the honest lever. Verified across both themes driven through the app's own control: no horizontal overflow on 15 routes at four viewports, 672 stat values that fit, 297 labels at exactly 11px/500, Escape returning focus to its opener rather than the body on every overlay, and a rejected write no longer reporting "Succeeded" with a green check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
123 lines
4.7 KiB
TypeScript
123 lines
4.7 KiB
TypeScript
/**
|
||
* That the ledger is not a keyhole into somebody's chat history.
|
||
*
|
||
* The two files were contradicting each other. `piggy-conversations.ts` states
|
||
* that a transcript belongs to exactly one person and that a platform admin is
|
||
* deliberately not an exception, because the audit trail lives in `agent_runs`.
|
||
* `PiggyActivityService` agrees in its header — and then widens `agent_runs` to
|
||
* the whole workspace for an admin while returning `label`, which is the user's
|
||
* question, and `summary`, which is the first line of Piggy's answer. Both of
|
||
* those are the transcript by another name.
|
||
*
|
||
* It is settled the way the conversation store settles it: cost and outcome are
|
||
* the company's record, the words are the person's. These assertions are what
|
||
* keep the two files agreeing.
|
||
*/
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import test from 'node:test';
|
||
import {
|
||
PIGGY_WITHHELD_LABEL,
|
||
withoutOtherPeoplesWords,
|
||
} from '../src/routes/piggy-activity';
|
||
import type {
|
||
PiggyActivityOverview,
|
||
PiggyRunSummary,
|
||
} from '../src/services/piggy-activity';
|
||
|
||
function run(overrides: Partial<PiggyRunSummary> = {}): PiggyRunSummary {
|
||
return {
|
||
id: '40000000-0000-4000-8000-000000000001',
|
||
kind: 'chat',
|
||
agent: 'piggy',
|
||
status: 'succeeded',
|
||
model: 'nvidia/nemotron-3-nano-30b-a3b',
|
||
mode: 'confirm',
|
||
label: 'Are we under water on the Northwind renewal?',
|
||
summary: 'Yes — the block is 38 per cent idle at the current rate.',
|
||
error: null,
|
||
inputTokens: 2_100,
|
||
outputTokens: 180,
|
||
costMicroCents: 4_200,
|
||
startedAt: '2026-08-13T09:00:00.000Z',
|
||
finishedAt: '2026-08-13T09:00:04.000Z',
|
||
durationMs: 4_000,
|
||
taskKind: null,
|
||
conversation: null,
|
||
/**
|
||
* Populated ONLY when the run is somebody else's — that is what the service
|
||
* promises, and it is the signal the redaction turns on.
|
||
*/
|
||
principal: { id: '50000000-0000-4000-8000-00000000000b', name: 'A colleague' },
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function overview(runs: PiggyRunSummary[]): PiggyActivityOverview {
|
||
return {
|
||
runs,
|
||
tasks: [],
|
||
spend: { todayMicroCents: 4_200, monthMicroCents: 91_000, turns: 22 },
|
||
};
|
||
}
|
||
|
||
test('an administrator reads a colleague’s spend and not their question', () => {
|
||
const [redacted] = withoutOtherPeoplesWords(overview([run()])).runs;
|
||
assert.ok(redacted);
|
||
|
||
// The words, which are the half that belongs to the person who typed them.
|
||
assert.equal(redacted.label, PIGGY_WITHHELD_LABEL);
|
||
assert.equal(redacted.summary, null);
|
||
|
||
// Everything an audit is actually for, which is the half that belongs to PIG.
|
||
assert.equal(redacted.status, 'succeeded');
|
||
assert.equal(redacted.model, 'nvidia/nemotron-3-nano-30b-a3b');
|
||
// What a turn was allowed to do is the company's record, not the person's
|
||
// words: an audit that cannot say which turns could write is not an audit.
|
||
assert.equal(redacted.mode, 'confirm');
|
||
assert.equal(redacted.costMicroCents, 4_200);
|
||
assert.equal(redacted.inputTokens, 2_100);
|
||
assert.equal(redacted.durationMs, 4_000);
|
||
assert.equal(redacted.principal?.name, 'A colleague');
|
||
});
|
||
|
||
test('a failure stays legible, because that is what an admin is looking for', () => {
|
||
const failed = run({ status: 'failed', error: 'Prime Inference returned 429.' });
|
||
const [redacted] = withoutOtherPeoplesWords(overview([failed])).runs;
|
||
assert.equal(redacted?.error, 'Prime Inference returned 429.');
|
||
assert.equal(redacted?.status, 'failed');
|
||
assert.equal(redacted?.label, PIGGY_WITHHELD_LABEL);
|
||
});
|
||
|
||
test('my own rows are untouched, whoever I am', () => {
|
||
// The service leaves `principal` null on the caller's own runs, so this is
|
||
// the shape an ordinary member sees for every row and an admin sees for
|
||
// theirs. Redacting it would take somebody's history away from themselves.
|
||
const mine = run({ principal: null });
|
||
const [kept] = withoutOtherPeoplesWords(overview([mine])).runs;
|
||
assert.deepEqual(kept, mine);
|
||
});
|
||
|
||
test('the spend and the queue are not touched', () => {
|
||
const before = overview([run(), run({ principal: null })]);
|
||
const after = withoutOtherPeoplesWords(before);
|
||
assert.deepEqual(after.spend, before.spend);
|
||
assert.deepEqual(after.tasks, before.tasks);
|
||
assert.equal(after.runs.length, 2);
|
||
});
|
||
|
||
/**
|
||
* The gate is one call, and a route that stops making it looks exactly like a
|
||
* route that still does. Asserted against the source for the same reason
|
||
* read-governance.test.ts reads route files: there is nothing else to catch a
|
||
* deletion here.
|
||
*/
|
||
test('the route still applies the gate', () => {
|
||
const source = readFileSync(
|
||
join(import.meta.dirname, '..', 'src', 'routes', 'piggy-activity.ts'),
|
||
'utf8',
|
||
);
|
||
assert.match(source, /withoutOtherPeoplesWords\(await activity\.overview\(/);
|
||
});
|