# Console voice — Jollibee Rewards Operator Console

> **Scope:** every string an operator reads in `app.html`, `js/app.js`, `js/agent.js` and
> `js/simulations.js`. Not the dev harness, not code comments, not this folder's docs.
>
> **Status:** written alongside the prototype, 2026-08-24. The member app's voice went eleven strings
> and four months before anyone wrote it down; this one starts with the build.

## The member app's voice does not apply here

[`../prototypes/member/VOICE.md`](../prototypes/member/VOICE.md) governs strings a **member** reads. It exists to keep
spec language off a consumer screen. **This console is the opposite case**, and its word list
inverts: the words that file bans are an operator's correct vocabulary.

An operator is a professional whose job is this system. Softening *"refused — low confidence (0.31)"*
into *"hmm, we couldn't read that one"* takes away the number they need and tells them nothing. The
member gets warmth because they did not ask to learn a ledger. The operator did.

| The member app says | This console says | Why |
|---|---|---|
| "on the way" | **pending** | An operator reconciles a ledger. "On the way" is not a state you can filter on |
| "we'll add your points" | **credit** · **settle** | |
| "we're checking your receipt" | **validation** · **the feed match** | |
| "we couldn't read that one" | **refused — low confidence (0.31)** | A number is more use than sympathy |
| "points arrive" | **T+1** · **settlement window** | |
| "show your code" | **presentation** | |
| — | **dedup key** · **band** · **holdout** · **breakage** · **append-only** | Spec words. Correct here, and needed |
| the **crew** | the **crew** | *Kept.* Jollibee's own word, right in both registers |

## The register

**Precise, dense, unhedged.** Warmth is not the goal; being unambiguous at speed is. PRD-0007 is
explicit that on this surface *"brand matters less than density and scannability"* — so the Jollibee
palette, logo and type all stay, and the prose gets out of the way.

Six rules:

1. **Lead with the number or the noun.** "8 scans held past T+1", not "There are currently a number
   of scans awaiting review."
2. **Name the thing exactly.** `voucherExpiry`, `JB0142`, `SI 0100024902`, `0.78`. An operator
   searches on these; a paraphrase is a dead end.
3. **State the consequence, not the reassurance.** "The 412 vouchers already issued keep their
   30-day expiry" beats "Don't worry, existing vouchers are safe."
4. **Say what is unknown, as a value.** *unset*, *not configured*, *unspecified* — never blank, never
   a zero standing in for a decision nobody made. A blank reads as "fine"; **unset** reads as "someone
   has to choose."
5. **A refusal gives the ceiling, not an apology.** "The most I can deduct is 120." One exception,
   and it is a rule not a style choice — see below.
6. **No hedging, no filler.** Cut *just*, *simply*, *please note*, *feel free to*. If a sentence
   survives deletion, delete it.

## No product notes on screen

**Nothing in the console may reference the documents that specified it.** No PRD numbers, no story
numbers, no `[confirm]` tags, no open-question numbers, no `A5`, no "net-new", no "Prism". Those are
notes to the people building this; an operator reading them learns nothing and loses trust in
everything around them.

Where a spec fact is genuinely useful to an operator, **say the fact, not the citation**:

| Don't write | Write |
|---|---|
| "Analysis belongs to Prism — PRD-0007 says of this console, 'Not this console.'" | *nothing.* If there are no charts, the absence needs no defending |
| "A5 — blocks a real launch" | "Reward cost is outside target" |
| "Derived from the distribution of the scans this console holds — not from a calibrated model" | "Suggested from recent scans. Review the numbers before applying." |
| "Band authority is narrower than catalog authority — a loosened band is an unlogged fraud decision" | "Changing thresholds needs fraud-tuning authority, which this role does not have." |
| "Retention is bounded below by the PH Data Privacy Act" | "Data-privacy rules set a minimum this cannot go below." |
| "Out-of-scope subjects are absent from results, not shown and refused (7.4.2)" | "You are seeing the 2 stores Fremont Foods owns." |

**This is enforced, not trusted.** The verification driver sweeps every view — accordions and
disclosures opened — for that vocabulary, and fails the build if any of it reaches a screen.

## Headers and counts

**Title + count + one helper line**, on every section of every page.

- **Title is a plain noun** for what is in the section — *Rewards*, *Members*, *Activity*,
  *Waiting on a decision*. Not a label for the container (*The record*, *Queue*, *Listings*).
- **Count is `<numeral> <noun>`, pluralised properly** — "5 modules", "1 match", "12 listings",
  "27 settings". Never `item(s)`. Never a spelled-out number beside a numeral elsewhere on the page.
  Use `Store.plural()`; do not assemble one by hand.
- **Helper line says what the section is for**, in one sentence, under the header.

The same rule governs a proposal card header, because it is a header: *"Proposed change · 5 modules"*.

## Helper text

**Every view gets one line under its title, and every field gets one line under its label.** They
answer *what is this for*, in the operator's own words — never *how it was specified*.

- **One sentence.** If it needs two, the label is wrong.
- **Say what it does, then what changes if you touch it.** "How long a claimed reward stays valid."
- **Don't restate the label.** "Voucher expiry — the expiry of the voucher" is worse than nothing.
- **Reference detail is not helper text.** Permitted range and shipped default belong behind the
  row's disclosure, not in the line an operator reads every time.

## Two rules carry over from the member app unchanged

Neither is about warmth, which is why they survive the inversion:

- **Status never rides on colour alone.** Icon plus words, every time.
- **A refusal states its actual reason.** A duplicate and a blurry photo are different events — and
  so are "outside your award cap" and "would take the balance below zero".

## Spec voice, not just spec citations

Removing the citations is the easy half. The harder half is the **register** — a sentence that quotes
a PRD's reasoning without naming it. This one shipped in a proposal card and read as a note to the
author, not guidance to the operator:

> *"No points are credited. Aileen Bautista is told the receipt could not be used, and the reason is
> recorded — **a member refused by a model is owed a reason a human can defend.**"*

**A spec sentence argues. An operator sentence informs.** If a line explains *why the product was
designed this way*, cut it. If it says *what happens when they press the button*, keep it.

| Argues — cut | Informs — keep |
|---|---|
| "a member refused by a model is owed a reason a human can defend" | "your reason goes on the record" |
| "a measurement obligation, not an ordinary setting" | "its results can be compared against the rest" |
| "the ledger is append-only" | "the balance is not edited — the original entry stays" |
| "never silently retroactive" | "nothing already settled changes" |
| "held scans need somewhere to sit" | "flagged receipts wait here for a decision" |

Banned outright, because each is a PRD phrase wearing plain clothes: *append-only*, *measurement
obligation*, *never silent*, *point of record*, *not a verdict*, *the ledger is*, *hold-don't-refuse*,
*owed a reason*, *a human can defend*.

**One exception: "Prism" stays.** It is the name of a real product an operator may be sent to, so the
analytics refusal keeps it. It is banned in effect lines, allowed in refusals.

## The one place to be deliberately vaguer

**Out-of-scope subjects.** Story 7.4.2 requires that they be *absent from results, not shown and
refused*. In conversation that means the console must **not distinguish** between:

- this member does not exist, and
- this member exists but is outside your scope.

One message serves both — *"No member found for 0917 555 0134."* — and it must **never** add *"you
don't have access to them."* That phrase confirms the member exists, which is exactly the disclosure
the rule prevents. Being more specific here would be a leak, not a courtesy.

The lookup **is** recorded, which is what makes repeated probing visible.

## The assistant's own voice

First person, brief, and honest about its limits. It is a tool reporting what it will do, not a
persona.

- **Proposing:** no preamble. The card does the talking.
- **Refusing:** the reason, then the nearest thing it can do. *"That's a Prism question — this
  console holds the configuration, not the analysis. I can show you the fraud bands behind it."*
- **Applying:** what happened and where to verify it. *"Applied. config.set — Voucher expiry. See the
  audit entry."*
- **Never:** "Certainly!", "I'd be happy to", "Great question", an emoji, or an exclamation mark.
- **Never claims to have done something it proposed.** The past tense is only for what is applied.

Its name is **Ops Assistant**. Not "AI Copilot", not a sparkle icon — an internal tool for people who
do this all day, named for what it does.

## Before you commit a string

1. Would an operator search on this wording, or paraphrase it? If they'd paraphrase, use their word.
2. Does it state the consequence, or reassure about it?
3. Is anything unknown shown **as unset**, rather than as blank or zero?
4. Does a refusal give the actual reason — and, if the subject is out of scope, does it give away
   that the subject exists?
