/* Parley — one fixture, read by every screen. A record that appears on two
   screens is this entry, read twice; nothing is written a second time to match.

   The narrative is authored against the entities. If a turn says a file did not
   index, that file's status is `failed` or `unsupported` here; if a turn says
   the figures are missing, the spreadsheet is still `indexing`. Re-read the
   record before changing what a message claims about it. */

const PARLEY_NOW = new Date(2026, 7, 24, 13, 40).getTime(); /* Monday 24 August 2026, 13:40 */
const ago = (days, hh, mm) => { const d = new Date(PARLEY_NOW); d.setDate(d.getDate() - days); d.setHours(hh, mm, 0, 0); return d.getTime(); };

/* `plan` is an id from PLANS in enums.js — the label and every rate come from
   there, so no screen types a price. `seats` is what the workspace PAYS for;
   what it USES is derived in metrics.js from the people plus the invites still
   standing, and the two are equal at the moment, which is why inviting anyone
   else is blocked until a seat comes back. */
const WORKSPACE = {
  name: "Northgate", plan: "studio", seats: 10,
  cycleStart: new Date(2026, 7, 1).getTime(), cycleEnd: new Date(2026, 7, 31, 23, 59).getTime(),
  includedTokens: 1200000, retentionDays: 0, ssoRequired: false, spendCapCents: 40000,
  startedAt: new Date(2025, 9, 14).getTime(),
};

/* `role` is the job title a colleague would recognise; `access` is the
   permission level, an id from ACCESS_LEVELS. `lastSeenAt` is what the dormant
   predicate reads — one person here has not signed in since June, and they are
   holding one of the ten seats. */
const PEOPLE = [
  { id: "u_nils", name: "Nils Sørensen", initials: "NS", avatar: "assets/avatar-nils.jpg", email: "nils@northgate.co", role: "Product lead", access: "owner", joinedAt: new Date(2025, 9, 14).getTime(), lastSeenAt: ago(0, 13, 38) },
  { id: "u_priya", name: "Priya Raghunathan", initials: "PR", email: "priya@northgate.co", role: "Staff engineer", access: "admin", joinedAt: new Date(2025, 9, 20).getTime(), lastSeenAt: ago(0, 11, 6) },
  { id: "u_ruth", name: "Ruth Adeyemi", initials: "RA", email: "ruth@northgate.co", role: "Finance", access: "admin", joinedAt: new Date(2025, 10, 3).getTime(), lastSeenAt: ago(1, 15, 20) },
  { id: "u_theo", name: "Theo Marchetti", initials: "TM", email: "theo@northgate.co", role: "Lifecycle marketing", access: "member", joinedAt: new Date(2025, 11, 8).getTime(), lastSeenAt: ago(0, 10, 12) },
  { id: "u_ivo", name: "Ivo Brandt", initials: "IB", email: "ivo@northgate.co", role: "Support lead", access: "member", joinedAt: new Date(2026, 0, 19).getTime(), lastSeenAt: ago(1, 16, 40) },
  { id: "u_hana", name: "Hana Iwasaki", initials: "HI", email: "hana@northgate.co", role: "Designer", access: "member", joinedAt: new Date(2026, 1, 2).getTime(), lastSeenAt: ago(3, 9, 15) },
  { id: "u_marek", name: "Marek Duda", initials: "MD", email: "marek@northgate.co", role: "Data analyst", access: "member", joinedAt: new Date(2026, 3, 27).getTime(), lastSeenAt: ago(6, 17, 5) },
  { id: "u_sam", name: "Sam Okonjo", initials: "SO", email: "sam@northgate.co", role: "Contract writer", access: "member", joinedAt: new Date(2026, 2, 11).getTime(), lastSeenAt: ago(71, 11, 0) },
];
const PARLEY_USER = PEOPLE[0];

/* Invites. Whether one is still standing is DERIVED from `expiresAt` against
   the clock — never stored, never an enum. A standing invite holds a seat; an
   expired one does not, which is the difference the seat figure turns on. */
const INVITES = [
  { id: "i_lena", email: "lena@northgate.co", access: "member", invitedBy: "u_nils", at: ago(2, 10, 15), expiresAt: ago(-5, 10, 15), note: "Starting on the lifecycle work in September." },
  { id: "i_omar", email: "omar@northgate.co", access: "admin", invitedBy: "u_nils", at: ago(0, 9, 50), expiresAt: ago(-7, 9, 50), note: "Taking over the board pack from October." },
  { id: "i_dev", email: "dev@fieldworks.io", access: "member", invitedBy: "u_priya", at: ago(9, 9, 0), expiresAt: ago(2, 9, 0), note: "Contractor on the events migration." },
];

/* Issued invoices — stored records, one per period actually billed. The
   current cycle is NOT here: it has not been invoiced yet, so the billing
   screen derives it from the same turns the usage screen reads. */
const INVOICES = [
  { id: "in_0044", number: "NG-0044", periodStart: new Date(2026, 6, 1).getTime(), periodEnd: new Date(2026, 6, 31).getTime(), seats: 10, seatCost: 24000, usageCost: 1180, status: "failed", issuedAt: new Date(2026, 7, 1, 6, 0).getTime(), paidAt: null, statusNote: "The card on file expired on 31 July, and nothing has been retried since." },
  { id: "in_0043", number: "NG-0043", periodStart: new Date(2026, 5, 1).getTime(), periodEnd: new Date(2026, 5, 30).getTime(), seats: 9, seatCost: 21600, usageCost: 940, status: "paid", issuedAt: new Date(2026, 6, 1, 6, 0).getTime(), paidAt: new Date(2026, 6, 2, 9, 14).getTime(), statusNote: null },
  { id: "in_0042", number: "NG-0042", periodStart: new Date(2026, 5, 1).getTime(), periodEnd: new Date(2026, 5, 30).getTime(), seats: 9, seatCost: 21600, usageCost: 940, status: "void", issuedAt: new Date(2026, 6, 1, 5, 40).getTime(), paidAt: null, statusNote: "Issued twice for June. Withdrawn before it was charged and replaced by NG-0043." },
  { id: "in_0041", number: "NG-0041", periodStart: new Date(2026, 4, 1).getTime(), periodEnd: new Date(2026, 4, 31).getTime(), seats: 8, seatCost: 19200, usageCost: 620, status: "paid", issuedAt: new Date(2026, 5, 1, 6, 0).getTime(), paidAt: new Date(2026, 5, 1, 6, 2).getTime(), statusNote: null },
];

/* The card the failed invoice was charged to. Expiry is a stored date; whether
   it has passed is derived. */
const PAYMENT = { brand: "Visa", last4: "4491", expMonth: 7, expYear: 2026, holder: "Northgate Studio Ltd", addedAt: new Date(2025, 9, 14).getTime() };

/* Files. `tokens` is what the file costs to carry once indexed — nothing that
   did not index carries anything, which is the whole argument for fixing the
   two that failed. */
const FILES = [
  { id: "f_emails", name: "onboarding-emails-v4.md", bytes: 41000, tokens: 9400, status: "ready", projectId: "p_lifecycle", uploadedBy: "u_theo", at: ago(9, 10, 12), note: "Six sends, the current live copy." },
  { id: "f_voice", name: "brand-voice.pdf", bytes: 1240000, tokens: 12800, status: "ready", projectId: "p_lifecycle", uploadedBy: "u_nils", at: ago(26, 15, 40), note: "The 2026 rewrite, including the subject-line rules." },
  { id: "f_fin", name: "q3-financials.xlsx", bytes: 486000, tokens: 0, status: "indexing", projectId: "p_board", uploadedBy: "u_nils", at: ago(0, 13, 5), note: "Eleven sheets. Large workbooks take a few minutes." },
  { id: "f_deck", name: "board-deck-q2.pdf", bytes: 3100000, tokens: 18600, status: "ready", projectId: "p_board", uploadedBy: "u_nils", at: ago(21, 9, 30), note: "Last quarter's pack, for structure." },
  { id: "f_zip", name: "interview-transcripts.zip", bytes: 24800000, tokens: 0, status: "unsupported", projectId: "p_research", uploadedBy: "u_priya", at: ago(6, 14, 20), note: "Twelve interviews.", statusNote: "Archives are not read. Unpack it and upload the transcripts." },
  { id: "f_survey", name: "churn-survey-responses.csv", bytes: 212000, tokens: 22400, status: "ready", projectId: "p_research", uploadedBy: "u_priya", at: ago(6, 14, 22), note: "38 completed responses, June to August." },
  { id: "f_nps", name: "nps-verbatims.csv", bytes: 96000, tokens: 0, status: "failed", projectId: "p_research", uploadedBy: "u_priya", at: ago(5, 11, 4), note: "Verbatim comments from the NPS run.", statusNote: "The whole file is one line — no delimiter the parser recognises." },
  { id: "f_pricing", name: "pricing-experiments-2025.md", bytes: 74000, tokens: 6200, status: "ready", projectId: null, uploadedBy: "u_nils", at: ago(14, 16, 50), note: "Every price test we ran last year and what it did." },
  { id: "f_bands", name: "seats-by-tier-2026.csv", bytes: 31000, tokens: 7400, status: "ready", projectId: null, uploadedBy: "u_nils", at: ago(4, 16, 18), note: "Every account, its tier and its seat count." },
];

const PROJECTS = [
  { id: "p_lifecycle", name: "Lifecycle copy", ownerId: "u_theo", visibility: "workspace", createdAt: ago(34, 11, 0), archivedAt: null, purpose: "Every automated email the product sends, and the voice they are written in.", instructions: "Write in the Northgate voice: plain sentences, no questions in subject lines, never more than one ask per email. Cite the voice guide when you make a call that comes from it." },
  { id: "p_board", name: "Q3 board pack", ownerId: "u_nils", visibility: "private", createdAt: ago(21, 9, 20), archivedAt: null, purpose: "The narrative and the numbers for the September board meeting.", instructions: "Follow the Q2 structure. Lead with what went wrong before what went right. Leave a blank rather than an estimate when a figure is not in the files." },
  { id: "p_research", name: "Churn interviews", ownerId: "u_priya", visibility: "workspace", createdAt: ago(12, 16, 10), archivedAt: null, purpose: "Twelve exit interviews and the survey that followed them.", instructions: "Themes before quotes. Say how many responses a theme rests on, and never merge two complaints because they sound alike." },
  { id: "p_pricing", name: "Pricing rework", ownerId: "u_nils", visibility: "private", createdAt: ago(58, 10, 0), archivedAt: ago(3, 17, 30), purpose: "The 2026 tier restructure, shelved until the churn work lands.", instructions: "Compare against the 2025 experiments before proposing anything new." },
];

const PROMPTS = [
  { id: "pr_brief", title: "Turn a rough note into a brief", category: "writing", authorId: "u_nils", uses: 41, lastUsedAt: ago(1, 9, 5), body: "Here is a rough note. Turn it into a one-page brief: the problem in two sentences, who it hurts, what we would ship, and what we would measure. Keep anything I wrote that is already sharp." },
  { id: "pr_plan", title: "Read an EXPLAIN plan", category: "code", authorId: "u_priya", uses: 27, lastUsedAt: ago(0, 11, 6), body: "Below is a Postgres EXPLAIN ANALYZE. Tell me which node is actually costing the time, whether it is an index problem or a query problem, and the smallest change that would fix it." },
  { id: "pr_themes", title: "Themes from open text", category: "research", authorId: "u_priya", uses: 18, lastUsedAt: ago(6, 15, 41), body: "Pull the themes out of the attached responses. Say how many responses each theme rests on. No quotes yet — I want the shape first." },
  { id: "pr_shorten", title: "Cut it by a third", category: "writing", authorId: "u_theo", uses: 33, lastUsedAt: ago(2, 14, 12), body: "Cut this by a third without losing a claim. Tell me what you removed and why it was safe to remove." },
  { id: "pr_digest", title: "Changelog into a digest", category: "ops", authorId: "u_nils", uses: 9, lastUsedAt: ago(11, 8, 42), body: "Turn these merged pull requests into three lines someone outside engineering would read and care about." },
  { id: "pr_risks", title: "Argue the other side", category: "research", authorId: "u_nils", uses: 6, lastUsedAt: ago(74, 10, 0), body: "Take the position I have just argued and make the strongest case against it. Do not hedge — if the counter-argument is weak, say that instead." },
];

/* ── Threads ───────────────────────────────────────────────────────────── */
/* `tokens` on a message is its own body — what it occupies in the window.
   What each turn COST is derived in metrics.js from the messages before it, so
   nothing here can disagree with the usage screen. */

const THREADS = [
  {
    id: "t_emails", title: "Onboarding sequence, v4", projectId: "p_lifecycle", ownerId: "u_nils",
    model: "corvus-l", visibility: "workspace", pinned: true, createdAt: ago(2, 9, 12),
    fileIds: ["f_emails", "f_voice"],
    messages: [
      { id: "m1", role: "user", at: ago(2, 9, 12), tokens: 48, blocks: [
        { t: "p", text: "Sequence v3 is at 18% open on email three and everything after it falls off a cliff. The live copy and the voice guide are attached. Where is it losing people?" },
      ] },
      { id: "m2", role: "assistant", model: "corvus-l", status: "complete", at: ago(2, 9, 13), tokens: 268, cites: ["f_emails", "f_voice"], blocks: [
        { t: "p", text: "Three things, in the order they cost you the most." },
        { t: "ol", items: [
          "Email three is the first one that asks for work. One and two are “here is a thing”; three is “go and connect your calendar”, and it arrives on day four, before anyone has a reason to want it connected.",
          "The subject lines change register at three. One and two are flat statements — “Your first project”. Three switches to a question. The voice guide lists questions in subject lines under the things we do not do.",
          "Four opens by repeating three's ask, so anyone who ignored three has already read four's first line and closed it.",
        ] },
        { t: "p", text: "The drop after three is mostly the same people never coming back rather than four being weak on its own. I would move the calendar ask later and give three something to show instead." },
      ] },
      { id: "m3", role: "user", at: ago(0, 9, 31), tokens: 22, blocks: [
        { t: "p", text: "Rewrite three and four on that basis. Subject lines under 45 characters." },
      ] },
      { id: "m4", role: "assistant", model: "corvus-l", status: "complete", at: ago(0, 9, 32), tokens: 402, feedback: "up", cites: ["f_emails"], blocks: [
        { t: "p", text: "Both rewritten. Three now shows what the workspace looks like once there is something in it; four is the calendar ask on its own, moved to day seven." },
        { t: "h", text: "Email three — “What your first week looked like”" },
        { t: "p", text: "Opens on the three numbers their own workspace can already show: projects opened, people invited, hours logged. One link, into the summary view. No ask." },
        { t: "h", text: "Email four — “Two minutes to your calendar”" },
        { t: "p", text: "States what connecting does in one sentence, says it takes two minutes, and stops. One link, into the connect screen." },
        { t: "p", text: "Neither uses a question as a subject line, and neither carries a second link." },
      ] },
      { id: "m5", role: "user", at: ago(0, 9, 46), tokens: 26, blocks: [
        { t: "p", text: "Four is still doing two jobs — it asks for the calendar and pitches the mobile app. Split the app out." },
      ] },
      { id: "m6", role: "assistant", model: "corvus-l", status: "complete", at: ago(0, 9, 48), tokens: 214, blocks: [
        { t: "p", text: "Split. Four is the calendar ask alone; the app becomes a sixth send a week later, where it has something to point at." },
        { t: "ul", items: [
          "Day 7 — Two minutes to your calendar. One link.",
          "Day 14 — Your week, on your phone. Shows the summary they have now seen twice.",
          "Nothing else moves; one, two, three and five keep their days.",
        ] },
        { t: "p", text: "That makes it six emails over fourteen days, and the last one is the only send with two links in it." },
      ] },
    ],
  },
  {
    id: "t_events", title: "Events table is slow on the range scan", projectId: null, ownerId: "u_nils",
    model: "corvus-l", visibility: "private", pinned: false, createdAt: ago(0, 11, 5),
    fileIds: [],
    messages: [
      { id: "m1", role: "user", at: ago(0, 11, 5), tokens: 126, blocks: [
        { t: "p", text: "Four seconds on a 40M row table for one week of events. Priya sent me the plan. Is this an index problem or a query problem?" },
        { t: "code", lang: "text", code: "Seq Scan on events  (cost=0.00..1284410 rows=214883 width=96)\n                    (actual time=0.31..3901.44 rows=208114 loops=1)\n  Filter: ((occurred_at >= '2026-08-17') AND (occurred_at < '2026-08-24')\n           AND (workspace_id = 4192))\n  Rows Removed by Filter: 39791886\nPlanning Time: 0.19 ms\nExecution Time: 3944.02 ms" },
      ] },
      { id: "m2", role: "assistant", model: "corvus-l", status: "complete", at: ago(0, 11, 7), tokens: 322, blocks: [
        { t: "p", text: "Index problem. The planner has no index that answers both predicates, so it reads all forty million rows and throws away 39.8M of them — that discard line is the four seconds." },
        { t: "code", lang: "sql", code: "CREATE INDEX CONCURRENTLY events_workspace_occurred_idx\n  ON events (workspace_id, occurred_at DESC)\n  INCLUDE (kind, actor_id);" },
        { t: "p", text: "Workspace first because it is the equality predicate; occurred_at second so the range walks the index instead of filtering after it. The INCLUDE columns are there so the common query never goes back to the heap. Build it CONCURRENTLY — on a table that size a plain CREATE INDEX holds a write lock for minutes." },
      ] },
      { id: "m3", role: "user", at: ago(0, 11, 24), tokens: 20, blocks: [
        { t: "p", text: "That took it to 120ms. Does it hurt the writes?" },
      ] },
      { id: "m4", role: "assistant", model: "corvus-l", status: "complete", at: ago(0, 11, 26), tokens: 176, blocks: [
        { t: "p", text: "A little, and predictably. Every insert now maintains one more B-tree, and this one is wide because of the INCLUDE columns." },
        { t: "ul", items: [
          "Inserts arrive in occurred_at order, so pages fill at the right edge rather than splitting in the middle — the cheap case.",
          "Budget roughly 5–8% on insert throughput and about 1.4 GB on disk at your row count.",
          "If ingest is already the bottleneck, drop the INCLUDE and take the heap fetch back; the scan stays under 200ms.",
        ] },
      ] },
    ],
  },
  {
    id: "t_board", title: "Board narrative, Q3", projectId: "p_board", ownerId: "u_nils",
    model: "corvus-l", visibility: "private", pinned: false, createdAt: ago(0, 13, 10),
    fileIds: ["f_deck", "f_fin"],
    messages: [
      { id: "m1", role: "user", at: ago(0, 13, 10), tokens: 54, blocks: [
        { t: "p", text: "Draft the narrative section. Same structure as Q2, which is attached. We missed expansion and I would rather say so in the first paragraph than have someone find it on slide nine." },
      ] },
      { id: "m2", role: "assistant", model: "corvus-l", status: "stopped", at: ago(0, 13, 12), tokens: 188, cites: ["f_deck"], blocks: [
        { t: "p", text: "q3-financials.xlsx is still indexing, so I have written the argument and left every figure as a blank for you to fill rather than guess at it." },
        { t: "h", text: "The quarter in one paragraph" },
        { t: "p", text: "Two things happened this quarter and only one of them was planned. New business landed at ⟨new ARR⟩ against a ⟨target⟩ plan, which is the third quarter running that the top of the funnel has held. The second is that expansion did not" },
      ] },
    ],
  },
  {
    id: "t_themes", title: "Churn interviews — the shape", projectId: "p_research", ownerId: "u_priya",
    model: "corvus-m", visibility: "workspace", pinned: false, createdAt: ago(6, 15, 40),
    fileIds: ["f_survey", "f_nps", "f_zip"],
    messages: [
      { id: "m1", role: "user", at: ago(6, 15, 40), tokens: 34, blocks: [
        { t: "p", text: "Survey responses and the transcript bundle are on the project. Pull the themes — no quotes yet, I want the shape first." },
      ] },
      { id: "m2", role: "assistant", model: "corvus-m", status: "complete", at: ago(6, 15, 41), tokens: 296, cites: ["f_survey"], blocks: [
        { t: "p", text: "Two of the three files did not make it into the index: interview-transcripts.zip is an archive, which I cannot open, and nps-verbatims.csv failed to parse. So this rests on the 38 survey responses in churn-survey-responses.csv and nothing else." },
        { t: "ol", items: [
          "Price is named by 21 of 38, but only 4 name it alone. It is almost always the second reason given after something else went wrong.",
          "Nobody could tell whether anything was working. 17 responses describe checking the dashboard and finding it either empty or unchanged.",
          "The person who bought it left. 9 responses mention a champion leaving, and 7 of those 9 also say nobody was trained on it.",
        ] },
        { t: "p", text: "The third theme is the one I would chase. It is small, but it is the only one where the cause sits entirely inside the first month." },
      ] },
      { id: "m3", role: "user", at: ago(6, 16, 2), tokens: 24, blocks: [
        { t: "p", text: "Two and three feel like the same complaint from different seats." },
      ] },
      { id: "m4", role: "assistant", model: "corvus-m", status: "complete", at: ago(6, 16, 3), tokens: 168, blocks: [
        { t: "p", text: "They overlap in 5 of the 38, so they are not the same complaint — merging them would let a theme claim 21 responses when the shared ground is five." },
        { t: "p", text: "The honest version is that both are downstream of the same thing: nobody could show the account a result inside the first month. That is a sentence for the summary, not a merged theme." },
      ] },
    ],
  },
  {
    id: "t_naming", title: "Naming the middle tier", projectId: null, ownerId: "u_nils",
    model: "corvus-s", visibility: "link", pinned: false, createdAt: ago(4, 16, 20),
    fileIds: ["f_pricing", "f_voice", "f_bands"],
    linkNote: "Shared with the pricing contractor. Anyone with the link can read it.",
    messages: [
      { id: "m1", role: "user", at: ago(4, 16, 20), tokens: 52, blocks: [
        { t: "p", text: "Three tiers: self-serve to 20 seats, then sales. They are called Starter, Growth and Enterprise, and nobody inside the company can say what Growth means. Last year's price tests and the voice guide are attached." },
      ] },
      { id: "m2", role: "assistant", model: "corvus-s", status: "complete", at: ago(4, 16, 21), tokens: 214, feedback: "down", cites: ["f_pricing", "f_voice"], blocks: [
        { t: "p", text: "Three sets, each consistent within itself." },
        { t: "ol", items: [
          "Solo · Team · Company",
          "Start · Scale · Enterprise",
          "Basic · Standard · Premium",
        ] },
        { t: "p", text: "The second keeps the most from the current names while removing the word people cannot define." },
      ] },
      { id: "m3", role: "user", at: ago(3, 10, 4), tokens: 28, blocks: [
        { t: "p", text: "These are all the same idea in different words. The problem is the middle tier itself, not the set." },
      ] },
      { id: "m4", role: "assistant", model: "corvus-s", status: "complete", at: ago(3, 10, 5), tokens: 236, cites: ["f_pricing"], blocks: [
        { t: "p", text: "Fair — I named the tiers instead of reading them. The experiments say the middle tier is not a size, it is a threshold: it is where a workspace gets shared permissions and an admin. Every test that moved the middle tier's conversion moved it by changing what it unlocked, not what it cost." },
        { t: "ul", items: [
          "Name it after the unlock, not the volume: Shared, or Admin, or Controls.",
          "If it has to be a size word, it should be the one the buyer already uses about themselves — the tests used “Team” and it beat “Growth” on click-through in both runs.",
          "Whatever it is called, the seat ceiling has to appear beside it. Half the confusion in the tests was people not knowing which tier they were already in.",
        ] },
      ] },
    ],
  },
  {
    id: "t_tokens", title: "Spacing scale migration", projectId: null, ownerId: "u_nils",
    model: "corvus-m", visibility: "private", pinned: false, createdAt: ago(5, 14, 2),
    fileIds: [],
    messages: [
      { id: "m1", role: "user", at: ago(5, 14, 2), tokens: 40, blocks: [
        { t: "p", text: "We are moving from an 8px base to a 4px base with half steps. Give me the mapping table and flag every current value that has no clean equivalent." },
      ] },
      { id: "m2", role: "assistant", model: "corvus-m", status: "failed", at: ago(5, 14, 3), tokens: 0, failedReason: "The model stopped responding after 41 seconds. Nothing was charged for this turn.", blocks: [] },
    ],
  },
  {
    id: "t_refunds", title: "Refund policy, annual mid-term", projectId: null, ownerId: "u_nils",
    model: "corvus-s", visibility: "private", pinned: false, createdAt: ago(9, 11, 20), archivedAt: ago(4, 9, 0),
    fileIds: [],
    messages: [
      { id: "m1", role: "user", at: ago(9, 11, 20), tokens: 96, blocks: [
        { t: "p", text: "Customer paid annually, cancels in month two, used one seat of ten. What does this actually commit us to?" },
        { t: "quote", text: "Annual plans are non-refundable. Where a customer cancels within the first 90 days, Northgate will refund the unused portion of the term at its discretion, less any seats activated during the term." },
      ] },
      { id: "m2", role: "assistant", model: "corvus-s", status: "complete", at: ago(9, 11, 21), tokens: 202, blocks: [
        { t: "p", text: "It commits you to nothing, and that is the problem with the clause rather than the case. “At its discretion” means the refund is a decision, not an entitlement — so whatever you do here becomes the precedent someone quotes back next time." },
        { t: "p", text: "The trap is “less any seats activated during the term”. Read strictly, activating ten seats on day one and using one of them removes the entire refund. If that is not what you mean, the clause needs to say activated-and-used, and this customer is the reason to change it now rather than after the next one." },
      ] },
    ],
  },
  {
    id: "t_digest", title: "Weekly digest from the changelog", projectId: null, ownerId: "u_nils",
    model: "corvus-m", visibility: "private", pinned: false, createdAt: ago(11, 8, 40), archivedAt: ago(1, 17, 10),
    fileIds: [],
    messages: [
      { id: "m1", role: "user", at: ago(11, 8, 40), tokens: 78, blocks: [
        { t: "p", text: "Three lines someone outside engineering would care about, from this week's merges." },
        { t: "code", lang: "text", code: "#4188 fix: debounce workspace switcher\n#4190 feat: bulk archive in the projects table\n#4193 perf: events range scan uses new composite index\n#4195 chore: bump base image\n#4196 fix: avatar fallback initials on two-word surnames" },
      ] },
      { id: "m2", role: "assistant", model: "corvus-m", status: "complete", at: ago(11, 8, 42), tokens: 142, blocks: [
        { t: "ul", items: [
          "Archiving projects is now a bulk action — select the rows and archive them in one go, instead of one at a time.",
          "The activity view loads in a fraction of a second on large workspaces. It used to take four.",
          "Two small fixes: switching workspaces no longer flickers, and two-word surnames get the right initials.",
        ] },
      ] },
    ],
  },

  /* Two threads owned by other people and shared into the workspace. Nils can
     read them and branch from them; he cannot reply. Both are authored against
     the same entities the rest of the fixture holds: the subject lines follow
     the six-send rewrite in t_emails and land after it, and the seat bands come
     from seats-by-tier-2026.csv, which is indexed. */
  {
    id: "t_subjects", title: "Subject lines for the six-send version", projectId: "p_lifecycle", ownerId: "u_theo",
    model: "corvus-m", visibility: "workspace", pinned: false, createdAt: ago(0, 10, 10),
    fileIds: ["f_emails", "f_voice"],
    messages: [
      { id: "m1", role: "user", at: ago(0, 10, 10), tokens: 44, blocks: [
        { t: "p", text: "Nils's rewrite makes three a show-then-ask and moves the calendar to day seven, with the app as a sixth send on day fourteen. I need subject lines for all six, under 45 characters, and not one of them a question." },
      ] },
      { id: "m2", role: "assistant", model: "corvus-m", status: "complete", at: ago(0, 10, 11), tokens: 238, cites: ["f_emails", "f_voice"], blocks: [
        { t: "p", text: "Six, every one a statement. The longest is 31 characters." },
        { t: "ol", items: [
          "Day 0 — “Your first project” · 18",
          "Day 2 — “Where your work lives” · 21",
          "Day 4 — “What your first week looked like” · 31",
          "Day 7 — “Two minutes to your calendar” · 28",
          "Day 10 — “The three things people miss” · 28",
          "Day 14 — “Your week, on your phone” · 24",
        ] },
        { t: "p", text: "Three and six are Nils's own lines, kept as they are. Day 10 is the only send whose body has not been rewritten yet, so treat its line as provisional." },
      ] },
    ],
  },
  {
    id: "t_seats", title: "Accounts one seat under a tier line", projectId: null, ownerId: "u_ruth",
    model: "corvus-s", visibility: "workspace", pinned: false, createdAt: ago(1, 15, 20),
    fileIds: ["f_bands"],
    messages: [
      { id: "m1", role: "user", at: ago(1, 15, 20), tokens: 42, blocks: [
        { t: "p", text: "seats-by-tier-2026.csv is attached. Which accounts sit one seat under a tier line, and what happens to them if they cross it?" },
      ] },
      { id: "m2", role: "assistant", model: "corvus-s", status: "complete", at: ago(1, 15, 21), tokens: 206, cites: ["f_bands"], blocks: [
        { t: "p", text: "Nineteen accounts are one seat under a line, and they split into two groups that mean completely different things." },
        { t: "ul", items: [
          "Fourteen sit at 19 or 20 seats, one under the self-serve ceiling. Crossing it hands them to sales, and the pricing rework has not settled what that costs yet.",
          "Five sit at 2 seats, one under the Solo ceiling. Crossing it moves them onto Studio, at double the per-seat price.",
        ] },
        { t: "p", text: "One caution: nothing in the file says whether a seat is in use or merely paid for, so all nineteen are counts of paid seats. The fourteen at the self-serve ceiling are the ones worth a conversation before the tier names change under them." },
      ] },
    ],
  },
];

/* ── What the assistant says next ──────────────────────────────────────── */
/* Sending is a real commit — the reply is written into the fixture, so the
   context meter, the usage figures and the thread list all move. Each thread
   carries the turn that would honestly follow it; anything else gets a reply
   that works from what was actually typed and what is actually in context. */

const NEXT_REPLY = {
  t_emails: [
    { t: "p", text: "Taking that as the brief for the next pass — the sequence is six sends over fourteen days now, and only the last one carries two links." },
    { t: "p", text: "The open question is still email one. It is the only send with no measurable action in it, which makes it the one I would test cutting entirely before adding anything else." },
  ],
  t_events: [
    { t: "p", text: "One more thing worth doing before you call it finished: check pg_stat_user_indexes in a week. If the old single-column index on occurred_at has zero scans by then, drop it — you are paying to maintain two indexes where the composite answers both." },
  ],
  t_board: [
    { t: "p", text: "Picking up from where that stopped. The second thing is that expansion did not follow new business — it is the first quarter in five where the two moved in opposite directions, and the pack should say which of the two you think is the anomaly." },
  ],
  t_naming: [
    { t: "p", text: "Then the seat ceiling is the name. Call the tiers by what they hold — up to 20, then Company — and the middle tier stops needing a word that means growth." },
  ],
  t_refunds: [
    { t: "p", text: "If you want the clause rewritten rather than interpreted, the smallest change that fixes this case is two words: “less any seats activated and used during the term”. Everything else in it can stand." },
  ],
};

const GENERIC_REPLY = (text, contextNote) => [
  { t: "p", text: "Before I answer that properly — here is what I have and what I am missing." },
  { t: "ul", items: [
    "You asked: “" + (text.length > 90 ? text.slice(0, 90).trim() + "…" : text.trim()) + "”",
    contextNote,
    "What would sharpen this: who reads the answer, and what you would do differently depending on it.",
  ] },
  { t: "p", text: "Say either of those and I will write the real thing rather than a version that hedges." },
];

Object.assign(window, { PARLEY_NOW, WORKSPACE, PEOPLE, PARLEY_USER, INVITES, INVOICES, PAYMENT, FILES, PROJECTS, PROMPTS, THREADS, NEXT_REPLY, GENERIC_REPLY });
