push-guard.ts
Language TypeScript3.8 KB96 lines
/*** Whether the local artifacts are safe to publish to the deployed board.* Runtime-neutral and pure, so it can be tested without a filesystem or a network.** `pnpm push` writes straight into the Worker's KV, which is what the team* reads. There is no review step between a laptop and that* page, so the checks that would normally be someone noticing have to be here.** The one that matters most is fixture mode. `pnpm dashboard --fixtures` writes* the same filenames as a live run, so a push immediately after one would put* sample data on the production board under "live" source pills: the exact* failure `Snapshot.mode` exists to prevent, with an audience.*/export interface PushInputs {/** `mode` from briefing-payload.json. */payloadMode: string | null | undefined/** `generatedAt` from briefing-payload.json, equal to the sync's syncedAt. */payloadGeneratedAt: string | null | undefined/** Length of dashboard.html, to catch a truncated or empty render. */htmlBytes: number/** `mode` from briefings.json. Null when no briefings exist yet. */briefingsMode?: string | null/** `payloadGeneratedAt` from briefings.json: the sync the prose describes. */briefingsPayloadGeneratedAt?: string | null/** How many briefings were written. */briefingCount?: number}export interface PushVerdict {ok: boolean/** Why the push must not happen. Non-empty means refuse. */refusals: string[]/** Worth saying out loud, but not a reason to stop. */warnings: string[]}/** A page this small is not a dashboard; the last render must have failed. */const MIN_HTML_BYTES = 20_000export function checkPush(input: PushInputs): PushVerdict {const refusals: string[] = []const warnings: string[] = []// Fixtures must never reach the deployed board. Sample data renders under the// same filenames as live data; only `mode` distinguishes them.if (input.payloadMode === 'fixtures') {refusals.push('briefing-payload.json is FIXTURE data. Pushing it would put sample orders on ' +'the board the team reads. Run `pnpm dashboard` (no --fixtures), then push.',)}if (input.briefingsMode === 'fixtures') {refusals.push('briefings.json was written from FIXTURE data, so its prose names people as ' +'holding work that is not real. Regenerate it from a live sync.',)}if (input.payloadMode !== 'live' && input.payloadMode !== 'fixtures') {refusals.push(`briefing-payload.json has mode ${JSON.stringify(input.payloadMode)}, which this ` +'does not recognise. Refusing rather than guessing whether it is real.',)}if (!input.payloadGeneratedAt) {refusals.push('briefing-payload.json has no generatedAt, so nothing on it can be dated.')}if (input.htmlBytes < MIN_HTML_BYTES) {refusals.push(`dashboard.html is only ${input.htmlBytes} bytes, well under the ${MIN_HTML_BYTES} a ` +'real render produces. Re-run `pnpm dashboard` and check it for errors.',)}// Briefings are optional. Absent is a normal state and the page says so, but it// is worth naming: a silent push of numbers-only looks identical to a push// that included the prose.if (!input.briefingCount) {warnings.push('No written briefings to push. The board will render the deterministic ' +'briefing and say the written one was not generated. Run `pnpm morning` for both.',)} else if (input.briefingsPayloadGeneratedAt &&input.briefingsPayloadGeneratedAt !== input.payloadGeneratedAt) {warnings.push('The briefings were written from an EARLIER sync than the page being pushed, so ' +'the board will carry a staleness caveat. Run `pnpm morning` to pair them.',)}return { ok: refusals.length === 0, refusals, warnings }}