Factory docs, home
Page navigation

Regular, concise updates for the owner from recorded facts (the Factory bet's module M6, quick win H2). Exception batches carry each new branch-health episode once, in the next of at most three daily batches, never from 22:00 to 07:00. A Monday digest of at most 150 words gives one line per Product with its coverage, and cites every claim. It also records the owner's delight and frustration notes.

It sends nothing: no delivery channel exists until the owner picks one, so batches and digests are recorded and read by the Dashboard. It never says "Nothing needs you" under incomplete coverage.

Status: Accepted local implementation: independent Astra reviews and combined checks passed. No live polling, scheduling or delivery. Acceptance and evidence.

  • Source: src/owner-digest.ts
  • Tests: test/owner-digest.test.ts, test/dashboard-digest.test.ts, digest cases in test/branch-health-adversarial.test.ts, and apps/dashboard/test/digest-render.test.ts.
  • Helpers: those of Branch health: the recorded capture, synthetic repositories and the in-memory reader
  • Design: <local evidence archive> §2 (the guardrails and "How we learn"), §3 (M6) and §4 (H2's acceptance)
  • Human page: guide

Intelligence: none — Every line is rendered from recorded facts and cites them; a model would put unsourced words into what the owner reads and trusts.

What it hides

  • Local time. The commission's IANA zone gives wall-clock times through Intl.DateTimeFormat; a local time that happens twice takes the earlier instant and one in a gap moves forward, as Temporal's "compatible" rule does. Quiet hours are 22:00 until 07:00 local.
  • Batch slots. A batch is due at the latest of the day's batch times that has passed, in local time. Nothing forms in quiet hours or before the day's first batch time; a slot missed in quiet hours waits for the next day's first. A batch takes the episodes branch health opened at or before its slot, after each Product's watermark, reading whole branch-health.episodes-opened events; with neither episodes nor coverage gaps, nothing is recorded (empty). A detection time is the poll's start, so a poll recorded after its slot's batch formed waits for the next batch. Each line names the episode's first failed step, when its jobs were read, and links its latest red run on GitHub.
  • The digest. Due from Monday at the digest time, formed once per ISO week and never in quiet hours. Its input holds a line's worth for each registered Product, in registry order, from its branch-health record. It adds the owner-required count over the Portfolio's current conditions (Required actions and blockers), the asks as given (unknown until an ask ledger records them) and the previous week's notes. A pure function renders it, red Products first, then those not known, then green ones.
  • Batch coverage gaps. A batch names unreadable history or records, and a poll older than an hour before its slot. A poll reporting unread head, workflow or run evidence adds poll-unreadable, even when recent. These gaps form a batch without new episodes; they never mean nothing changed. A Product with no Branch health record is named as unwatched in the Monday digest.
  • Current or not. A Product's last poll counts as current when it is at most an hour older than the digest (two of branch health's proposed 30-minute polls). An older one is shown as "not polled since" its time that day, or its date, with the state it had then; it is never shown as the current state and counts as unknown.
  • The record. One journal aggregate, owner-digest:main (sf-owner-digest/2): per-Product batch watermarks, the last slot, the 21 most recent batches, the 8 most recent digests (each with the input it was rendered from) and the notes of the last 8 weeks. What would be stored is re-validated first. A stored /1 aggregate is refused as CORRUPT; there is no silent migration or reinterpretation of its text.

Public interface

NameWhat
OwnerDigestnew OwnerDigest(journal, {commission, now, portfolio?}): formBatch(), formDigest({asks?}), recordNote({commandId, feeling, outsideAskMinutes, text?})
BatchOutcomequiet, not-due, empty with its slot, or formed with replayed and the BatchRecord
DigestOutcomequiet, not-due with dueAt, or formed with replayed and the DigestRecord (week, dueAt, formedAt, input, rendered)
NoteOutcomerecorded with replayed, the ISO week and that week's note count
renderDigest(input)the pure rendering: RenderedDigest {text, words, readingSeconds, lines, citations, coverageComplete, nothingNeedsYou, compact, asksCounted}; DigestError LIMIT when even the compact form with the asks counted passes 150 words
readOwnerDigest(journal)the recorded digests and batches, newest first, and the retained notes, re-validated; reads only
proposedDigestCommission(), parseDigestCommission(value)the proposal (Europe/London, batches at 09:00, 13:00 and 18:00, the digest at 07:00) and the check: 1–3 ascending batch times and a digest time, each from 07:00 to before 22:00, in a canonical zone
wordsOf(text), isoWeekOf(date)words as counted here (tokens holding a letter or digit; a dash alone is none, and a citation marker belongs to its word); the ISO week of a date
OWNER_DIGEST_LIMITS150 words; 175 words a minute for reading time (the slow end of most adults' range); 3 batch times; quiet 22:00–07:00; a poll current for 1 hour; 50 episodes a batch; 2 episodes named a Product line; 5 asks listed; 21 batches, 8 digests and 8 weeks of notes kept, 400 notes at most; notes of 280 characters and at most 10,080 minutes; 100 Products; 786,432 bytes of state
TypesDigestCommission, DigestInput, DigestProduct, DigestHealth, DigestTally, DigestEpisode, AsksInput, DigestLine, FactRef, Citation, BatchItem, BatchRecord, NoteRecord, Feeling; DigestError with code INVALID, CONFLICT, CORRUPT or LIMIT

Stored layout. Commands owner-digest/batch/<local slot> (such as 2026-09-30T09:00), owner-digest/digest/<ISO week> and owner-digest/note/<commandId>. Events owner-digest.batch-formed, owner-digest.digest-formed (with its words and reading time) and owner-digest.note-recorded.

The digest's lines.

  1. A header, such as "Factory digest, week of 5 October 2026."
  2. One line per Product, reds first: <name>: red. with at most two open episodes (workflow, signature, onset date, runaway, shared step) and the check tally on its head; <name>: —, <reason>. when unknown or not current; <name>: green. with its tally.
  3. A line on asks, a summary and last week's notes.

Each factual line carries a marker such as [3]; citations maps it to Branch health records, episode events, Portfolio conditions, registry entries, notes or the asks' source. Unknown asks have no source to cite.

DigestEpisode and BatchItem store blockerMirrored. Formation sets it only when Portfolio.blockerRecord(..., {asOf}) contains the exact source triple: collector branch-health, the expected record ID and the expected revision from blockerSource. A missing or mismatched mirror omits only that blocker citation; the Branch health record or episode event still supports the line. Stored inputs require the boolean and re-render their citations during validation. A poll-unreadable gap cites the registry; a stale-poll gap cites the last poll.

Invariants and guarantees

  • Replay and batching. Watermarks advance only past events a batch took. An event's episodes stay together; one that would overfill a non-empty batch waits for the next. A formed slot replays its recorded batch. A no-signal episode's first failure is a separate event included once.
  • Word bound. Over 150 words, Product lines lose detail and the summary counts unknown Products instead of repeating names. Next, asks are counted instead of listed. A still-oversized digest is refused, never truncated; reading seconds are rounded up at 175 words a minute.
  • Complete coverage. "Nothing needs you" requires every Product green on a current poll, owner-required items read as zero, and complete ask coverage with none open. Incomplete recorded asks are "at least N"; unknown asks name their gap. Unknown health never becomes green or zero.
  • Notes. Delight/frustration notes keep the owner's reported minutes and optional words. Reusing their command ID replays identical content or conflicts on different content. The digest counts the previous ISO week's notes.
  • Stored evidence. Input, text, lines and citations are re-validated before writing and on reading. A malformed input, including an ask containing a line break, is refused before storage. Corrupt Product records/history produce gaps without hiding readable Products. Formation records text only: it never sends, applies a rule or schedules work.

Failure semantics

CodeWhen
INVALIDA bad commission (a fourth batch time, a time inside quiet hours, times out of order, a non-canonical zone), clock, note (feeling, minutes, text, command ID), journal, or an input, digest or batch that would not re-validate once stored
CONFLICTA note's command ID reused with other content; another writer moving the record while a batch, digest or note forms (asking again answers with the recorded one)
CORRUPTUnsupported stored protocol (including /1), malformed state, or recorded text/citations that do not follow from their input
LIMITA digest over 150 words even compact with its asks counted; 400 notes; the state's byte bound

Journal errors propagate unchanged.

Trust scope

Implementation tests cover recorded/synthetic fixtures, timing, replay, evidence, limits and corruption. Dashboard checks cover the read-only view; they do not establish live delivery or scheduling.

Not established:

  • Delivery. Nothing reaches the owner but the dashboard page; the channel is the owner's decision.
  • Scheduling. Nothing forms batches or digests on a schedule, and the times are a proposal. The extension spec's quarterly-only cadence has not been amended by the owner (bet-proposals.md §4). Branch health polls on no schedule either, so every Product line reads "not polled since" until something does.
  • Asks and rule uses. The ask ledger is implemented in a separate lane but is not composed here, so default asks remain unknown and the digest never says "Nothing needs you"; M6's rule uses, verified movement and per-Product next step are not built.
  • Notes' home. The ask ledger's weekly note also records a feeling and outside-ask minutes; which record the digest counts once both land is not decided, and no dashboard route records a note (recordNote has no caller).
  • Minutes. The minutes in a note are what the owner reports, not measured; reading time is an estimate at 175 words a minute.
  • Zone changes. Slots compare as local date and time in one zone; changing the commission's zone between batches is not handled.

Composition

  • Depends on: Branch health (readBranchHealth, openedSince, HEALTH_REASONS, runUrl, types), Portfolio (the registry and current conditions), Required actions and blockers (attentionItems, for the owner-required count) and the Command journal.
  • Used by: the Dashboard: GET /api/digest reads readOwnerDigest on a read-only journal, and the Digest page shows it, its sources folded away. Nothing in src/ forms a batch or digest.

Changing it safely

What must stay true

  • The digest's text is a pure function of its stored input: a change to renderDigest makes every stored digest fail its re-validation, so a rendering change is a new protocol, never an edit in place.
  • Watermarks count branch-health event sequences: a change to branch health's event types or grouping must keep each episode in exactly one batch.
  • The 150-word limit, quiet hours and three batches are the bet's acceptance; never relax them without the owner.

Tests and helpers

Focused tests: node --test test/owner-digest.test.ts test/dashboard-digest.test.ts test/branch-health-adversarial.test.ts, then test/branch-health.test.ts. Every change also needs the steps in AGENTS.md's documentation obligations.

FileWhat it covers
test/owner-digest.test.tsCommission, batching, gaps, digest wording and limits, asks, notes, replay and stored corruption
test/dashboard-digest.test.tsRecorded DTOs and citations, read-only GET/HEAD, rejected commands and corrupt-state responses
test/branch-health-adversarial.test.tsDigest freshness, incomplete ask coverage, long asks and invalid stored input
apps/dashboard/test/digest-render.test.tsLoading/reload/error presentation, batch gaps, links and folded sources

The continuation's independent probes check exact blocker source matching, omitted unavailable mirrors, unreadable poll gaps and refusal of /1 without writing. Their paths and source hashes are in the implementation review above. Browser evidence is separate from these rendered tests; none establishes scheduling or delivery.

Source: docs/agents/owner-digest.md