@vibes.diy/api-svc 14.3.87 → 14.3.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/evento-handler-manifest.js +0 -4
  2. package/evento-handler-manifest.js.map +1 -1
  3. package/intern/agent-assembly.js +1 -4
  4. package/intern/agent-assembly.js.map +1 -1
  5. package/intern/agent-belt.js +10 -10
  6. package/intern/agent-belt.js.map +1 -1
  7. package/intern/agent-dispatcher.js +16 -41
  8. package/intern/agent-dispatcher.js.map +1 -1
  9. package/intern/agent-router.js.map +1 -1
  10. package/intern/ensure-chat-id.d.ts +0 -2
  11. package/intern/ensure-chat-id.js +25 -57
  12. package/intern/ensure-chat-id.js.map +1 -1
  13. package/intern/ensure-slug-binding.js.map +1 -1
  14. package/intern/first-brief-pairs.d.ts +13 -0
  15. package/intern/first-brief-pairs.js +138 -0
  16. package/intern/first-brief-pairs.js.map +1 -0
  17. package/intern/first-brief.d.ts +4 -0
  18. package/intern/first-brief.js +1 -0
  19. package/intern/first-brief.js.map +1 -1
  20. package/intern/first-build-race.d.ts +2 -0
  21. package/intern/first-build-race.js +6 -0
  22. package/intern/first-build-race.js.map +1 -1
  23. package/intern/first-build-request.js +1 -3
  24. package/intern/first-build-request.js.map +1 -1
  25. package/intern/get-model-defaults.d.ts +1 -0
  26. package/intern/get-model-defaults.js +2 -1
  27. package/intern/get-model-defaults.js.map +1 -1
  28. package/intern/guest-principal-store.js +2 -31
  29. package/intern/guest-principal-store.js.map +1 -1
  30. package/intern/instant-build.d.ts +154 -0
  31. package/intern/instant-build.js +443 -0
  32. package/intern/instant-build.js.map +1 -0
  33. package/intern/instruction-variant.d.ts +4 -13
  34. package/intern/instruction-variant.js +13 -153
  35. package/intern/instruction-variant.js.map +1 -1
  36. package/intern/mercury-first-build-seam.d.ts +2 -41
  37. package/intern/mercury-first-build-seam.js +4 -98
  38. package/intern/mercury-first-build-seam.js.map +1 -1
  39. package/intern/mercury-first-build.d.ts +34 -407
  40. package/intern/mercury-first-build.js +243 -2340
  41. package/intern/mercury-first-build.js.map +1 -1
  42. package/intern/mercury-resilience.d.ts +4 -11
  43. package/intern/mercury-resilience.js +4 -22
  44. package/intern/mercury-resilience.js.map +1 -1
  45. package/intern/mint-decision.d.ts +4 -0
  46. package/intern/mint-decision.js +11 -7
  47. package/intern/mint-decision.js.map +1 -1
  48. package/intern/mint-job-store.js +2 -2
  49. package/intern/mint-job-store.js.map +1 -1
  50. package/intern/onboarding-handle.d.ts +1 -0
  51. package/intern/onboarding-handle.js +1 -1
  52. package/intern/onboarding-handle.js.map +1 -1
  53. package/intern/prompt-assembly.d.ts +20 -0
  54. package/intern/prompt-assembly.js +12 -19
  55. package/intern/prompt-assembly.js.map +1 -1
  56. package/intern/provisional-handle.d.ts +8 -0
  57. package/intern/provisional-handle.js +56 -0
  58. package/intern/provisional-handle.js.map +1 -0
  59. package/intern/subagent-model.d.ts +0 -7
  60. package/intern/subagent-model.js +0 -6
  61. package/intern/subagent-model.js.map +1 -1
  62. package/package.json +13 -13
  63. package/public/agent-turn-action.js +18 -22
  64. package/public/agent-turn-action.js.map +1 -1
  65. package/public/app-setting-items.d.ts +2 -0
  66. package/public/app-setting-items.js +32 -2
  67. package/public/app-setting-items.js.map +1 -1
  68. package/public/codegen-turn-route.d.ts +9 -2
  69. package/public/codegen-turn-route.js +46 -20
  70. package/public/codegen-turn-route.js.map +1 -1
  71. package/public/ensure-app-settings.js +7 -7
  72. package/public/ensure-app-settings.js.map +1 -1
  73. package/public/get-app-by-fsid.js +2 -2
  74. package/public/get-app-by-fsid.js.map +1 -1
  75. package/public/get-chat-details.js +3 -4
  76. package/public/get-chat-details.js.map +1 -1
  77. package/public/get-chat-response.js +3 -4
  78. package/public/get-chat-response.js.map +1 -1
  79. package/public/handle-prompt-context.d.ts +3 -2
  80. package/public/handle-prompt-context.js +28 -76
  81. package/public/handle-prompt-context.js.map +1 -1
  82. package/public/handler-llm-request.d.ts +6 -1
  83. package/public/handler-llm-request.js +11 -9
  84. package/public/handler-llm-request.js.map +1 -1
  85. package/public/list-recent-vibes.js +1 -57
  86. package/public/list-recent-vibes.js.map +1 -1
  87. package/public/open-chat.js +1 -2
  88. package/public/open-chat.js.map +1 -1
  89. package/public/prompt-chat-section.js +2 -13
  90. package/public/prompt-chat-section.js.map +1 -1
  91. package/public/prompt-section-actions.d.ts +0 -2
  92. package/public/prompt-section-actions.js +24 -1
  93. package/public/prompt-section-actions.js.map +1 -1
  94. package/public/prompt-turn-terminal.js +1 -5
  95. package/public/prompt-turn-terminal.js.map +1 -1
  96. package/public/prompt-turn-types.d.ts +2 -0
  97. package/public/publish-app.js +2 -2
  98. package/public/publish-app.js.map +1 -1
  99. package/public/select-app.d.ts +1 -1
  100. package/public/select-app.js +1 -1
  101. package/public/select-app.js.map +1 -1
  102. package/public/shared-mint.js +1 -2
  103. package/public/shared-mint.js.map +1 -1
  104. package/public/stream-assembly.d.ts +5 -2
  105. package/public/stream-assembly.js +5 -2
  106. package/public/stream-assembly.js.map +1 -1
  107. package/usage-report/codegen-delivery-alert-util.d.ts +4 -4
  108. package/usage-report/codegen-delivery-alert-util.js +17 -17
  109. package/usage-report/codegen-delivery-alert-util.js.map +1 -1
  110. package/usage-report/first-build-no-app-alarm.js.map +1 -1
  111. package/usage-report/{draft-output-alert.d.ts → new-user-alert-post.d.ts} +0 -6
  112. package/usage-report/new-user-alert-post.js +14 -0
  113. package/usage-report/new-user-alert-post.js.map +1 -0
  114. package/usage-report/new-user-alert-util.d.ts +0 -1
  115. package/usage-report/new-user-alert-util.js +3 -16
  116. package/usage-report/new-user-alert-util.js.map +1 -1
  117. package/usage-report/new-user-alert.js +1 -6
  118. package/usage-report/new-user-alert.js.map +1 -1
  119. package/intern/attach-draft-handle.d.ts +0 -18
  120. package/intern/attach-draft-handle.js +0 -342
  121. package/intern/attach-draft-handle.js.map +0 -1
  122. package/intern/automatic-draft-attachment.d.ts +0 -18
  123. package/intern/automatic-draft-attachment.js +0 -31
  124. package/intern/automatic-draft-attachment.js.map +0 -1
  125. package/intern/draft-chat-store.d.ts +0 -99
  126. package/intern/draft-chat-store.js +0 -86
  127. package/intern/draft-chat-store.js.map +0 -1
  128. package/intern/draft-runtime-store.d.ts +0 -8
  129. package/intern/draft-runtime-store.js +0 -31
  130. package/intern/draft-runtime-store.js.map +0 -1
  131. package/public/attach-draft-handle.d.ts +0 -3
  132. package/public/attach-draft-handle.js +0 -53
  133. package/public/attach-draft-handle.js.map +0 -1
  134. package/public/draft-runtime.d.ts +0 -5
  135. package/public/draft-runtime.js +0 -302
  136. package/public/draft-runtime.js.map +0 -1
  137. package/usage-report/draft-output-alert.js +0 -24
  138. package/usage-report/draft-output-alert.js.map +0 -1
@@ -3,29 +3,19 @@ import { and, eq } from "drizzle-orm";
3
3
  import { type } from "arktype";
4
4
  import { lazyType } from "@vibes.diy/api-types/lazy-type";
5
5
  import { sanitizeRoomShortlist } from "@vibes.diy/prompts";
6
- import { KNOWN_VALUES_DIRECT_CONTROLS, ONE_TREE_PER_PERSON } from "./agent-session-instruction.js";
7
6
  import { keepsToThemselves as keepsToThemselvesWords } from "./keeps-to-themselves.js";
7
+ import { KNOWN_VALUES_DIRECT_CONTROLS } from "./agent-session-instruction.js";
8
8
  import { poufRoomCatalog, poufRoomSlugs } from "./pouf-room-catalog.js";
9
9
  import { vibeFontCatalog, vibeFontSlugs } from "./vibe-font-catalog.js";
10
10
  import { slugWordBag } from "./ensure-slug-binding.js";
11
11
  import { MERCURY_CAPABILITY_TRIGGERS } from "./mercury-capability-triggers.js";
12
12
  import { parseRetryAfterMs, readFailureBody, waitMs } from "../public/provider-errors.js";
13
- import { MERCURY_BREAKER, MERCURY_MIN_ATTEMPT_MS, MERCURY_STAGE_BUDGET_MS, MercuryCircuitBreaker, MercuryStageError, mercuryBudget, mercuryRetryWaitMs, mercuryTesterRetryWaitMs, MERCURY_TESTER_FANOUT, MERCURY_TESTER_REQUEST_DEADLINE_MS, MERCURY_TESTER_STAGE_BUDGET_MS, } from "./mercury-resilience.js";
13
+ import { MERCURY_BREAKER, MERCURY_MIN_ATTEMPT_MS, MercuryCircuitBreaker, MercuryStageError, mercuryBudget, mercuryTesterRetryWaitMs, MERCURY_TESTER_FANOUT, MERCURY_TESTER_REQUEST_DEADLINE_MS, MERCURY_TESTER_STAGE_BUDGET_MS, } from "./mercury-resilience.js";
14
14
  export const MERCURY_FIRST_BUILD_MODEL = "inception/mercury-2.5";
15
15
  export const MERCURY_FIRST_BUILD_REASONING = { effort: "none" };
16
- export const MERCURY_OPUS_PLANNER_MODEL = "~anthropic/claude-opus-latest";
17
- export const MERCURY_OPUS_PLANNER_DEADLINE_MS = 240_000;
18
- export const MERCURY_OPUS_PLANNER_BUDGET_MS = 300_000;
19
- export const MERCURY_OPUS_PLANNER_FANOUT = 1;
20
- export const MERCURY_OPUS_PLANNER_MAX_TOKENS = 8_000;
21
- export const MERCURY_OPUS_PLANNER_BREAKER = new MercuryCircuitBreaker();
22
16
  export const MERCURY_HAIKU_PLANNER_MODEL = "anthropic/claude-haiku-5.5";
23
- export const MERCURY_HAIKU_PLANNER_DEADLINE_MS = 40_000;
24
- export const MERCURY_HAIKU_PLANNER_BUDGET_MS = 50_000;
25
17
  export const MERCURY_HAIKU_PLANNER_BREAKER = new MercuryCircuitBreaker();
26
18
  function plannerModel(planner) {
27
- if (planner === "opus")
28
- return { id: MERCURY_OPUS_PLANNER_MODEL };
29
19
  if (planner === "haiku")
30
20
  return { id: MERCURY_HAIKU_PLANNER_MODEL, reasoning: { enabled: false } };
31
21
  return { id: MERCURY_FIRST_BUILD_MODEL, reasoning: MERCURY_FIRST_BUILD_REASONING };
@@ -33,9 +23,6 @@ function plannerModel(planner) {
33
23
  function plannerAskedModel(planner) {
34
24
  return plannerModel(planner ?? "mercury").id;
35
25
  }
36
- export const MERCURY_CALL_DEADLINE_MS = 8_000;
37
- export const MERCURY_CALL_ATTEMPTS = 2;
38
- export const MERCURY_TRANSIENT_RETRIES = 3;
39
26
  const MERCURY_TRANSIENT_STATUSES = new Set([429, 502, 503, 504]);
40
27
  class MercuryTransientError extends Error {
41
28
  status;
@@ -59,104 +46,12 @@ export const MERCURY_FLAGS = [
59
46
  "boardGame",
60
47
  "sensitive",
61
48
  ];
62
- export const MERCURY_BRIEF_FIELDS = ["opening", "access", "seed", "look", "pairs", "icon"];
63
- export const MERCURY_SHAPE_SYSTEM = `You classify a person's one-message request for a small web app before a build brief is written. Return only JSON.
64
-
65
- archetype, exactly one:
66
- - personal: one person's own tool ("for me", "my", "private", a tracker, a log, a list, a journal) with no other people named.
67
- - group: the person names other people who use it with them (we, our, friends, family, team, club, kids, housemates, partner, members, a foursome).
68
- - publish: the person writes and other people only read (a blog, menu, gallery, portfolio, schedule, zine, "everyone can read").
69
- - intake: strangers or customers submit something without signing up and the person sees the submissions (orders, bookings, requests, sign-ups, replies, reports from the public).
70
- - open: anyone with the link joins in and adds their own bit (a shared wall, canvas, feed, leaderboard game, community board, swap board, "anyone can post").
71
- - toy: a game, simulator, visual, sound or generator that keeps little or nothing (scores at most).
72
- Tie-breaks: group means other people open the app on their own devices; a calculator, splitter or tool used on one phone "with friends" at the table is personal. toy is for games, simulators and visual or sound toys; a tool that analyzes, estimates, designs, counts, tracks or logs stores records and is personal. A "feed of information" pulled from the internet is personal with backend true. A test or nonsense phrase is still a request: make the most playful literal app of it (a "squash bloom soothsayer" is a fortune teller about squash) and classify that.
73
-
74
- coreAction: the one thing the person does most in this app, as a short verb phrase in their own words ("tap once per glass", "paste the group chat", "lasso cows"). It becomes the biggest control on the first screen.
75
- kind: 2-6 word noun phrase for the app in the person's own nouns. who: who it is for, 2-8 words.
76
- recordNames: 1-5 short singular record type names the app stores (toys may have just "score" or none).
77
- flags, each true only when the request itself needs it:
78
- - ai: the app must read, write or judge free text or photos with a language model (extract facts from pasted words, answer questions, generate text, chat, analyze a picture, suggest recipes).
79
- - composer: ai is true AND the person types or pastes their own words or photos as the way records get in.
80
- - imageGen: the app generates pictures.
81
- - backend: server work the page cannot do: scheduled reminders or digests, a live feed pulled from outside, secret dealing, email.
82
- - calendar: dates on a calendar, bookings by slot, recurring events, paystub periods.
83
- - audio: sound or music playback or synthesis.
84
- - threeD: 3D graphics, a simulator, a world you move through.
85
- - charts: heatmaps, graphs, statistics views.
86
- - boardGame: a turn-based card or board game.
87
- - sensitive: health, money of real people, kids, or the word private.`;
88
- export function mercuryShapeSchema() {
89
- return {
90
- type: "object",
91
- additionalProperties: false,
92
- required: ["archetype", "coreAction", "kind", "who", "recordNames", ...MERCURY_FLAGS],
93
- properties: {
94
- archetype: { type: "string", enum: [...MERCURY_ARCHETYPES] },
95
- coreAction: { type: "string" },
96
- kind: { type: "string" },
97
- who: { type: "string" },
98
- recordNames: { type: "array", items: { type: "string" } },
99
- ...Object.fromEntries(MERCURY_FLAGS.map((f) => [f, { type: "boolean" }])),
100
- },
101
- };
102
- }
103
- export function mercuryShapeMessages(o) {
104
- return [
105
- { role: "system", content: [{ type: "text", text: MERCURY_SHAPE_SYSTEM }] },
106
- { role: "user", content: [{ type: "text", text: `Person's words: "${o.personWords}"` }] },
107
- ];
108
- }
109
- const shapeParsed = lazyType(() => type({
110
- archetype: type.enumerated(...MERCURY_ARCHETYPES),
111
- coreAction: "string",
112
- kind: "string",
113
- who: "string",
114
- recordNames: "string[]",
115
- ai: "boolean",
116
- composer: "boolean",
117
- imageGen: "boolean",
118
- backend: "boolean",
119
- calendar: "boolean",
120
- audio: "boolean",
121
- threeD: "boolean",
122
- charts: "boolean",
123
- boardGame: "boolean",
124
- sensitive: "boolean",
125
- }));
126
- export function parseMercuryShape(raw) {
127
- const p = shapeParsed(raw);
128
- if (p instanceof type.errors)
129
- return Result.Err(`mercury shape: ${p.summary}`);
130
- if (p.coreAction.trim().length === 0)
131
- return Result.Err("mercury shape: empty coreAction");
132
- return Result.Ok({
133
- archetype: p.archetype,
134
- coreAction: p.coreAction.trim(),
135
- kind: p.kind.trim(),
136
- who: p.who.trim(),
137
- recordNames: p.recordNames,
138
- flags: {
139
- ai: p.ai,
140
- composer: p.composer,
141
- imageGen: p.imageGen,
142
- backend: p.backend,
143
- calendar: p.calendar,
144
- audio: p.audio,
145
- threeD: p.threeD,
146
- charts: p.charts,
147
- boardGame: p.boardGame,
148
- sensitive: p.sensitive,
149
- },
150
- });
151
- }
152
49
  const COMPOSER_WORDS = /paste|photo|picture|own words|however they|repl(y|ies)|in their words|type in|jot/;
153
- const BACKEND_WORDS = /remind|notif|email|digest|schedul|every (day|week|morning|monday)|nag|feed|sync|server|secret|deal|webhook|cron|poll/;
154
50
  const PRIVATE_WORDS = /\b(private|just for me|only me|for myself)\b/;
155
- const MULTIPLAYER_WORDS = /\b(friends? can join|multiplayer|same world)\b/;
156
- export function fixMercuryShape(shape, personWords, opts = {}) {
51
+ const MULTIPLAYER_WORDS = /\b(friends? can join|multi-?players?|same world)\b/;
52
+ export function fixMercuryShape(shape, personWords) {
157
53
  const m = personWords.toLowerCase();
158
54
  const composer = shape.flags.ai && shape.flags.composer && COMPOSER_WORDS.test(m);
159
- const backend = shape.flags.backend && (opts.keepBackend === true || BACKEND_WORDS.test(m));
160
55
  const privateAsk = PRIVATE_WORDS.test(m);
161
56
  const keepsToThemselves = keepsToThemselvesWords(personWords);
162
57
  const keepsPersonal = keepsToThemselves && shape.archetype !== "toy" && shape.archetype !== "personal";
@@ -172,28 +67,25 @@ export function fixMercuryShape(shape, personWords, opts = {}) {
172
67
  .map((r) => r.trim())
173
68
  .filter((r) => r.length > 0)
174
69
  .slice(0, 5),
175
- flags: { ...shape.flags, composer, backend },
70
+ flags: { ...shape.flags, composer, threeD: shape.flags.threeD || mercuryBlockWorldNamed(personWords) },
176
71
  ...(keepsToThemselves ? { keepsToThemselves: true } : {}),
177
72
  };
178
73
  }
179
- const draftShapeParsed = lazyType(() => type({
74
+ const briefShapeParsed = lazyType(() => type({
180
75
  archetype: type.enumerated(...MERCURY_ARCHETYPES),
181
76
  coreAction: "string",
182
77
  recordNames: "string[]",
183
78
  capabilities: type.enumerated(...MERCURY_FLAGS).array(),
184
79
  }));
185
- export function mercuryBackendWordsNamed(personWords) {
186
- return BACKEND_WORDS.test(personWords.toLowerCase());
187
- }
188
- export function parseMercuryDraftShape(raw, personWords) {
189
- const p = draftShapeParsed(raw);
80
+ export function parseMercuryBriefShape(raw, personWords) {
81
+ const p = briefShapeParsed(raw);
190
82
  if (p instanceof type.errors)
191
83
  return Result.Err(`mercury classification: ${p.summary}`);
192
84
  if (p.coreAction.trim().length === 0)
193
85
  return Result.Err("mercury classification: empty coreAction");
194
86
  const caps = new Set(p.capabilities);
195
87
  const flags = Object.fromEntries(MERCURY_FLAGS.map((f) => [f, caps.has(f)]));
196
- return Result.Ok(fixMercuryShape({ archetype: p.archetype, coreAction: p.coreAction.trim(), kind: "", who: "", recordNames: p.recordNames, flags }, personWords, { keepBackend: true }));
88
+ return Result.Ok(fixMercuryShape({ archetype: p.archetype, coreAction: p.coreAction.trim(), recordNames: p.recordNames, flags }, personWords));
197
89
  }
198
90
  function setFlags(flags) {
199
91
  return MERCURY_FLAGS.filter((f) => flags[f]);
@@ -210,14 +102,7 @@ function splitMercurySlugs(v) {
210
102
  export function mercuryAddressEncoding() {
211
103
  const strings = (v) => Array.isArray(v) && v.every((t) => typeof t === "string");
212
104
  return {
213
- properties: { title: { type: "string" }, slugs: { type: "array", items: { type: "string" } } },
214
- rule: "The title is the app's name in Title Case, two or three words in its own vocabulary. The slugs are three short kebab-case addresses for that one app, as an array of three strings; each becomes a web address and they need not repeat the title.",
215
- ask: "title: the app's name, two or three words; slugs: an array of three short kebab-case addresses",
216
- hints: {
217
- title: "the app's name, two or three words in its own vocabulary",
218
- slugs: "three short kebab-case addresses for the app",
219
- },
220
- example: (title, slugs) => [`title: ${title}`, `slugs: ${JSON.stringify(slugs)}`],
105
+ rule: "The title is the app's name in Title Case, two or three words in its own vocabulary. The slugs are three short kebab-case addresses for that one app, comma-separated; each becomes a web address.",
221
106
  decode: (reply) => {
222
107
  const repairs = new Set();
223
108
  let title;
@@ -247,7 +132,6 @@ export function mercuryAddressEncoding() {
247
132
  return Result.Err("slugs-shape");
248
133
  return Result.Ok({ ...(title === undefined ? {} : { title }), slugs, repairs: [...repairs] });
249
134
  },
250
- salvage: (json) => (strings(json) && json.length === 3 ? { slugs: json } : undefined),
251
135
  };
252
136
  }
253
137
  export const MERCURY_ADDRESS = mercuryAddressEncoding();
@@ -256,559 +140,25 @@ export function mercurySlugWordsSentence(words) {
256
140
  ? ""
257
141
  : `Fold a different one of these words into each slug, picking the ones that best suit the app; the title stays in the app's own words: ${words.join(", ")}.`;
258
142
  }
259
- const MERCURY_SLOT_HINTS = {
260
- opening: "one sentence: what it is, who it is for, and the moment that makes it fun to open",
261
- access: "who owns the object, who else is let in, what a visitor sees; the archetype's shape",
262
- seed: "demo: a lively cast and where each one stands, in words: who is ahead, what is overdue, what just happened",
263
- look: "one scene: surface, palette, card shape, one gesture of mood",
264
- icon: "one object from the app's world",
265
- };
266
- export const MERCURY_OWN_WORLD_SEED_HINT = "demo: a lively cast and where each one stands, in words, in states this app's own records hold";
267
- function replyKey(f) {
268
- return f === "seed" ? "demo" : f;
269
- }
270
143
  export const MERCURY_ROOMS_HINT = "an array of 2 or 3 room slugs from the room list";
271
- export const MERCURY_ROOMS_ASK = `Also rooms: ${MERCURY_ROOMS_HINT}.`;
272
144
  export const MERCURY_FONT_HINT = "one font slug from the font list";
273
- export const MERCURY_FONT_ASK = `Also font: ${MERCURY_FONT_HINT}.`;
274
- export const MERCURY_CLASSIFY_HINTS = {
275
- archetype: "one of personal, group, publish, intake, open, toy: the ownership shape that fits the person's words",
276
- coreAction: "the one thing the person does most in this app, a short verb phrase in their own words",
277
- recordNames: "an array of 1 to 5 short singular record type names the app stores",
278
- capabilities: "an array naming only the capabilities this request needs, from the capability list; empty when none",
279
- };
280
- function withClassifyFirst(obj, classify, value) {
281
- if (!classify)
282
- return obj;
283
- return { ...Object.fromEntries(Object.keys(MERCURY_CLASSIFY_HINTS).map((k) => [k, value(k)])), ...obj };
284
- }
285
- export function mercuryAnswerSkeleton(fields, opts = {}) {
286
- const lead = (o) => withClassifyFirst(o, opts.classify === true, (k) => `<${MERCURY_CLASSIFY_HINTS[k]}>`);
287
- const obj = {};
288
- const recordsRules = opts.recordsRules === true;
289
- const seedHint = recordsRules
290
- ? MERCURY_RECORDS_SEED_HINT
291
- : opts.juice === "own-world"
292
- ? MERCURY_OWN_WORLD_SEED_HINT
293
- : MERCURY_SLOT_HINTS.seed;
294
- const screensHint = recordsRules ? MERCURY_RECORDS_SCREENS_HINT : MERCURY_DEPTH_SCREENS_HINT;
295
- for (const f of fields) {
296
- if (f === "pairs")
297
- for (const [k, hint] of Object.entries(MERCURY_ADDRESS.hints))
298
- obj[k] = `<${hint}>`;
299
- else
300
- obj[replyKey(f)] = `<${f === "seed" ? seedHint : MERCURY_SLOT_HINTS[f]}>`;
301
- }
302
- const withRecords = (o) => recordsRules ? withRecordsKeys(o, `<${MERCURY_RECORDS_HINT}>`, `<${MERCURY_RULES_HINT}>`) : o;
303
- if (opts.story === true || opts.preamble === true) {
304
- const outline = withRecords(opts.screens === true ? withScreensKey(obj, `<${screensHint}>`) : obj);
305
- const whole = withWholeKeys(outline, {
306
- ...(opts.story === true ? { story: `<${MERCURY_STORY_HINT}>` } : {}),
307
- ...(opts.preamble === true ? { preamble: `<${MERCURY_WHOLE_PREAMBLE_HINT}>` } : {}),
308
- });
309
- if (opts.rooms === true)
310
- whole.rooms = `<${MERCURY_ROOMS_HINT}>`;
311
- if (opts.font === true)
312
- whole.font = `<${MERCURY_FONT_HINT}>`;
313
- return JSON.stringify(lead(whole), null, 1);
314
- }
315
- const out = withRecords(opts.screens === true ? withScreensKey(obj, `<${screensHint}>`) : obj);
316
- if (opts.rooms === true)
317
- out.rooms = `<${MERCURY_ROOMS_HINT}>`;
318
- if (opts.font === true)
319
- out.font = `<${MERCURY_FONT_HINT}>`;
320
- return JSON.stringify(lead(out), null, 1);
321
- }
322
- const MERCURY_EXEMPLARS = [
323
- {
324
- archetype: "personal",
325
- opening: "A glaze test notebook for a home potter, where opening the kiln means a fresh tile to rate.",
326
- access: "Each signed-in person has their own notebook object (keyed on their handle, named for the app, renameable in settings); glazes and tiles carry the notebook id so a studio mate could be let in later. The creator reads and writes it alone.",
327
- demo: "demo: the owner's notebook with Tenmoku the favourite glaze, Shino just mixed and Celadon still untested; tiles fired recently, the Chun blue one the best yet, an Ash tile cracked last week and still waiting for a good one.",
328
- look: "Earthy studio: oatmeal background, iron-red accents, chunky rounded tile cards.",
329
- title: "Glaze Ledger",
330
- slugs: ["glaze-ledger", "kiln-tile-book", "glaze-notes"],
331
- icon: "a glazed test tile beside a small kiln",
332
- },
333
- {
334
- archetype: "group",
335
- opening: "A tool library for one neighbourhood block, shared by the households on it, where a returned ladder lights up for the next borrower.",
336
- access: "Each signed-in person starts with their own block library object. Households are let in with the platform's HandleInput invite screen, which writes a membership into the library; members read every tool and write their own loans. The library list shows shared ones with ViewerTag.",
337
- demo: 'demo: the library "Maple Street Tools" with the owner\'s own membership plus @priya and @tomas; the ladder out with @priya and due back soon, the tile saw overdue with @tomas, the pressure washer just returned and waiting for its next loan.',
338
- look: "Workshop pegboard: plywood tones, safety-orange accents, square tool cards with big status chips.",
339
- title: "Block Tools",
340
- slugs: ["block-tools", "maple-street-shelf", "borrow-board"],
341
- icon: "a hand drill inside a house outline",
342
- },
343
- {
344
- archetype: "publish",
345
- opening: "A taco truck menu board the owner updates from their phone and customers read, where a sold-out stamp lands the moment the last plate goes.",
346
- access: "The owner's menu object is keyed on their handle. Menu items are written by the owner and readable by everyone, signed in or not. Shaped so a co-owner could be let in later.",
347
- demo: "demo: the owner's menu across tacos, sides and drinks; the al pastor sold out just now, the horchata nearly gone, a new salsa added this morning, and today's note from the owner.",
348
- look: "Street-food bright: chalkboard dark with chalk-white type and chili-red price tags.",
349
- title: "Taco Today",
350
- slugs: ["taco-today", "truck-board", "menu-chalk"],
351
- icon: "a chalkboard with a taco drawn on it",
352
- },
353
- {
354
- archetype: "intake",
355
- opening: "An appointment book for a dog grooming salon: clients book without signing up, and the groomer watches the day fill in.",
356
- access: "Two sides. Clients book anonymously: anyone, signed in or not, can create a booking record on the salon object and reads back the confirmation they just made. The groomer, signed in as the salon's owner, reads and updates every booking.",
357
- demo: "demo: the owner's salon with bookings in every state; Biscuit checked in just now, Juno booked for later today, Pepper still waiting on confirmation, Moss cancelled yesterday.",
358
- look: "Clean salon: pale mint, charcoal type, generous slot buttons easy to tap one-handed.",
359
- title: "Groom Book",
360
- slugs: ["groom-book", "salon-slots", "paws-diary"],
361
- icon: "a dog brush beside a clock face",
362
- },
363
- {
364
- archetype: "open",
365
- opening: "A lost-and-found board for a music festival that anyone with the link can post to, where a found tag turns a post green.",
366
- access: "The owner creates the board and names it for the app. Any visitor with the link reads the whole board and writes their own posts; each poster edits or removes what they wrote, keyed on the record's creator. The board creator can remove anything.",
367
- demo: "demo: the festival board with posts from a handful of handles; @lin's lost phone found just now, @ari's jacket still missing since last night, a set of keys claimed this morning.",
368
- look: "Festival poster: cream paper, bold two-colour print, pinned-note cards with torn edges.",
369
- title: "Festival Finds",
370
- slugs: ["festival-finds", "lost-tent", "found-it"],
371
- icon: "a tent peg tied with a ribbon",
372
- },
373
- {
374
- archetype: "toy",
375
- opening: "A hex-grid pong game with a slow-motion ball, where a long rally sets the board glowing.",
376
- access: "Each signed-in player keeps a best score record keyed on their handle; a shared Top Scores list shows everyone's best with ViewerTag.",
377
- demo: "demo: score records from a small cast; @rin on top, the owner close behind and climbing, @bo just joined with no score yet, so the Top Scores list opens filled.",
378
- look: "Neon arcade: deep navy, electric cyan ball, glowing hex outlines.",
379
- title: "Hex Pong",
380
- slugs: ["hex-pong", "slow-rally", "honey-paddle"],
381
- icon: "a hexagonal paddle and a glowing ball",
382
- },
383
- ];
384
- export function mercuryFieldArchetypes(archetype) {
385
- return [archetype, ...MERCURY_ARCHETYPES.filter((a) => a !== archetype).slice(0, 2)];
386
- }
387
- const MERCURY_EXEMPLAR_CLASSIFICATION = {
388
- personal: { coreAction: "rate a fresh test tile", records: ["glaze", "tile"] },
389
- group: { coreAction: "borrow a tool", records: ["library", "membership", "tool", "loan"] },
390
- publish: { coreAction: "mark a dish sold out", records: ["menu", "menu item"] },
391
- intake: { coreAction: "book a grooming slot", records: ["salon", "booking"] },
392
- open: { coreAction: "post a lost or found item", records: ["board", "post"] },
393
- toy: { coreAction: "keep the rally going", records: ["score"] },
394
- };
395
- const MERCURY_EXEMPLAR_RECORDS = {
396
- personal: {
397
- records: "notebook (name); glaze (name, recipe, cone, notes); tile (glaze, fired on, rating, note)",
398
- rules: "each glaze shows its best-rated tile; a cracked tile never counts toward its glaze's rating",
399
- },
400
- group: {
401
- records: "library (name); membership (library, handle); tool (name, condition); loan (tool, borrower, due back, returned at)",
402
- rules: "a tool is out with one borrower at a time; returning it frees it for the next borrower",
403
- },
404
- publish: {
405
- records: "menu (name, today's note); menu item (name, price, section, sold out)",
406
- rules: "a sold-out item stays on the board, stamped, until the owner clears it",
407
- },
408
- intake: {
409
- records: "salon (name, hours); booking (dog, owner's name, service, slot, status)",
410
- rules: "one booking per slot; a cancelled booking frees its slot",
411
- },
412
- open: {
413
- records: "board (name); post (lost or found, item, where seen, note, found)",
414
- rules: "each poster edits only their own posts; a found tag turns the post green",
415
- },
416
- toy: {
417
- records: "score (player, best rally)",
418
- rules: "only each player's best rally is kept",
419
- },
420
- };
421
- function exemplarClassificationLine(a) {
422
- const c = MERCURY_EXEMPLAR_CLASSIFICATION[a];
423
- return `archetype: ${a}; core action: ${c.coreAction}; records: ${c.records.join(", ")}; capabilities: none`;
424
- }
425
- function exemplarBlock(e, address, classify = false, recordsRules = false) {
426
- const rr = MERCURY_EXEMPLAR_RECORDS[e.archetype];
427
- return [
428
- `## Example [${e.archetype}]`,
429
- "",
430
- ...(classify ? [`- ${exemplarClassificationLine(e.archetype)}`] : []),
431
- `- opening: ${e.opening}`,
432
- `- access: ${e.access}`,
433
- ...(recordsRules ? [`- records: ${rr.records}`, `- rules: ${rr.rules}`] : []),
434
- `- ${e.demo}`,
435
- `- look: ${e.look}`,
436
- ...address.example(e.title, e.slugs).map((line) => `- ${line}`),
437
- `- icon: ${e.icon}`,
438
- ].join("\n");
439
- }
440
- export function mercuryExemplarField(archetype, address = MERCURY_ADDRESS, recordsRules = false) {
441
- const byArchetype = new Map(MERCURY_EXEMPLARS.map((e) => [e.archetype, e]));
442
- const blocks = mercuryFieldArchetypes(archetype).flatMap((a) => {
443
- const e = byArchetype.get(a);
444
- return e === undefined ? [] : [exemplarBlock(e, address, false, recordsRules)];
445
- });
446
- return [
447
- `## How to read this field
448
-
449
- Each example below is one whole brief, tagged with the ownership archetype it fits:
450
-
451
- - [personal] one person's own tool: private, records keyed to their own object so a friend could be let in later.
452
- - [group] a named set of people (friends, family, team, club, housemates): the owner invites them by handle, members read and write; the invite screen is part of this first build.
453
- - [publish] the owner writes, everyone reads: a blog, menu, gallery, zine, schedule.
454
- - [intake] strangers submit without signing in (orders, bookings, requests, RSVPs); the owner sees and manages what came in.
455
- - [open] anyone with the link joins and writes their own bit: a shared wall, a canvas, a leaderboard game. Each person edits what they made.
456
- - [toy] a game or visual that keeps little or nothing: scores or a save slot at most.
457
-
458
- Derive the person's app the way the example with their archetype derives its own, and write each part without its tag. The examples' subjects, record names and cast belong to the examples alone; every noun comes from the person's words.`,
459
- ...blocks,
460
- ].join("\n\n");
461
- }
462
- export const MERCURY_ONECALL_TRIPLES = [
463
- ["personal", "group", "publish"],
464
- ["intake", "open", "toy"],
465
- ["group", "intake", "toy"],
466
- ["personal", "publish", "open"],
467
- ];
468
- const MERCURY_FIELD_TAGS = `- [personal] one person's own tool: private, records keyed to their own object so a friend could be let in later.
469
- - [group] a named set of people (friends, family, team, club, housemates): the owner invites them by handle, members read and write; the invite screen is part of this first build.
470
- - [publish] the owner writes, everyone reads: a blog, menu, gallery, zine, schedule.
471
- - [intake] strangers submit without signing in (orders, bookings, requests, RSVPs); the owner sees and manages what came in.
472
- - [open] anyone with the link joins and writes their own bit: a shared wall, a canvas, a leaderboard game. Each person edits what they made.
473
- - [toy] a game or visual that keeps little or nothing: scores or a save slot at most.`;
474
- export function mercuryOneCallField(draft, address = MERCURY_ADDRESS, recordsRules = false) {
475
- const byArchetype = new Map(MERCURY_EXEMPLARS.map((e) => [e.archetype, e]));
476
- const triple = MERCURY_ONECALL_TRIPLES[Math.abs(draft) % MERCURY_ONECALL_TRIPLES.length];
477
- const blocks = triple.flatMap((a) => {
478
- const e = byArchetype.get(a);
479
- return e === undefined ? [] : [exemplarBlock(e, address, true, recordsRules)];
480
- });
481
- return [
482
- `## How to read this field
483
-
484
- Each example below is one whole brief, tagged with the ownership archetype it fits; three of the six archetypes are shown:
485
-
486
- ${MERCURY_FIELD_TAGS}
487
-
488
- First decide which archetype fits the person's words, then derive their app the way the example with that archetype derives its own, or, when no example has it, the way the examples derive theirs, and write each part without its tag. The examples' subjects, record names and cast belong to the examples alone; every noun comes from the person's words.`,
489
- ...blocks,
490
- ].join("\n\n");
491
- }
492
- export const MERCURY_BRIEF_FLOORS = `Floors for every brief: each screen names at least two visible controls and one state that changes (a colour, a ribbon, a sort order, a chip, a badge); the demo names a lively cast and where each one stands (who is winning, what is overdue, what just landed, who has nothing yet); the opening names the one moment that makes the app fun to open. All of it in the person's own nouns and in words rather than numbers, and nothing their words did not imply.`;
493
- export const MERCURY_DEMO_RULE = `The demo part starts with "demo:" and names a lively cast and where each one stands, in words: who is ahead, what is overdue, what just happened, what is still waiting; dates and counts are the builder's to pick, so the demo uses words like recently, just now, last week, nearly, still. Every demo record is one the app's own Add could write.`;
494
- export const MERCURY_OWN_WORLD_DEMO_RULE = `The demo part starts with "demo:" and names a lively cast and where each one stands, in words, named in the app's own world: the states and progress this app's own records hold; dates and counts are the builder's to pick, so the demo uses words like recently, just now, last week, nearly, still. Every demo record is one the app's own Add could write.`;
495
- export const MERCURY_OWN_WORLD_BRIEF_FLOORS = `Floors for every brief: each screen names at least two visible controls and one state that changes (a colour, a ribbon, a sort order, a chip, a badge); the demo names a lively cast and where each one stands, in the app's own world and in states its own records hold; the opening names the one moment that makes the app fun to open. All of it in the person's own nouns and in words rather than numbers, and nothing their words did not imply.`;
496
- export const MERCURY_DEPTH_RULE = `Each screen also names one deeper interaction behind its main control, whichever this app's core action calls for: an undo, a long-press, a drag to reorder, an inline edit, or a filter chip.`;
497
- export const MERCURY_DEPTH_SCREENS_HINT = `1. the core action as the biggest control, two visible controls, one state that changes, one deeper interaction behind the main control (an undo, a long-press, a drag to reorder, an inline edit or a filter chip), why it exists; 2. ...; 3. ...`;
498
- export const MERCURY_RECORDS_RULE = `The records part names each record type the app stores, with its fields, as "volunteer (name, shift, note)". The rules part names the one to three computations or invariants the ask hinges on, in the person's terms — for example "close each tab as the fewest who-pays-whom transfers and save each payment", "a second sign-up under the same name updates that volunteer", "the band has a fixed lineup of five players and every player appears on each setlist", "every Friday email the summary to the coach's address". The screens build on the records and the rules.`;
499
- export const MERCURY_RECORDS_SCREENS_RULE = `The screens part names two or three screens; each one says what it shows from the records and what the person does there for this ask, and the screen that carries out a rule says so. At most one screen adds a deeper interaction, and only where the ask implies one.`;
500
- export const MERCURY_RECORDS_DEMO_RULE = `The demo part starts with "demo:" and is lively, in the app's own world: a small cast with names and the records the screens would show, in the states this app's own records hold. When the person typed data — items, amounts, names, a list — the demo carries every piece of it verbatim, quantities included ("half a bag" stays "half a bag"); dates and counts they did not give are the builder's to pick. Every demo record is one the app's own Add could write.`;
501
- export const MERCURY_ASK_WORDS_RULE = `Keep the ask's genre and capability words exactly as the person wrote them — 3D, first-person, email, a named recipient, a schedule, a fixed number of people — in the opening, the rules or the screens.`;
502
- export const MERCURY_RECORDS_BRIEF_FLOORS = `Floors for every brief: every record names its fields; the rules carry the computation or invariant the ask hinges on; each screen shows and does something this ask needs; the demo is lively and carries the person's own typed data word for word; the opening names the one moment that makes the app fun to open. All of it in the person's own nouns, and nothing their words did not imply.`;
503
- export const MERCURY_RECORDS_HINT = `each record type the app stores, with its fields: "name (field, field, field); name (field, field)"`;
504
- export const MERCURY_RULES_HINT = `the one to three computations or invariants the ask hinges on, in the person's terms`;
505
- export const MERCURY_RECORDS_SCREENS_HINT = `1. the core action's screen: what it shows from the records and what the person does there for this ask, and the rule it carries out; 2. ...; 3. ...`;
506
- export const MERCURY_RECORDS_SEED_HINT = "demo: a lively cast and the records the screens show, in the app's own world, with every item, amount and name the person typed, verbatim";
507
- export const MERCURY_CHOPPED_RECORDS_SEED_LINE = `- seed: a small, lively world in the app's own words — a cast with quoted names and the records the screens would show, in the states this app's own records hold; any data the person typed (items, amounts, names, quantities) appears in it verbatim. Every seeded record is one the app's own Add could write.`;
508
145
  const MERCURY_FIRST_PERSON_TRIGGER = " - a first-person walk around a place";
509
146
  export const MERCURY_BLOCK_WORLD_TRIGGER = " - a Minecraft-style block world to build and break blocks in";
510
- const MERCURY_BLOCK_WORLD_WORDS = /\b(minecraft|voxels?|block[- ]world|blocky|sandbox game)\b|\b(build|place|break|mine)\w*\b[^.]{0,30}\bblocks\b/i;
147
+ const MERCURY_BLOCK_WORLD_WORDS = /\b(minecraft|voxels?|block[- ]world|blocky|sandbox game)\b|\b(?:build|place|break|mine|dig)(?:s|ing)?(?:\s+(?:and|or|&)\s+(?:build|place|break|mine|dig)(?:s|ing)?)?\s+(?:the\s+|some\s+)?blocks\b/i;
511
148
  export function mercuryBlockWorldNamed(personWords) {
512
149
  return MERCURY_BLOCK_WORLD_WORDS.test(personWords);
513
150
  }
514
- const MERCURY_STORY_FOOD = /soup|sauce|ferment|recipe|potluck|beverage|dinner|grocer|food|menu|cook|kitchen|meal|drink|cafe|restaurant|bake|snack|taco/i;
515
- export const MERCURY_STORY_EXAMPLE_SOUP = "Mia opens the app to post today's soups by taking a picture of the kitchen recipe via the platform camera widget, right after the morning prep; she also sets the inventory level via an onscreen slider… customers at the queue browse the menu and make selections by clicking the quantity buttons on their phones.";
516
- export const MERCURY_STORY_EXAMPLE_BIKE = "Dev opens the app to post today's repair queue by taking a picture of each bike's tag via the platform camera widget, right after the shop opens; he also sets each repair's progress via an onscreen slider… riders waiting at the counter browse the queue and pick their pickup time by turning the time picker on their phones.";
517
- export function mercuryStoryExample(personWords) {
518
- return MERCURY_STORY_FOOD.test(personWords) ? MERCURY_STORY_EXAMPLE_BIKE : MERCURY_STORY_EXAMPLE_SOUP;
519
- }
520
- export function mercuryStoryRule(example) {
521
- return `The story part is a short scene of three to five sentences: one person from the demo cast uses the app through its core loop, and every step names its on-screen action: which control they use, the platform widget when one applies (the camera, a slider, a quantity stepper, a picker), and what changes on screen; then who else sees it and which control they use. The shape, shown for a different app: "${example}" That example's subject, cast, nouns and widgets belong to it alone; use a platform widget only when the person's app needs one. Every step of the story is something the screens offer, and the story stays inside the person's words.`;
522
- }
523
- export const MERCURY_STORY_HINT = "three to five sentences: one person from the demo cast using the app through its core loop, every step naming its on-screen action: which control they use, the platform widget if one applies (camera, slider, quantity stepper, picker), and what changes on screen; then who else sees it and which control they use";
524
- export const MERCURY_WHOLE_PREAMBLE_RULE = `The preamble part is spoken to the person who asked, just before the app is built: plain sentences in their own words, starting with "I'm making" or "I'm building" — what you are making for them, its core action and the big control on its first screen that does it, then what else it does, its features named in their nouns. Make it vivid, like you can already see the app; it promises what the screens and the story carry.`;
525
- export const MERCURY_WHOLE_PREAMBLE_HINT = `plain sentences to the person, starting with "I'm making" or "I'm building": what it is for them, the core action and its first-screen control, then what else it does`;
526
- const ACCESS_SENTENCE = "The access part matches the archetype";
527
- function briefRulesText(demoRule, floors) {
528
- return `You write the settled parts of a build brief for a small web app (React, a local-first database, access rules). The brief is read by a builder, who works out the records, screens and mechanics from the person's own words. Write plain, concrete prose in the app's own nouns, as unformatted text in each JSON string.
529
-
530
- The decisions already made are given in the user message; follow them (archetype, record names, capabilities). The access part matches the archetype: who owns the object, who else is let in, what visitors see. ${demoRule} Take from the field the SHAPE of a part (its length and how a cast, a look or a title is named); every noun comes from the person's words, and the app stays the one they asked for. ${MERCURY_ADDRESS.rule}
531
-
532
- ${floors}`;
151
+ const MERCURY_REAL_TIME_GAME_WORDS = /\b(arcade|runner|racer|racing game|shooter|shoot[- ]?'?em[- ]?up|platformer|snake game|asteroids|pinball|slingshot|catapult|flappy|endless run|dodge|joystick|swipe to)\b/i;
152
+ export function mercuryRealTimeGameNamed(personWords) {
153
+ return MERCURY_REAL_TIME_GAME_WORDS.test(personWords);
533
154
  }
534
- function recordsBriefRulesText() {
535
- return `You write the settled parts of a build brief for a small web app (React, a local-first database, access rules). The brief is read by a builder, who builds the records, rules and screens it names, from the person's own words. Write plain, concrete prose in the app's own nouns, as unformatted text in each JSON string.
536
-
537
- The decisions already made are given in the user message; follow them (archetype, record names, capabilities). The access part matches the archetype: who owns the object, who else is let in, what visitors see. ${MERCURY_RECORDS_RULE} ${MERCURY_RECORDS_SCREENS_RULE} ${MERCURY_RECORDS_DEMO_RULE} ${MERCURY_ASK_WORDS_RULE} Take from the field the SHAPE of a part (its length and how a cast, a look or a title is named); every noun comes from the person's words, and the app stays the one they asked for. ${MERCURY_ADDRESS.rule}
538
-
539
- ${MERCURY_RECORDS_BRIEF_FLOORS}`;
540
- }
541
- export const MERCURY_BRIEF_RULES = briefRulesText(MERCURY_DEMO_RULE, MERCURY_BRIEF_FLOORS);
542
- const MERCURY_DECIDED_SENTENCE = "The decisions already made are given in the user message; follow them (archetype, record names, capabilities).";
543
- export const MERCURY_ONECALL_DECIDED = "The classification keys in your reply (archetype, core action, record names, capabilities) decide the brief; follow them.";
544
- const MERCURY_SHAPE_ARCHETYPE_TEXT = (MERCURY_SHAPE_SYSTEM.split("archetype, exactly one:\n")[1] ?? "")
545
- .split("\n\ncoreAction:")[0]
546
- .trim();
547
- const MERCURY_LEAN_ARCHETYPES = MERCURY_SHAPE_ARCHETYPE_TEXT.replace(/\nTie-breaks:[\s\S]*$/, "");
155
+ const MERCURY_LEAN_ARCHETYPES = `- personal: one person's own tool ("for me", "my", "private", a tracker, a log, a list, a journal) with no other people named.
156
+ - group: the person names other people who use it with them (we, our, friends, family, team, club, kids, housemates, partner, members, a foursome).
157
+ - publish: the person writes and other people only read (a blog, menu, gallery, portfolio, schedule, zine, "everyone can read").
158
+ - intake: strangers or customers submit something without signing up and the person sees the submissions (orders, bookings, requests, sign-ups, replies, reports from the public).
159
+ - open: anyone with the link joins in and adds their own bit (a shared wall, canvas, feed, leaderboard game, community board, swap board, "anyone can post").
160
+ - toy: a game, simulator, visual, sound or generator that keeps little or nothing (scores at most).`;
548
161
  const MERCURY_LEAN_CAPABILITIES = MERCURY_CAPABILITY_TRIGGERS.replace(/^A trigger is an example, not a password: [^\n]*?composer is set only together with ai\./, "composer goes with ai.");
549
- const MERCURY_LEAN_ADDRESS = MERCURY_ADDRESS.rule
550
- .replace("as an array of three strings", "comma-separated")
551
- .replace(" and they need not repeat the title", "");
552
- export const MERCURY_ONECALL_CLASSIFY = `Your reply also classifies the person's request, in its own keys, and every part of the brief follows that classification.
553
-
554
- archetype, exactly one:
555
- ${MERCURY_SHAPE_ARCHETYPE_TEXT}
556
-
557
- coreAction: the one thing the person does most in this app, as a short verb phrase in their own words ("tap once per glass", "paste the group chat", "lasso cows"). It becomes the biggest control on the first screen.
558
- recordNames: 1-5 short singular record type names the app stores (toys may have just "score" or none).
559
- capabilities: an array naming each capability the request itself needs, from this list; empty when it needs none:
560
- ${MERCURY_CAPABILITY_TRIGGERS}
561
-
562
- The brief follows the classification: the access part matches the archetype; the demo names the records; with no ai capability the app works from its own records and no part mentions AI or callAI; with composer, the records come in through a sentence composer (Add and Ask), named as the front door with its mechanics left to the builder; a toy with record names saves best scores or a save slot per signed-in player and nothing else, and a toy with none keeps its state in memory and its access part says it opens with no sign-in and no records.`;
563
- export function mercuryBriefRules(o = {}) {
564
- const own = o.juice === "own-world";
565
- const floors = own ? MERCURY_OWN_WORLD_BRIEF_FLOORS : MERCURY_BRIEF_FLOORS;
566
- const rules = o.recordsRules === true
567
- ? recordsBriefRulesText()
568
- : briefRulesText(own ? MERCURY_OWN_WORLD_DEMO_RULE : MERCURY_DEMO_RULE, o.depth === "sub-feature" ? `${floors} ${MERCURY_DEPTH_RULE}` : floors);
569
- if (o.reply !== "whole")
570
- return rules;
571
- const story = mercuryStoryRule(mercuryStoryExample(o.personWords ?? ""));
572
- return rules.replace(ACCESS_SENTENCE, `${story} ${MERCURY_WHOLE_PREAMBLE_RULE} ${ACCESS_SENTENCE}`);
573
- }
574
- function withScreensKey(obj, value) {
575
- const keys = Object.keys(obj);
576
- const after = keys.includes("access") ? keys.indexOf("access") + 1 : keys.includes("opening") ? keys.indexOf("opening") + 1 : 0;
577
- const out = {};
578
- keys.slice(0, after).forEach((k) => (out[k] = obj[k]));
579
- out.screens = value;
580
- keys.slice(after).forEach((k) => (out[k] = obj[k]));
581
- return out;
582
- }
583
- function withRecordsKeys(obj, records, rules) {
584
- const keys = Object.keys(obj);
585
- const before = keys.indexOf("screens");
586
- const at = before >= 0
587
- ? before
588
- : keys.includes("access")
589
- ? keys.indexOf("access") + 1
590
- : keys.includes("opening")
591
- ? keys.indexOf("opening") + 1
592
- : 0;
593
- const out = {};
594
- keys.slice(0, at).forEach((k) => (out[k] = obj[k]));
595
- out.records = records;
596
- out.rules = rules;
597
- keys.slice(at).forEach((k) => (out[k] = obj[k]));
598
- return out;
599
- }
600
- function withWholeKeys(obj, extra) {
601
- const settings = ["look", ...Object.keys(MERCURY_ADDRESS.properties), "icon"];
602
- const keys = Object.keys(obj);
603
- const found = keys.findIndex((k) => settings.includes(k));
604
- const at = found < 0 ? keys.length : found;
605
- const out = {};
606
- keys.slice(0, at).forEach((k) => (out[k] = obj[k]));
607
- Object.entries(extra).forEach(([k, v]) => (out[k] = v));
608
- keys.slice(at).forEach((k) => (out[k] = obj[k]));
609
- return out;
610
- }
611
- export const MERCURY_CHOPPED_BRIEF_RULES = `These parts are turned up, in the person's own nouns, at the field's own lengths:
612
- - opening: reads like you can already see the app — what it is, who it is for, and the one moment that makes it fun to open.
613
- - seed: a small world in words — a lively cast with quoted names and where each one stands (one leading, one close behind, one just joined), what happened just now and last week, and a couple of records in the interesting states the screens would show (one overdue, one sold out, one on a streak). Every seeded record is one the app's own Add could write.
614
- - look: a scene — a surface, a palette, a card shape and one gesture of mood — in under twenty words.
615
- The access part, the title and the slugs stay as plain and exact as the field shows them. A vivid phrase belongs to this app or it stays out; every part describes the app the person asked for, and each part grows by a few words, never by a feature their words did not imply.`;
616
- export const MERCURY_CHOPPED_SEED_LINE = `- seed: a small world in words — a lively cast with quoted names and where each one stands (one leading, one close behind, one just joined), what happened just now and last week, and a couple of records in the interesting states the screens would show (one overdue, one sold out, one on a streak). Every seeded record is one the app's own Add could write.`;
617
- export const MERCURY_CHOPPED_OWN_WORLD_SEED_LINE = `- seed: a small world in words — a lively cast with quoted names and where each one stands in the app's own world, what happened just now and last week, and a couple of records in the interesting states this app's own records hold. Every seeded record is one the app's own Add could write.`;
618
- export const MERCURY_CHOPPED_OWN_WORLD_BRIEF_RULES = MERCURY_CHOPPED_BRIEF_RULES.replace(MERCURY_CHOPPED_SEED_LINE, MERCURY_CHOPPED_OWN_WORLD_SEED_LINE);
619
- export const MERCURY_CHOPPED_RECORDS_BRIEF_RULES = MERCURY_CHOPPED_BRIEF_RULES.replace(MERCURY_CHOPPED_SEED_LINE, MERCURY_CHOPPED_RECORDS_SEED_LINE);
620
- export const MERCURY_ONECALL_CLASSIFY_RECORDS = MERCURY_ONECALL_CLASSIFY.replace(MERCURY_FIRST_PERSON_TRIGGER, `${MERCURY_FIRST_PERSON_TRIGGER}\n${MERCURY_BLOCK_WORLD_TRIGGER}`);
621
- export const MERCURY_CHOPPED_PREAMBLE = `Make it vivid, like you can already see the app: name the first screen's hero control and the one moment that makes it fun to open, in their nouns. The promise stays the brief's: the core action and that control.`;
622
- const FIELD_BOUNDS = {
623
- opening: [15, 200],
624
- access: [60, 600],
625
- seed: [60, 650],
626
- look: [15, 150],
627
- icon: [8, 100],
628
- };
629
- const MERCURY_CLASSIFY_PROPERTIES = {
630
- archetype: { type: "string", enum: [...MERCURY_ARCHETYPES] },
631
- coreAction: { type: "string" },
632
- recordNames: { type: "array", items: { type: "string" } },
633
- capabilities: { type: "array", items: { type: "string", enum: [...MERCURY_FLAGS] } },
634
- };
635
- export function mercuryBriefKeys(fields) {
636
- return fields.flatMap((f) => (f === "pairs" ? Object.keys(MERCURY_ADDRESS.properties) : [replyKey(f)]));
637
- }
638
- export function mercuryBriefSchema(fields, opts = {}) {
639
- const rooms = opts.rooms === true;
640
- const font = opts.font === true;
641
- const fieldProps = Object.fromEntries(fields.flatMap((f) => (f === "pairs" ? Object.entries(MERCURY_ADDRESS.properties) : [[replyKey(f), { type: "string" }]])));
642
- const screened = opts.screens === true ? withScreensKey(fieldProps, { type: "string" }) : fieldProps;
643
- const outline = opts.recordsRules === true ? withRecordsKeys(screened, { type: "string" }, { type: "string" }) : screened;
644
- const props = opts.story === true || opts.preamble === true
645
- ? withWholeKeys(outline, {
646
- ...(opts.story === true ? { story: { type: "string" } } : {}),
647
- ...(opts.preamble === true ? { preamble: { type: "string" } } : {}),
648
- })
649
- : outline;
650
- const lead = withClassifyFirst(props, opts.classify === true, (k) => MERCURY_CLASSIFY_PROPERTIES[k]);
651
- return {
652
- type: "object",
653
- additionalProperties: false,
654
- required: [...Object.keys(lead), ...(rooms ? ["rooms"] : []), ...(font ? ["font"] : [])],
655
- properties: {
656
- ...lead,
657
- ...(rooms ? { rooms: { type: "array", items: { type: "string" } } } : {}),
658
- ...(font ? { font: { type: "string" } } : {}),
659
- },
660
- };
661
- }
662
- function capabilityDecisions(shape) {
663
- return [
664
- `AI text features: ${shape.flags.ai ? "yes (callAI)" : "none; the app works from its own records"}`,
665
- `generated pictures: ${shape.flags.imageGen ? "yes (ImgGen)" : "none"}`,
666
- ...(shape.flags.composer
667
- ? [
668
- "the records come in through a sentence composer (Add and Ask); name it as the front door and leave its mechanics to the builder",
669
- ]
670
- : []),
671
- ];
672
- }
673
- export function mercuryBriefMessages(o) {
674
- return briefMessages(o, { shape: o.shape });
675
- }
676
- export function mercuryOneCallBriefMessages(o) {
677
- return briefMessages(o, { draft: o.draft });
678
- }
679
- function briefMessages(o, by) {
680
- const s = "shape" in by ? by.shape : undefined;
681
- const classify = s === undefined;
682
- const rooms = o.rooms === true;
683
- const font = o.font === true;
684
- const screens = o.screens ?? o.depth === "sub-feature";
685
- const story = o.story ?? o.reply === "whole";
686
- const preamble = o.preamble ?? o.reply === "whole";
687
- const recordsRules = o.recordsRules === true;
688
- const recordsOwed = recordsRules && (o.recordsOwed ?? true);
689
- const keys = mercuryBriefKeys(o.fields);
690
- const keyed = Object.fromEntries(keys.map((k) => [k, k]));
691
- const screenedKeys = screens ? withScreensKey(keyed, "screens") : keyed;
692
- const outlineKeys = recordsOwed ? withRecordsKeys(screenedKeys, "records", "rules") : screenedKeys;
693
- const askedKeys = Object.keys(withClassifyFirst(story || preamble
694
- ? withWholeKeys(outlineKeys, { ...(story ? { story: "story" } : {}), ...(preamble ? { preamble: "preamble" } : {}) })
695
- : outlineKeys, classify, (k) => k));
696
- const skeleton = [
697
- "Answer in exactly this shape, each value replaced by the real text:",
698
- mercuryAnswerSkeleton(o.fields, {
699
- rooms,
700
- font,
701
- ...(screens ? { screens } : {}),
702
- ...(o.juice === undefined ? {} : { juice: o.juice }),
703
- ...(story ? { story } : {}),
704
- ...(preamble ? { preamble } : {}),
705
- ...(classify ? { classify } : {}),
706
- ...(recordsOwed ? { recordsRules: true } : {}),
707
- }),
708
- ...(rooms ? ["", `Rooms: ${poufRoomCatalog()}`] : []),
709
- ...(font ? ["", `Fonts: ${vibeFontCatalog()}`] : []),
710
- "",
711
- `Person's words: "${o.personWords}"`,
712
- "",
713
- ];
714
- const also = (rooms ? ` ${MERCURY_ROOMS_ASK}` : "") + (font ? ` ${MERCURY_FONT_ASK}` : "");
715
- const slugWords = o.fields.includes("pairs") && o.words.length > 0 ? [mercurySlugWordsSentence(o.words)] : [];
716
- const chopped = recordsRules
717
- ? MERCURY_CHOPPED_RECORDS_BRIEF_RULES
718
- : o.juice === "own-world"
719
- ? MERCURY_CHOPPED_OWN_WORLD_BRIEF_RULES
720
- : MERCURY_CHOPPED_BRIEF_RULES;
721
- if (s === undefined) {
722
- const notes = [
723
- ...(o.fields.includes("pairs") ? [MERCURY_ADDRESS.ask] : []),
724
- `archetype: one of ${MERCURY_ARCHETYPES.join(", ")}`,
725
- "recordNames: an array of record names",
726
- "capabilities: an array from the capability list, empty when none",
727
- ];
728
- const user = [
729
- ...skeleton,
730
- `Classify the person's words, then derive this app the way the example with that archetype derives its own. Return JSON with these keys: ${askedKeys.join(", ")} (${notes.join("; ")}).${also}`,
731
- ...slugWords,
732
- ].join("\n");
733
- const rules = mercuryBriefRules(o).replace(MERCURY_DECIDED_SENTENCE, MERCURY_ONECALL_DECIDED);
734
- const field = mercuryOneCallField("draft" in by ? by.draft : 0, MERCURY_ADDRESS, recordsRules);
735
- const classifyBlock = recordsRules ? MERCURY_ONECALL_CLASSIFY_RECORDS : MERCURY_ONECALL_CLASSIFY;
736
- return [
737
- {
738
- role: "system",
739
- content: [
740
- {
741
- type: "text",
742
- text: o.style === "chopped"
743
- ? `${rules}\n\n${classifyBlock}\n\n${chopped}\n\n${field}`
744
- : `${rules}\n\n${classifyBlock}\n\n${field}`,
745
- },
746
- ],
747
- },
748
- { role: "user", content: [{ type: "text", text: user }] },
749
- ];
750
- }
751
- const decisions = [
752
- `archetype: ${s.archetype}`,
753
- `core action: ${s.coreAction}`,
754
- `kind: ${s.kind}`,
755
- `for: ${s.who}`,
756
- `records: ${s.recordNames.length === 0 ? "(you choose, at most three)" : s.recordNames.join(", ")}`,
757
- ...capabilityDecisions(s),
758
- ...(s.archetype === "toy"
759
- ? [
760
- `saved: ${s.recordNames.length > 0 ? "best scores or a save slot per signed-in player, and nothing else" : "the toy keeps its state in memory; the access part says it opens with no sign-in and no records"}`,
761
- ]
762
- : []),
763
- ];
764
- const user = [
765
- ...skeleton,
766
- "Decisions:",
767
- ...decisions.map((d) => `- ${d}`),
768
- "",
769
- `Derive this app the way the example tagged [${s.archetype}] derives its own, from the person's words. Return JSON with these keys: ${askedKeys.join(", ")}` +
770
- (o.fields.includes("pairs") ? ` (${MERCURY_ADDRESS.ask})` : "") +
771
- "." +
772
- also,
773
- ...slugWords,
774
- ].join("\n");
775
- return [
776
- {
777
- role: "system",
778
- content: [
779
- {
780
- type: "text",
781
- text: o.style === "chopped"
782
- ? `${mercuryBriefRules(o)}\n\n${chopped}\n\n${mercuryExemplarField(s.archetype, MERCURY_ADDRESS, recordsRules)}`
783
- : `${mercuryBriefRules(o)}\n\n${mercuryExemplarField(s.archetype, MERCURY_ADDRESS, recordsRules)}`,
784
- },
785
- ],
786
- },
787
- { role: "user", content: [{ type: "text", text: user }] },
788
- ];
789
- }
790
- const FIELD_LABEL = /^\s*(?:\*\*)?(opening|access|seed|look|icon|preamble)(?:\*\*)?\s*:\s*/i;
791
- const FENCED = /^```[a-zA-Z]*\n([\s\S]*?)\n?```$/;
792
- export function cleanMercuryText(text) {
793
- const t = text.trim();
794
- const fenced = FENCED.exec(t);
795
- return (fenced === null ? t : fenced[1].trim()).replace(FIELD_LABEL, "").trim();
796
- }
797
- function stem(name) {
798
- return name
799
- .replace(/([a-z])([A-Z])/g, "$1 $2")
800
- .toLowerCase()
801
- .split(/[\s_-]+/)
802
- .filter((w) => w.length > 2)
803
- .map((w) => w.replace(/s$/, ""));
804
- }
805
- function mentions(name, text) {
806
- const lower = text.toLowerCase();
807
- return stem(name).every((w) => lower.includes(w));
808
- }
809
- const TAG_LEAK = /\[(personal|group|publish|intake|open|toy)\]|archetype/i;
810
- const MARKUP = /```|^\s*#/m;
811
- const AI_LEAK = /\bcallai\b|\bAI\b|language model/;
812
162
  function kebab(s) {
813
163
  return s
814
164
  .toLowerCase()
@@ -816,31 +166,9 @@ function kebab(s) {
816
166
  .replace(/^-+|-+$/g, "");
817
167
  }
818
168
  const LEADING_TAG = /^\s*\[(?:personal|group|publish|intake|open|toy)\]\s*/i;
819
- const DEMO_PREFIX = /^demo\s*:\s*/i;
820
169
  export function stripMercuryArchetypeTag(text) {
821
170
  return LEADING_TAG.test(text) ? { text: text.replace(LEADING_TAG, ""), repaired: true } : { text, repaired: false };
822
171
  }
823
- export function repairMercuryField(field, value) {
824
- const repaired = new Set();
825
- const lead = stripMercuryArchetypeTag(cleanMercuryText(value));
826
- if (lead.repaired)
827
- repaired.add("archetype-tag");
828
- let t = lead.text.trim();
829
- if (field === "seed" && t.length > 0) {
830
- const prefix = DEMO_PREFIX.exec(t);
831
- const rest = stripMercuryArchetypeTag(prefix === null ? t : t.slice(prefix[0].length));
832
- if (rest.repaired)
833
- repaired.add("archetype-tag");
834
- if (prefix === null)
835
- repaired.add("demo-prefix");
836
- t = `demo: ${rest.text.trim()}`;
837
- }
838
- return { value: t, repaired: [...repaired] };
839
- }
840
- export function prepareMercuryField(field, value) {
841
- const r = repairMercuryField(field, value);
842
- return { gate: r.value, value: cleanMercuryText(value), misses: r.repaired };
843
- }
844
172
  export function gateMercuryAddress(o) {
845
173
  const raw = o.title.trim();
846
174
  const cut = stripMercuryArchetypeTag(raw);
@@ -866,71 +194,6 @@ export function mercurySlugsWithWord(pairs, words) {
866
194
  const ws = words.map((w) => kebab(w)).filter((w) => w.length > 0);
867
195
  return pairs.filter((p) => ws.some((w) => p.slug.includes(w))).length;
868
196
  }
869
- export function gateMercuryField(o) {
870
- const t = cleanMercuryText(o.value);
871
- const a = o.shape.archetype;
872
- if (o.field === "seed" && a === "toy" && o.shape.recordNames.length === 0)
873
- return [];
874
- const [lo, hi] = FIELD_BOUNDS[o.field];
875
- const fails = [];
876
- if (t.length === 0)
877
- fails.push("empty");
878
- else if (t.length < lo)
879
- fails.push("short");
880
- else if (t.length > hi)
881
- fails.push("long");
882
- if (MARKUP.test(t))
883
- fails.push("markup");
884
- if (TAG_LEAK.test(t))
885
- fails.push("tag-leak");
886
- if (o.field !== "look" && o.field !== "icon" && o.shape.flags.ai === false && AI_LEAK.test(t))
887
- fails.push("ai-leak");
888
- if (o.field === "seed" && DEMO_PREFIX.test(t) === false)
889
- fails.push("demo-prefix");
890
- if (o.field === "seed" && o.shape.recordNames.length > 0 && o.shape.recordNames.some((r) => mentions(r, t)) === false)
891
- fails.push("seed-no-record");
892
- if (o.field === "access") {
893
- if (a === "group" && /invite|member/i.test(t) === false)
894
- fails.push("access-no-invite");
895
- if (a === "personal" && /invite screen (is|ships)|members (read|can)/i.test(t))
896
- fails.push("access-personal-invites");
897
- if (a === "toy" && o.shape.recordNames.length > 0 && /nothing is saved|no records/i.test(t))
898
- fails.push("toy-contradiction");
899
- if ((a === "publish" || a === "intake") && /read|see|visitor|customer|client|anyone|everyone|public/i.test(t) === false)
900
- fails.push("access-no-readers");
901
- }
902
- return fails;
903
- }
904
- export const MERCURY_ACCESS_TAIL = {
905
- personal: "Rules key on the object's creator; records carry the object id so someone could be let in later. The invite screen waits for a later build.",
906
- group: "Memberships are written by the platform's HandleInput invite screen, which is part of this first build; shared objects show who made them with ViewerTag.",
907
- publish: "The owner writes; everything they publish is readable by anyone with the link, signed in or not. Shaped so a co-owner could be let in later.",
908
- intake: "Every visitor writes their own submissions into the shared list, signed in or not, and edits only their own; the signed-in owner reads and manages all of them. Shaped so staff could be let in later.",
909
- open: "Everyone with the link writes the shared records — posts, votes, entries — and edits only their own; the owner can remove anything.",
910
- toy: "",
911
- };
912
- const TAIL_ALREADY_SAID = {
913
- personal: /invite screen waits|later build|no invite screen/i,
914
- group: /HandleInput/,
915
- publish: /readable by anyone|anyone with the link/i,
916
- };
917
- export function mercuryAccessLine(shape, access) {
918
- const said = TAIL_ALREADY_SAID[shape.archetype];
919
- const tail = said !== undefined && said.test(access) ? "" : MERCURY_ACCESS_TAIL[shape.archetype];
920
- return `${access} ${tail}`.trim();
921
- }
922
- const CAPABILITY_NAMES = [
923
- ["ai", "AI text (callAI)"],
924
- ["composer", "a sentence composer (Add and Ask) as the front door"],
925
- ["imageGen", "generated pictures (ImgGen)"],
926
- ["backend", "server work (backend.js)"],
927
- ["calendar", "calendar dates"],
928
- ["audio", "sound"],
929
- ["threeD", "3D graphics"],
930
- ["charts", "charts"],
931
- ["boardGame", "a turn-based board or card game"],
932
- ["sensitive", "private records"],
933
- ];
934
197
  const FLAG_SKILLS = [
935
198
  ["ai", "callai"],
936
199
  ["imageGen", "image-gen"],
@@ -942,59 +205,11 @@ const FLAG_SKILLS = [
942
205
  ["boardGame", "board-games"],
943
206
  ];
944
207
  export function mercurySkills(shape, opts = {}) {
945
- const skills = ["access", "fireproof", ...FLAG_SKILLS.filter(([f]) => shape.flags[f]).map(([, skill]) => skill)];
946
- if (opts.recordsRules !== true || !mercuryBlockWorldNamed(opts.personWords ?? ""))
947
- return skills;
948
- return [...skills.filter((k) => k !== "three-js"), "voxel", "three-js"];
949
- }
950
- const POSTURE_PUBLIC = /\b(anyone with the link|everyone with the link|signed in or not|any visitor|readable by anyone|public(ly)?)\b/i;
951
- const POSTURE_ALONE = /\b(alone|no one else|nobody else|only (the owner|the creator|you|me) (can )?(read|see)s?)\b/i;
952
- const POSTURE_MEMBERS = /\b(members?|invit\w*|let in|HandleInput|friends?|households?|travel+ers?|guests?|players?|teammates?)\b/i;
953
- const POSTURE_WRITE_VERB = /\b(edit|edits|write|writes|add|adds|change|changes|update|updates|post|posts|submit|submits|book|books|log|logs|place|places|remove|removes)\b/i;
954
- const POSTURE_OTHERS = /\b(members?|everyone|anyone|friends?|guests?|visitors?|others|travel+ers?|households?|players?|teammates?|each (poster|player|member|guest|visitor))\b/i;
955
- const POSTURE_OWNER_ONLY = /\b(alone|only the (owner|creator)|the (owner|creator) (alone )?(writes|edits))\b/i;
956
- const POSTURE_OTHERS_READ = /\b(members?|everyone|anyone|friends?|guests?|visitors?|they|others)\b[^.;]*\b(read|reads|see|sees|view|views)\b/i;
957
- export function mercuryAccessPosture(access) {
958
- const read = POSTURE_PUBLIC.test(access)
959
- ? "public"
960
- : POSTURE_ALONE.test(access)
961
- ? "private"
962
- : POSTURE_MEMBERS.test(access)
963
- ? "members"
964
- : undefined;
965
- let othersWrite = false;
966
- let ownerWrites = false;
967
- for (const sentence of access.split(/(?<=[.;])\s+/)) {
968
- if (!POSTURE_WRITE_VERB.test(sentence))
969
- continue;
970
- if (POSTURE_OWNER_ONLY.test(sentence) && !POSTURE_OTHERS.test(sentence))
971
- ownerWrites = true;
972
- else if (POSTURE_OTHERS.test(sentence))
973
- othersWrite = true;
974
- else if (/\b(owner|creator)\b/i.test(sentence))
975
- ownerWrites = true;
976
- }
977
- const write = othersWrite
978
- ? "members"
979
- : ownerWrites || POSTURE_OTHERS_READ.test(access)
980
- ? "owner"
981
- : undefined;
982
- return { ...(read === undefined ? {} : { read }), ...(write === undefined ? {} : { write }) };
983
- }
984
- export function mercuryDuoConflict(a, b, personWords) {
985
- if (a.shape.archetype !== b.shape.archetype)
986
- return `archetype: ${a.shape.archetype} vs ${b.shape.archetype}`;
987
- const na = mercuryNature(a.shape, personWords);
988
- const nb = mercuryNature(b.shape, personWords);
989
- if (na !== nb)
990
- return `nature: ${na} vs ${nb}`;
991
- const pa = mercuryAccessPosture(a.access);
992
- const pb = mercuryAccessPosture(b.access);
993
- if (pa.read !== undefined && pb.read !== undefined && pa.read !== pb.read)
994
- return `access-read: ${pa.read} vs ${pb.read}`;
995
- if (pa.write !== undefined && pb.write !== undefined && pa.write !== pb.write)
996
- return `access-write: ${pa.write} vs ${pb.write}`;
997
- return undefined;
208
+ const words = opts.personWords ?? "";
209
+ const base = ["access", "fireproof", ...FLAG_SKILLS.filter(([f]) => shape.flags[f]).map(([, skill]) => skill)];
210
+ const skills = mercuryBlockWorldNamed(words) ? [...base.filter((k) => k !== "three-js"), "voxel", "three-js"] : base;
211
+ const realTime = mercuryBlockWorldNamed(words) || mercuryRealTimeGameNamed(words) || (shape.archetype === "toy" && shape.flags.threeD);
212
+ return realTime ? [...skills, "touch-games"] : skills;
998
213
  }
999
214
  export function mercuryNature(shape, personWords) {
1000
215
  if (shape.keepsToThemselves === true)
@@ -1006,15 +221,6 @@ export function mercuryNature(shape, personWords) {
1006
221
  return "open";
1007
222
  return shape.flags.sensitive && (a === "personal" || a === "intake") ? "sensitive" : "collaborative";
1008
223
  }
1009
- export function mercuryWorkflowLine(shape) {
1010
- const caps = CAPABILITY_NAMES.filter(([f]) => shape.flags[f]).map(([, name]) => name);
1011
- return [
1012
- `Archetype: ${shape.archetype}.`,
1013
- `Core action: "${shape.coreAction}".`,
1014
- ...(shape.recordNames.length === 0 ? [] : [`Records: ${shape.recordNames.join(", ")}.`]),
1015
- ...(caps.length === 0 ? [] : [`Capabilities: ${caps.join(", ")}.`]),
1016
- ].join(" ");
1017
- }
1018
224
  const MERCURY_ROOMS_MAX = 3;
1019
225
  export function gateMercuryRooms(raw) {
1020
226
  if (!Array.isArray(raw))
@@ -1034,169 +240,6 @@ export function gateMercuryFont(raw) {
1034
240
  return { dropped: 0 };
1035
241
  return typeof raw === "string" && vibeFontSlugs().includes(raw) ? { font: raw, dropped: 0 } : { dropped: 1 };
1036
242
  }
1037
- export function gateMercuryScreens(raw) {
1038
- if (raw === undefined)
1039
- return { dropped: 0 };
1040
- return typeof raw === "string" && cleanMercuryText(raw).length > 0 ? { screens: raw, dropped: 0 } : { dropped: 1 };
1041
- }
1042
- function mercuryScreensLine(screens) {
1043
- const t = cleanMercuryText(screens ?? "");
1044
- if (t.length === 0)
1045
- return "";
1046
- return /^screen/i.test(t) ? t : `Screens: ${t}`;
1047
- }
1048
- function mercuryPartLine(label, text) {
1049
- const t = cleanMercuryText(text ?? "").replace(/^(records?|rules?)\s*:\s*/i, "");
1050
- return t.length === 0 ? "" : `${label}: ${t}`;
1051
- }
1052
- const WHOLE_MARKUP = /```|^\s*#/m;
1053
- export function gateMercuryStory(raw) {
1054
- if (raw === undefined)
1055
- return { dropped: 0 };
1056
- if (typeof raw !== "string")
1057
- return { dropped: 1 };
1058
- const t = cleanMercuryText(raw);
1059
- if (t.length === 0)
1060
- return { dropped: 1 };
1061
- return WHOLE_MARKUP.test(t) ? { story: raw, dropped: 0, misses: ["markup"] } : { story: raw, dropped: 0 };
1062
- }
1063
- export function gateMercuryWholePreamble(text) {
1064
- const t = cleanMercuryText(text);
1065
- const fails = [];
1066
- if (t.length === 0)
1067
- fails.push("empty");
1068
- if (/```|^\s*#|\bbrief\b|\bdemo\s*:/im.test(t))
1069
- fails.push("markup");
1070
- return fails;
1071
- }
1072
- function mercuryEnrichedPrompt(o) {
1073
- if (o.reply === "whole")
1074
- return mercuryWholeEnrichedPrompt(o);
1075
- const s = o.shape;
1076
- const keepsNothing = s.archetype === "toy" && s.recordNames.length === 0;
1077
- const access = cleanMercuryText(o.fields.access);
1078
- const seed = keepsNothing ? "" : cleanMercuryText(o.fields.seed);
1079
- return [
1080
- cleanMercuryText(o.fields.opening),
1081
- keepsNothing && access.length === 0 ? "" : mercuryAccessLine(s, access),
1082
- s.keepsToThemselves === true ? ONE_TREE_PER_PERSON : "",
1083
- mercuryWorkflowLine(o.capabilityFlags === undefined ? s : { ...s, flags: o.capabilityFlags }),
1084
- mercuryScreensLine(o.screens),
1085
- seed.length === 0 ? "" : `${seed} ${KNOWN_VALUES_DIRECT_CONTROLS}`,
1086
- `The person asked: "${o.personWords}".`,
1087
- ]
1088
- .filter((p) => p.length > 0)
1089
- .join("\n\n");
1090
- }
1091
- function mercuryWholeEnrichedPrompt(o) {
1092
- const own = wholeSection(o.shape, {
1093
- opening: o.fields.opening,
1094
- access: o.fields.access,
1095
- seed: o.fields.seed,
1096
- look: o.fields.look,
1097
- ...(o.records === undefined ? {} : { records: o.records }),
1098
- ...(o.rules === undefined ? {} : { rules: o.rules }),
1099
- ...(o.screens === undefined ? {} : { screens: o.screens }),
1100
- ...(o.story === undefined ? {} : { story: o.story }),
1101
- ...(o.preamble === undefined ? {} : { preamble: o.preamble }),
1102
- });
1103
- const partner = o.partner === undefined ? undefined : o.recordsRules === true ? { ...o.partner, seed: "", story: undefined } : o.partner;
1104
- const takes = partner === undefined ? own : [...own, "---", ...wholeSection(o.shape, partner)];
1105
- return [...takes, `The person asked: "${o.personWords}".`].join("\n\n");
1106
- }
1107
- function wholeSection(s, t) {
1108
- const keepsNothing = s.archetype === "toy" && s.recordNames.length === 0;
1109
- const access = cleanMercuryText(t.access);
1110
- const seed = keepsNothing ? "" : cleanMercuryText(t.seed);
1111
- return [
1112
- cleanMercuryText(t.opening),
1113
- keepsNothing && access.length === 0 ? "" : mercuryAccessLine(s, access),
1114
- mercuryPartLine("Records", t.records),
1115
- mercuryPartLine("Rules", t.rules),
1116
- mercuryScreensLine(t.screens),
1117
- seed.length === 0 ? "" : `${seed} ${KNOWN_VALUES_DIRECT_CONTROLS}`,
1118
- cleanMercuryText(t.story ?? ""),
1119
- cleanMercuryText(t.preamble ?? ""),
1120
- cleanMercuryText(t.look),
1121
- ].filter((p) => p.length > 0);
1122
- }
1123
- export function mercuryPreambleBrief(o) {
1124
- return mercuryEnrichedPrompt(o);
1125
- }
1126
- export function assembleMercuryCreateVibe(o) {
1127
- const s = o.shape;
1128
- const look = cleanMercuryText(o.fields.look);
1129
- const icon = cleanMercuryText(o.fields.icon);
1130
- const nature = mercuryNature(s, o.personWords);
1131
- const rooms = sanitizeRoomShortlist("pouf", o.rooms);
1132
- return {
1133
- prompt: o.personWords,
1134
- enrichedPrompt: mercuryEnrichedPrompt(o),
1135
- skills: mercurySkills(o.capabilityFlags === undefined ? s : { ...s, flags: o.capabilityFlags }, {
1136
- personWords: o.personWords,
1137
- ...(o.recordsRules === true ? { recordsRules: true } : {}),
1138
- }),
1139
- pairs: o.fields.pairs.map((p) => ({ title: p.title, slug: p.slug })),
1140
- ...(icon.length === 0 ? {} : { iconDescription: icon }),
1141
- ...(look.length === 0 ? {} : { textureNote: look }),
1142
- nature,
1143
- ...(nature === "sensitive" ? { privacySensitive: true } : {}),
1144
- ...(rooms === undefined ? {} : { rooms: [...rooms] }),
1145
- ...(o.font === undefined ? {} : { font: o.font }),
1146
- };
1147
- }
1148
- export const MERCURY_PREAMBLE_SYSTEM = `You speak to the person who asked for an app, just before it is built. Write two or three plain sentences in their own words: what you are making for them, its core action, and the big control on its first screen that does it. That is the whole promise — the builder works out everything else after you speak. Start with "I'm making" or "I'm building", and write it as plain sentences to the person.`;
1149
- export function mercuryPreambleMessages(o) {
1150
- const user = [
1151
- `Person's words: "${o.personWords}"`,
1152
- `Core action: ${o.shape.coreAction}`,
1153
- "",
1154
- "What is being built:",
1155
- o.enrichedPrompt,
1156
- "",
1157
- "Write the preamble.",
1158
- ].join("\n");
1159
- return [
1160
- {
1161
- role: "system",
1162
- content: [
1163
- {
1164
- type: "text",
1165
- text: o.style === "chopped" ? `${MERCURY_PREAMBLE_SYSTEM} ${MERCURY_CHOPPED_PREAMBLE}` : MERCURY_PREAMBLE_SYSTEM,
1166
- },
1167
- ],
1168
- },
1169
- { role: "user", content: [{ type: "text", text: user }] },
1170
- ];
1171
- }
1172
- const preambleParsed = lazyType(() => type({ preamble: "string" }));
1173
- export function mercuryPreambleSchema() {
1174
- return {
1175
- type: "object",
1176
- additionalProperties: false,
1177
- required: ["preamble"],
1178
- properties: { preamble: { type: "string" } },
1179
- };
1180
- }
1181
- export function gateMercuryPreamble(o) {
1182
- const t = cleanMercuryText(o.text);
1183
- const fails = [];
1184
- if (t.length === 0)
1185
- fails.push("empty");
1186
- else if (t.length < 60)
1187
- fails.push("short");
1188
- else if (t.length > 480)
1189
- fails.push("long");
1190
- if (/```|^\s*#|\bbrief\b|\bdemo\s*:/im.test(t))
1191
- fails.push("markup");
1192
- if ((t.match(/[.!?](\s|$)/g) ?? []).length > 3)
1193
- fails.push("too-many-sentences");
1194
- const briefWords = new Set(o.enrichedPrompt.toLowerCase().match(/[a-z]{5,}/g) ?? []);
1195
- const shared = new Set((t.toLowerCase().match(/[a-z]{5,}/g) ?? []).filter((w) => briefWords.has(w)));
1196
- if (shared.size < 3)
1197
- fails.push("unanchored");
1198
- return fails;
1199
- }
1200
243
  const completionBody = lazyType(() => type({
1201
244
  "model?": "string",
1202
245
  choices: type({ message: { content: "string | null" }, "finish_reason?": "string | null" }).array(),
@@ -1240,9 +283,6 @@ async function requestMercury(vctx, o, signal) {
1240
283
  stream: false,
1241
284
  max_tokens: o.maxTokens,
1242
285
  ...(model.reasoning === undefined ? {} : { reasoning: { ...model.reasoning } }),
1243
- ...(o.schema === undefined
1244
- ? {}
1245
- : { response_format: { type: "json_schema", json_schema: { name: o.name, strict: true, schema: o.schema } } }),
1246
286
  messages: o.messages,
1247
287
  };
1248
288
  const rRes = await exception2Result(() => vctx.llmRequest({ ...req, headers: vctx.params.llm.headers }, { signal }));
@@ -1267,13 +307,9 @@ async function requestMercury(vctx, o, signal) {
1267
307
  const ended = `(finish ${finish}, ${body.usage?.completion_tokens ?? "?"} of ${o.maxTokens} tokens)`;
1268
308
  if (content === undefined || content === null || content.trim().length === 0)
1269
309
  return Result.Err(`mercury ${o.name}: empty reply ${ended}`);
1270
- const rJson = o.schema === undefined ? Result.Ok(content) : exception2Result(() => JSON.parse(cleanMercuryText(content)));
1271
- if (rJson.isErr())
1272
- return Result.Err(`mercury ${o.name}: JSON ${ended}: ${rJson.Err().message.slice(0, 80)}`);
1273
310
  const u = body.usage;
1274
311
  const reasoningTokens = u?.completion_tokens_details?.reasoning_tokens;
1275
312
  return Result.Ok({
1276
- json: rJson.Ok(),
1277
313
  text: content,
1278
314
  ...(typeof reasoningTokens === "number" ? { reasoningTokens } : {}),
1279
315
  ...(body.model === undefined ? {} : { servedModel: body.model }),
@@ -1339,8 +375,6 @@ async function mercuryAttempt(vctx, o, log) {
1339
375
  }
1340
376
  return { kind: "cancelled", inFlight };
1341
377
  }
1342
- const persistent = o.persistent === true;
1343
- const maxTransientRetries = persistent ? Number.POSITIVE_INFINITY : (o.transientRetries ?? MERCURY_TRANSIENT_RETRIES);
1344
378
  let transientRetries = 0;
1345
379
  async function persistentWait(retryAfterMs) {
1346
380
  const wait = mercuryTesterRetryWaitMs(transientRetries + 1, retryAfterMs);
@@ -1360,7 +394,7 @@ async function mercuryAttempt(vctx, o, log) {
1360
394
  if (remaining < MERCURY_MIN_ATTEMPT_MS) {
1361
395
  const spent = `mercury ${o.call}: stage budget spent (${o.budget.elapsedMs()}ms of ${o.budget.totalMs}ms)`;
1362
396
  log.lastError = log.sends === 0 && log.carried !== true ? spent : `${spent} after ${log.cause}: ${log.lastError}`;
1363
- log.cause = o.budget.rateLimitCapMs() === undefined ? "budget" : "rate-limited";
397
+ log.cause = "budget";
1364
398
  return { kind: "budget" };
1365
399
  }
1366
400
  const deadline = Math.min(o.deadlineMs, remaining);
@@ -1370,7 +404,6 @@ async function mercuryAttempt(vctx, o, log) {
1370
404
  log.inFlight = true;
1371
405
  const r = await withDeadline((signal) => requestMercury(vctx, {
1372
406
  name: o.call,
1373
- ...(ask.schema === undefined ? {} : { schema: ask.schema }),
1374
407
  messages: ask.messages,
1375
408
  maxTokens: o.maxTokens,
1376
409
  ...(o.model === undefined ? {} : { model: o.model }),
@@ -1381,9 +414,9 @@ async function mercuryAttempt(vctx, o, log) {
1381
414
  if (r.timedOut) {
1382
415
  const clipped = deadline < o.deadlineMs;
1383
416
  log.lastError = `mercury ${o.call}: timed out after ${deadline}ms${clipped ? " (stage budget)" : ""}`;
1384
- log.cause = clipped ? (o.budget.rateLimitCapMs() === undefined ? "budget" : "rate-limited") : "timeout";
417
+ log.cause = clipped ? "budget" : "timeout";
1385
418
  noteSend(log, o.label);
1386
- if (persistent && !clipped && (await persistentWait(undefined)))
419
+ if (o.persistent && !clipped && (await persistentWait(undefined)))
1387
420
  continue;
1388
421
  return { kind: "failed" };
1389
422
  }
@@ -1392,33 +425,21 @@ async function mercuryAttempt(vctx, o, log) {
1392
425
  const err = r.reply.Err();
1393
426
  log.lastError = err.message;
1394
427
  if (err instanceof MercuryTransientError === false) {
1395
- log.cause = persistent ? "network" : "error";
428
+ log.cause = o.persistent ? "network" : "error";
1396
429
  noteSend(log, o.label);
1397
- if (persistent && (await persistentWait(undefined)))
430
+ if (o.persistent && (await persistentWait(undefined)))
1398
431
  continue;
1399
432
  return { kind: "failed" };
1400
433
  }
1401
434
  log.cause = err.status === 429 ? "rate-limited" : "upstream-error";
1402
- if (err.status === 429 && !persistent)
1403
- o.budget.noteRateLimited();
1404
435
  if (err.retryAfterMs !== undefined)
1405
436
  log.retryAfterMs = Math.max(log.retryAfterMs ?? 0, err.retryAfterMs);
1406
437
  noteSend(log, o.label);
1407
- if (persistent) {
1408
- if (await persistentWait(err.retryAfterMs))
1409
- continue;
1410
- return { kind: "stop" };
1411
- }
1412
- if (transientRetries >= maxTransientRetries)
438
+ if (!o.persistent)
1413
439
  return { kind: "failed" };
1414
- const wait = mercuryRetryWaitMs(transientRetries + 1, err.retryAfterMs);
1415
- if (wait === undefined || wait > o.budget.remainingMs() - MERCURY_MIN_ATTEMPT_MS) {
1416
- log.lastError = `${err.message} (retry-after ${err.retryAfterMs ?? "none"}ms; falling back inside the stage budget)`;
1417
- return { kind: "stop" };
1418
- }
1419
- await waitOrCancel(wait, cancel);
1420
- transientRetries += 1;
1421
- log.retries += 1;
440
+ if (await persistentWait(err.retryAfterMs))
441
+ continue;
442
+ return { kind: "stop" };
1422
443
  }
1423
444
  }
1424
445
  function absorbReply(into, reply) {
@@ -1429,34 +450,6 @@ function absorbReply(into, reply) {
1429
450
  if (reply.reasoningTokens !== undefined)
1430
451
  into.reasoningTokens = (into.reasoningTokens ?? 0) + reply.reasoningTokens;
1431
452
  }
1432
- async function mercuryCall(vctx, o) {
1433
- const startedAt = Date.now();
1434
- const totals = { usage: [] };
1435
- const log = mercurySendLog();
1436
- let attempts = 0;
1437
- while (attempts < MERCURY_CALL_ATTEMPTS) {
1438
- const a = await mercuryAttempt(vctx, o, log);
1439
- if (a.kind === "budget" || a.kind === "stop" || a.kind === "cancelled")
1440
- break;
1441
- attempts += 1;
1442
- if (a.kind === "failed")
1443
- continue;
1444
- absorbReply(totals, a.reply);
1445
- const accepted = o.accept(a.reply.json);
1446
- if (accepted.isOk()) {
1447
- o.onCounts?.(log.sends, log.retries);
1448
- await recordMercuryRow(vctx, {
1449
- ...o.row,
1450
- record: callRecord({ call: o.call, ok: true, attempts, startedAt, totals, log, extra: { text: a.reply.text } }),
1451
- });
1452
- return Result.Ok(accepted.Ok());
1453
- }
1454
- log.lastError = `mercury ${o.call}: ${accepted.Err().message}`;
1455
- log.cause = "gate";
1456
- noteSend(log);
1457
- }
1458
- return failCall(vctx, { call: o.call, row: o.row, attempts, startedAt, totals, log });
1459
- }
1460
453
  function callRecord(o) {
1461
454
  const t = o.totals;
1462
455
  return {
@@ -1528,9 +521,6 @@ async function recordMercuryRow(vctx, o) {
1528
521
  retries: o.record.retries,
1529
522
  ...(o.record.history === undefined || o.record.history.length === 0 ? {} : { history: [...o.record.history] }),
1530
523
  ...(o.record.reasoningTokens === undefined ? {} : { reasoningTokens: o.record.reasoningTokens }),
1531
- ...(o.record.passedThrough === undefined || o.record.passedThrough.length === 0
1532
- ? {}
1533
- : { passedThrough: [...o.record.passedThrough] }),
1534
524
  ...(o.record.addressRepair === undefined || o.record.addressRepair.length === 0
1535
525
  ? {}
1536
526
  : { addressRepair: [...o.record.addressRepair] }),
@@ -1538,44 +528,12 @@ async function recordMercuryRow(vctx, o) {
1538
528
  ...(o.record.roomsDropped === undefined ? {} : { roomsDropped: o.record.roomsDropped }),
1539
529
  ...(o.record.fontDropped === undefined ? {} : { fontDropped: o.record.fontDropped }),
1540
530
  ...(o.record.fanout === undefined ? {} : { fanout: o.record.fanout }),
1541
- ...(o.record.juice === undefined ? {} : { juice: o.record.juice }),
1542
- ...(o.record.depth === undefined ? {} : { depth: o.record.depth }),
1543
- ...(o.record.screensDropped === undefined ? {} : { screensDropped: o.record.screensDropped }),
1544
- ...(o.record.reply === undefined ? {} : { reply: o.record.reply }),
1545
- ...(o.record.preambleFrom === undefined ? {} : { preambleFrom: o.record.preambleFrom }),
1546
- ...(o.record.storyChars === undefined ? {} : { storyChars: o.record.storyChars }),
1547
- ...(o.record.storyDropped === undefined ? {} : { storyDropped: o.record.storyDropped }),
1548
- ...(o.record.preambleChars === undefined ? {} : { preambleChars: o.record.preambleChars }),
1549
531
  ...(o.record.softMisses === undefined ? {} : { softMisses: o.record.softMisses }),
1550
- ...(o.record.coerced === undefined ? {} : { coerced: [...o.record.coerced] }),
1551
- ...(o.record.fieldsDropped === undefined ? {} : { fieldsDropped: [...o.record.fieldsDropped] }),
1552
- ...(o.record.preambleFallback === undefined ? {} : { preambleFallback: o.record.preambleFallback }),
1553
- ...(o.record.duo === undefined ? {} : { duo: o.record.duo }),
1554
- ...(o.record.duoDrafts === undefined ? {} : { duoDrafts: [...o.record.duoDrafts] }),
1555
- ...(o.record.duoBreadth === undefined ? {} : { duoBreadth: [...o.record.duoBreadth] }),
1556
- ...(o.record.duoPool === undefined ? {} : { duoPool: o.record.duoPool }),
1557
- ...(o.record.duoArchetypes === undefined ? {} : { duoArchetypes: [...o.record.duoArchetypes] }),
1558
- ...(o.record.duoConflict === undefined
1559
- ? {}
1560
- : { duoConflict: o.record.duoConflict, duoConflictReason: o.record.duoConflictReason }),
1561
- ...(o.record.recordsRules === undefined ? {} : { recordsRules: o.record.recordsRules }),
1562
- ...(o.record.recordsDropped === undefined ? {} : { recordsDropped: o.record.recordsDropped }),
1563
- ...(o.record.rulesDropped === undefined ? {} : { rulesDropped: o.record.rulesDropped }),
1564
- ...(o.record.oneCall === undefined ? {} : { oneCall: o.record.oneCall }),
1565
- ...(o.record.shapeCall === undefined ? {} : { shapeCall: o.record.shapeCall }),
1566
- ...(o.record.draftArchetypes === undefined ? {} : { draftArchetypes: [...o.record.draftArchetypes] }),
1567
- ...(o.record.archetypeSplit === undefined ? {} : { archetypeSplit: o.record.archetypeSplit }),
1568
532
  ...(o.record.flagsWinner === undefined ? {} : { flagsWinner: [...o.record.flagsWinner] }),
1569
- ...(o.record.flagsUnion === undefined ? {} : { flagsUnion: [...o.record.flagsUnion] }),
1570
- ...(o.record.backendFilterCleared === undefined ? {} : { backendFilterCleared: [...o.record.backendFilterCleared] }),
1571
- ...(o.record.backendFilterKept === undefined ? {} : { backendFilterKept: o.record.backendFilterKept }),
1572
533
  ...(o.record.planner === undefined ? {} : { planner: o.record.planner }),
1573
534
  ...(o.record.format === undefined ? {} : { format: o.record.format }),
1574
- ...(o.record.thinkAloud === undefined ? {} : { thinkAloud: { ...o.record.thinkAloud } }),
1575
535
  ...(o.record.cards === undefined ? {} : { cards: { ...o.record.cards } }),
1576
536
  ...(o.keepText !== true || o.record.text === undefined ? {} : { text: o.record.text }),
1577
- ...(o.keepText !== true || o.record.partialText === undefined ? {} : { partialText: o.record.partialText }),
1578
- ...(o.keepText !== true || o.record.duoTexts === undefined ? {} : { duoTexts: [...o.record.duoTexts] }),
1579
537
  ...(rounds.length === 0 ? {} : { usage: rounds.map((r) => ({ ...r })) }),
1580
538
  },
1581
539
  },
@@ -1645,334 +603,61 @@ export async function recordMercuryFim(vctx, o) {
1645
603
  if (r.isErr())
1646
604
  vctx.logger.Warn().Err(r).Str("chatId", o.chatId).Msg("mercury-first-build-fim-row-failed");
1647
605
  }
1648
- export const MERCURY_PROSE_FORMAT_EXAMPLES = [
1649
- {
1650
- ask: "an appointments book for my dog grooming business, with a page of notes on each client's dog",
1651
- brief: 'An appointments book for a dog grooming business. The owner\'s business object (default, named for the app, renameable in settings) holds two record kinds: dogs (client dogs) and appointments.\n\nMain screen: appointments book — a day-by-day agenda grouped by date (today first, upcoming days below, past collapsible), with a week strip to jump between days. Each appointment shows time, dog name, owner name, service (e.g. full groom, bath & brush, nail trim), duration, and status (booked / done / no-show) as a one-tap toggle. "New appointment" button opens a plain form: pick an existing dog (or add a new dog inline), date (relative words like "tomorrow"/"Friday" resolve against today and show the resolved date), time, service, duration, price, short note. Edit and delete on each appointment.\n\nDogs page: list of client dogs (searchable), each opening its own dog page: name, breed, owner name, owner phone, and a free-form notes page (temperament, coat, allergies, preferred cut, behaviour tips) that saves as you type, plus that dog\'s appointment history and next appointment, and a "Book this dog" button. Tapping a dog name on an appointment opens its page.\n\nRecords: dog {type:\'dog\', name, breed, ownerName, ownerPhone, notes} and appointment {type:\'appointment\', dogId, date (YYYY-MM-DD), time, service, durationMin, price, status, note}, all keyed to the business object id so staff can be invited later. Access rules: creator of the business object can read/write everything in it; members could be admitted later. No invite screen on screen yet.\n\nseed.json demo: the owner\'s business object, 6 dogs with realistic notes (e.g. a nervous poodle, a doodle with matting, a husky that hates dryers), and ~10 appointments dated around 2026-10-06 (a few past done, several today and this week).',
1652
- },
1653
- {
1654
- ask: "a snake game with a high score",
1655
- brief: "A classic snake game. Main screen: a square grid board (drawn in SVG or canvas using theme tokens) where the snake moves steadily and eats food to grow; hitting a wall or itself ends the round. Controls: arrow keys / WASD on desktop, swipe plus an on-screen D-pad on phones. Start/Pause and Play again buttons. Current score shown above the board, and the person's high score beside it. When a round ends with a new best, save a score record (player handle, score, date) and celebrate \"New high score!\". Show a small high-score list (top 10 scores with ViewerTag for who set them) below the board. Speed rises gently as the snake grows. Records: each signed-in person's scores keyed on their own handle; access rules let the creator write their own score records and everyone with the link read the leaderboard. seed.json: a handful of demo scores (e.g. 8–6 entries from a small cast of players, scores 12–87, dated in the days around 2026-10-06), including one for the owner.",
1656
- },
1657
- {
1658
- ask: "Make a family reunion bingo app. Let me paste our inside jokes, memories, or a message thread and turn them into a batch of squares I can edit. Deal a different card to each person I invite. We can mark squares and call bingo on our phones, with cards and progress saved.",
1659
- brief: 'Family reunion bingo. The owner has a default game (named for the app, renamable in settings) keyed on their handle. Front door: one composer where the owner pastes inside jokes, memories or a whole message thread. Add sends the text to callAI, which extracts short bingo square phrases, saves each as a square record in the game immediately, and lists the saved squares with Edit (inline text edit) and Delete actions. Each saved square has Change by sentence: selecting it switches Add to Apply correction, which rewrites that same square id. Ask action answers a question about the squares/game read-only beside the composer (e.g. "which squares mention Grandma?"). One Undo restores the latest batch add or the previous version of an edited square. If extraction fails, keep the pasted text in the box. A plain "Add square" text input stays beside the composer.\nInvite: HandleInput in the game writes a membership for each invited family member. Deal: each member (owner included) gets their own card record — a 5x5 grid with a FREE center, drawn by a seeded shuffle from the square pool, so each person\'s card is different; owner can press Deal cards / Redeal. Players open the game on their phones, tap squares to mark them (marks saved on their own card record, only the card\'s player can mark), and a Bingo! button lights up when a row, column or diagonal is complete; calling bingo writes a bingo record that everyone in the game sees in a winners feed with ViewerTag. A progress view shows each player\'s marked count. Games list reads games by type, showing shared ones with who made them.\nAccess: game created by owner; members can read the game, squares, all cards and bingo calls; only owner edits squares/deals; each player writes only their own card\'s marks and their own bingo call.\nSeed.json demo: a "Miller Family Reunion 2025" game owned by the owner with ~30 squares of believable inside jokes ("Uncle Ray tells the fishing story", "Someone burns the burgers", "Grandma asks who\'s dating whom", "Kids find the creek", "Aunt Linda\'s potato salad debate"), owner\'s membership plus two cousins, dealt cards with a few marks, and one bingo call.',
1660
- },
1661
- ];
1662
- export const MERCURY_PROSE_SYSTEM = `You write the build brief for a small web app from a person's one-message request. The brief is read by the builder that writes the app's code. Return only JSON.
1663
-
1664
- Your reply first classifies the request, then names the app, then writes the brief.
1665
-
1666
- archetype, exactly one:
1667
- ${MERCURY_SHAPE_ARCHETYPE_TEXT}
1668
-
1669
- coreAction: the one thing the person does most in this app, as a short verb phrase in their own words ("tap once per glass", "paste the group chat", "lasso cows").
1670
- recordNames: 1-5 short singular record type names the app stores (toys may have just "score" or none).
1671
- capabilities: an array naming each capability the request itself needs, from this list; empty when it needs none:
1672
- ${MERCURY_CAPABILITY_TRIGGERS}
1673
-
1674
- ${MERCURY_ADDRESS.rule}
1675
-
1676
- brief: one free-prose brief to the builder, in exactly the shape of the example briefs below — the same sections in the same order, the same voice addressed to the builder, the same plain paragraphs. It opens with one line saying what the app is; names the object the records belong to and who owns it; walks the screens with the controls each one has and what each does; names each record with its fields, as the examples write them; states the access rules — who reads and who writes; and ends with the seed.json demo, specific: names, items and states. Everything in it follows the classification and the person's own words.`;
1677
- export function mercuryProseBriefSchema(opts = {}) {
1678
- const props = {
1679
- ...MERCURY_CLASSIFY_PROPERTIES,
1680
- ...MERCURY_ADDRESS.properties,
1681
- ...(opts.rooms === true ? { rooms: { type: "array", items: { type: "string" } } } : {}),
1682
- ...(opts.font === true ? { font: { type: "string" } } : {}),
1683
- brief: { type: "string" },
1684
- };
1685
- return { type: "object", additionalProperties: false, required: Object.keys(props), properties: props };
1686
- }
1687
- export function mercuryProseBriefMessages(o) {
1688
- const rooms = o.rooms === true;
1689
- const font = o.font === true;
1690
- const examples = MERCURY_PROSE_FORMAT_EXAMPLES.map((e, i) => `Example ${i + 1}. The person asked: "${e.ask}"\nbrief:\n${e.brief}`).join("\n\n");
1691
- const skeleton = {
1692
- ...Object.fromEntries(Object.entries(MERCURY_CLASSIFY_HINTS).map(([k, h]) => [k, `<${h}>`])),
1693
- ...Object.fromEntries(Object.entries(MERCURY_ADDRESS.hints).map(([k, h]) => [k, `<${h}>`])),
1694
- ...(rooms ? { rooms: `<${MERCURY_ROOMS_HINT}>` } : {}),
1695
- ...(font ? { font: `<${MERCURY_FONT_HINT}>` } : {}),
1696
- brief: "<the brief to the builder, in the examples' shape>",
1697
- };
1698
- const user = [
1699
- "Answer in exactly this shape, each value replaced by the real text:",
1700
- JSON.stringify(skeleton, null, 1),
1701
- ...(rooms ? ["", `Rooms: ${poufRoomCatalog()}`] : []),
1702
- ...(font ? ["", `Fonts: ${vibeFontCatalog()}`] : []),
1703
- "",
1704
- `Person's words: "${o.personWords}"`,
1705
- "",
1706
- `Classify the person's words, name the app, then write its brief the way the examples write theirs. Return JSON with these keys: ${Object.keys(skeleton).join(", ")} (${MERCURY_ADDRESS.ask}).` +
1707
- (rooms ? ` ${MERCURY_ROOMS_ASK}` : "") +
1708
- (font ? ` ${MERCURY_FONT_ASK}` : ""),
1709
- ...(o.words.length > 0 ? [mercurySlugWordsSentence(o.words)] : []),
1710
- ].join("\n");
1711
- return [
1712
- { role: "system", content: [{ type: "text", text: `${MERCURY_PROSE_SYSTEM}\n\n${examples}` }] },
1713
- { role: "user", content: [{ type: "text", text: user }] },
1714
- ];
1715
- }
1716
- export function gateMercuryProseDraft(json, o) {
1717
- const parsed = replyObject(json);
1718
- if (Array.isArray(json) || parsed instanceof type.errors)
1719
- return Result.Err("reply:not-an-object");
1720
- const rShape = parseMercuryDraftShape(parsed, o.personWords);
1721
- if (rShape.isErr())
1722
- return Result.Err(`classification:${rShape.Err().message}`);
1723
- const brief = typeof parsed.brief === "string" ? cleanMercuryText(parsed.brief) : "";
1724
- if (brief.length === 0)
1725
- return Result.Err("brief:empty");
1726
- const decoded = MERCURY_ADDRESS.decode(parsed);
1727
- if (decoded.isErr())
1728
- return Result.Err(`pairs:${decoded.Err().message}`);
1729
- const d = decoded.Ok();
1730
- const strict = d.title === undefined ? undefined : gateMercuryAddress({ title: d.title, slugs: d.slugs });
1731
- const address = strict?.isOk() === true ? strict.Ok() : looseMercuryAddress(d.title, d.slugs);
1732
- if (address === undefined)
1733
- return Result.Err("pairs:empty");
1734
- return Result.Ok({
1735
- shape: rShape.Ok(),
1736
- brief,
1737
- address,
1738
- addressRepair: [...d.repairs, ...(address.fallback ? ["slug-fallback"] : [])],
1739
- ...(o.askRooms && "rooms" in parsed ? { rooms: gateMercuryRooms(parsed.rooms) } : {}),
1740
- ...(o.askRooms && "font" in parsed ? { font: gateMercuryFont(parsed.font) } : {}),
1741
- });
1742
- }
1743
- export function assembleMercuryProseCreateVibe(o) {
1744
- const s = o.draft.shape;
1745
- const nature = mercuryNature(s, o.personWords);
1746
- const rooms = sanitizeRoomShortlist("pouf", o.draft.rooms?.rooms);
1747
- return {
1748
- prompt: o.personWords,
1749
- enrichedPrompt: `${o.draft.brief}\n\nThe person asked: "${o.personWords}".`,
1750
- skills: mercurySkills(s, { personWords: o.personWords }),
1751
- pairs: o.draft.address.pairs.map((p) => ({ title: p.title, slug: p.slug })),
1752
- nature,
1753
- ...(nature === "sensitive" ? { privacySensitive: true } : {}),
1754
- ...(rooms === undefined ? {} : { rooms: [...rooms] }),
1755
- ...(o.draft.font?.font === undefined ? {} : { font: o.draft.font.font }),
1756
- };
1757
- }
1758
- export function mercuryProsePreamble(draft) {
1759
- return `I'm making ${draft.address.pairs[0]?.title ?? "your app"} for you: ${draft.shape.coreAction}.`;
1760
- }
1761
- export const MERCURY_THINKALOUD_MARKER = "--- SPEC ---";
1762
- export const MERCURY_THINKALOUD_SPEC_HINTS = {
1763
- archetype: "one of personal, group, publish, intake, open, toy: the ownership shape that fits the person's words",
1764
- coreAction: "the one thing the person does most in this app, a short verb phrase in their own words",
1765
- recordNames: "1 to 5 short singular record type names the app stores, comma-separated",
1766
- capabilities: "the capabilities this request needs, from the capability list, comma-separated; none when it needs none",
1767
- opening: "one sentence to the builder: what the app is, for whom, and what it does, in the person's words",
1768
- access: "who owns the object, who else is let in and what they write, what a visitor sees",
1769
- records: 'each record type with its fields: "name (field, field); name (field, field)", plus the id rule where one matters',
1770
- screens: "1. the first screen: what it shows from the records, what the person does there, and what leads while it is empty; 2. ...; 3. ...",
1771
- seed: "seed.json: the demo records in words: who is in it, which state each record is in, how they relate; every state the mechanism and the clauses mention; the person's typed items verbatim; the builder picks amounts, counts and dates",
1772
- mechanism: "the one computation or invariant the app hinges on, with the unit of every number it computes",
1773
- clauses: 'one line per clause of the person\'s words: "their clause" → record field → control → twice: what the second identical write does',
1774
- look: "one scene: surface, palette, card shape, one gesture of mood",
1775
- title: "the app's name, two or three words in its own vocabulary",
1776
- slugs: "three short kebab-case addresses for the app, comma-separated",
1777
- icon: "one object from the app's world",
1778
- };
1779
- const THINKALOUD_BRIEF_KEYS = ["opening", "access", "records", "screens", "seed", "mechanism", "clauses", "look", "icon"];
1780
- export function mercuryThinkAloudSystem(o = {}) {
1781
- const keys = {
1782
- ...MERCURY_THINKALOUD_SPEC_HINTS,
1783
- ...(o.rooms === true ? { rooms: `${MERCURY_ROOMS_HINT}, comma-separated` } : {}),
1784
- ...(o.font === true ? { font: MERCURY_FONT_HINT } : {}),
1785
- };
1786
- const specLines = Object.entries(keys)
1787
- .map(([k, h]) => `${k}: <${h}>`)
1788
- .join("\n");
1789
- return `You are a usability tester. Someone has just shown you this request for a small web app, and you are seeing it for the first time. Think out loud, in the first person, the way you would in a recorded session. These are hunches, not answers: label them as guesses ("I'd guess", "probably", "not sure", "wait").
1790
-
1791
- 1. What you picture when you read it, and what you would tap first.
1792
- 2. Then keep asking "and then what happens to…": if I do this, what changes somewhere else? What does that number mean, and what is it made of? What did the person not say that the app has to decide anyway?
1793
- 3. What feels off or ambiguous in the person's exact words, quoting them.
1794
-
1795
- Then write one line that reads exactly "${MERCURY_THINKALOUD_MARKER}" and switch roles: write the settled parts of a brief for a builder, using only what you now believe after the think-aloud. One key per line, in this order, as "key: value"; the clauses and screens may take one line each under their key. Every line that rests on one of your guesses rather than on the person's words starts its value with "guess:"; a line the person's words settle carries no label.
1796
-
1797
- ${specLines}
1798
-
1799
- archetype, exactly one:
1800
- ${MERCURY_LEAN_ARCHETYPES}
1801
-
1802
- capabilities: each capability the request itself needs, from this list; none when it needs none:
1803
- ${MERCURY_LEAN_CAPABILITIES}
1804
-
1805
- ${MERCURY_LEAN_ADDRESS}`;
1806
- }
1807
- export function mercuryThinkAloudMessages(o) {
1808
- const rooms = o.rooms === true;
1809
- const font = o.font === true;
1810
- const user = [
1811
- `The app creator's initial prompt: "${o.personWords}"`,
1812
- ...(rooms ? ["", `Rooms: ${poufRoomCatalog()}`] : []),
1813
- ...(font ? ["", `Fonts: ${vibeFontCatalog()}`] : []),
1814
- ...(o.words.length > 0 ? ["", mercurySlugWordsSentence(o.words)] : []),
1815
- ].join("\n");
1816
- return [
1817
- { role: "system", content: [{ type: "text", text: mercuryThinkAloudSystem({ rooms, font }) }] },
1818
- { role: "user", content: [{ type: "text", text: user }] },
1819
- ];
1820
- }
1821
- const THINKALOUD_KEYS = new Set([...Object.keys(MERCURY_THINKALOUD_SPEC_HINTS), "rooms", "font"]);
1822
- const THINKALOUD_MARKER_LINE = /^[ \t]*[*_#>-]*[ \t]*-{2,}[ \t]*SPEC[ \t]*-{2,}[ \t]*[*_]*[ \t]*$/im;
1823
- const THINKALOUD_KEY_LINE = /^\s*(?:[-*]\s+)?(?:\*\*|__)?([A-Za-z]+)(?:\*\*|__)?\s*:(?:\*\*|__)?\s*(.*)$/;
1824
- const GUESS_LABEL = /\bguess\s*:/i;
1825
- export function parseMercuryThinkAloud(text) {
1826
- const m = THINKALOUD_MARKER_LINE.exec(text);
1827
- if (m === null)
1828
- return undefined;
1829
- const narration = text.slice(0, m.index).trim();
1830
- const spec = text
1831
- .slice(m.index + m[0].length)
1832
- .replace(/^\s*```[a-zA-Z]*\n?|\n?```\s*$/g, "")
1833
- .trim();
1834
- const keys = {};
1835
- let current;
1836
- for (const line of spec.split("\n")) {
1837
- const k = THINKALOUD_KEY_LINE.exec(line);
1838
- const name = k === null ? undefined : [...THINKALOUD_KEYS].find((x) => x.toLowerCase() === k[1].toLowerCase());
1839
- if (name !== undefined && k !== null && keys[name] === undefined) {
1840
- current = name;
1841
- keys[name] = k[2].trim();
1842
- continue;
1843
- }
1844
- if (current !== undefined && line.trim().length > 0)
1845
- keys[current] = `${keys[current]}\n${line.trim()}`.trim();
1846
- }
1847
- const guessLines = spec.split("\n").filter((l) => GUESS_LABEL.test(l)).length;
1848
- return { narration, spec, keys, guessLines };
1849
- }
1850
- function thinkAloudScalar(v) {
606
+ const TAIL_KEY_LINE = /^\s*(?:[-*]\s+)?(?:\*\*|__)?([A-Za-z]+)(?:\*\*|__)?\s*:(?:\*\*|__)?\s*(.*)$/;
607
+ function tailScalar(v) {
1851
608
  return (v ?? "")
1852
609
  .replace(/^\s*\(?\s*guess\s*:\s*/i, "")
1853
610
  .replace(/\(\s*guess[^)]*\)/gi, "")
1854
611
  .replace(/\bguess\s*:\s*/gi, "")
1855
- .trim()
1856
- .replace(/^["'`]+|["'`]+$/g, "")
1857
- .trim();
1858
- }
1859
- function thinkAloudList(v) {
1860
- const t = thinkAloudScalar(v).replace(/^\[|\]$/g, "");
1861
- return t
1862
- .split(/\s*[,;\n]\s*/)
1863
- .map((x) => x
1864
- .trim()
1865
- .replace(/^["'`]+|["'`]+$/g, "")
1866
- .trim())
1867
- .filter((x) => x.length > 0 && /^(none|n\/a|nothing|-)$/i.test(x) === false);
1868
- }
1869
- export const MERCURY_THINKALOUD_DEFAULT_ARCHETYPE = MERCURY_ARCHETYPES[0];
1870
- export function gateMercuryThinkAloud(raw, o) {
1871
- if (typeof raw !== "string" || raw.trim().length === 0)
1872
- return Result.Err("parse:empty");
1873
- const soft = {};
1874
- const miss = (part, what) => {
1875
- soft[part] = [...(soft[part] ?? []), what];
1876
- };
1877
- const parsed = parseMercuryThinkAloud(raw);
1878
- if (parsed === undefined)
1879
- miss("marker", "missing");
1880
- else if (parsed.spec.length === 0)
1881
- miss("spec", "empty");
1882
- const p = parsed ?? {
1883
- narration: raw.trim(),
1884
- spec: "",
1885
- keys: {},
1886
- guessLines: raw.split("\n").filter((l) => GUESS_LABEL.test(l)).length,
1887
- };
1888
- const caps = [];
1889
- for (const c of thinkAloudList(p.keys.capabilities).map((x) => x.toLowerCase())) {
1890
- const flag = MERCURY_FLAGS.find((f) => f.toLowerCase() === c);
1891
- if (flag === undefined)
1892
- miss("capabilities", "unknown");
1893
- else if (!caps.includes(flag))
1894
- caps.push(flag);
1895
- }
1896
- const named = thinkAloudScalar(p.keys.archetype)
1897
- .toLowerCase()
1898
- .match(/[a-z]+/)?.[0] ?? "";
1899
- const known = MERCURY_ARCHETYPES.find((a) => a === named);
1900
- if (known === undefined)
1901
- miss("archetype", "default");
1902
- const said = thinkAloudScalar(p.keys.coreAction);
1903
- if (said.length === 0)
1904
- miss("coreAction", "default");
1905
- const rShape = parseMercuryDraftShape({
1906
- archetype: known ?? MERCURY_THINKALOUD_DEFAULT_ARCHETYPE,
1907
- coreAction: said.length > 0 ? said : o.personWords.trim(),
1908
- recordNames: thinkAloudList(p.keys.recordNames),
1909
- capabilities: caps,
1910
- }, o.personWords);
1911
- if (rShape.isErr())
1912
- return Result.Err(`classification:${rShape.Err().message}`);
1913
- const decoded = MERCURY_ADDRESS.decode({
1914
- ...(p.keys.title === undefined ? {} : { title: thinkAloudScalar(p.keys.title) }),
1915
- slugs: thinkAloudList(p.keys.slugs),
1916
- });
1917
- if (decoded.isErr())
1918
- miss("pairs", decoded.Err().message);
1919
- const d = decoded.isOk() ? decoded.Ok() : { slugs: [], repairs: [] };
1920
- const strict = d.title === undefined ? undefined : gateMercuryAddress({ title: d.title, slugs: d.slugs });
1921
- const loose = strict?.isOk() === true ? strict.Ok() : looseMercuryAddress(d.title, d.slugs);
1922
- if (strict?.isErr() === true && loose !== undefined)
1923
- miss("pairs", strict.Err().message);
1924
- if (loose === undefined)
1925
- miss("pairs", "default");
1926
- const address = loose ?? { pairs: [], fallback: true, tagged: false };
1927
- if (p.narration.length === 0)
1928
- miss("narration", "empty");
1929
- for (const k of THINKALOUD_BRIEF_KEYS)
1930
- if (p.keys[k] === undefined || p.keys[k].trim().length === 0)
1931
- miss(k, "missing");
1932
- if (o.askRooms && p.keys.rooms === undefined)
1933
- miss("rooms", "default");
1934
- if (o.askRooms && p.keys.font === undefined)
1935
- miss("font", "default");
1936
- return Result.Ok({
1937
- shape: rShape.Ok(),
1938
- reply: raw,
1939
- narration: p.narration,
1940
- spec: p.spec,
1941
- address,
1942
- addressRepair: [...d.repairs, ...(address.fallback && address.pairs.length > 0 ? ["slug-fallback"] : [])],
1943
- ...(o.askRooms && p.keys.rooms !== undefined ? { rooms: gateMercuryRooms(thinkAloudList(p.keys.rooms)) } : {}),
1944
- ...(o.askRooms && p.keys.font !== undefined ? { font: gateMercuryFont(thinkAloudScalar(p.keys.font)) } : {}),
1945
- softMisses: soft,
1946
- guessLines: p.guessLines,
1947
- });
612
+ .trim()
613
+ .replace(/^["'`]+|["'`]+$/g, "")
614
+ .trim();
615
+ }
616
+ function tailList(v) {
617
+ const t = tailScalar(v).replace(/^\[|\]$/g, "");
618
+ return t
619
+ .split(/\s*[,;\n]\s*/)
620
+ .map((x) => x
621
+ .trim()
622
+ .replace(/^["'`]+|["'`]+$/g, "")
623
+ .trim())
624
+ .filter((x) => x.length > 0 && /^(none|n\/a|nothing|-)$/i.test(x) === false);
1948
625
  }
626
+ export const MERCURY_CARDS_DEFAULT_ARCHETYPE = MERCURY_ARCHETYPES[0];
1949
627
  export const MERCURY_TESTER_REPLY_INTRO = "A fast tester's first read:";
1950
628
  export const MERCURY_TESTER_REPLY_RULE = "Guesses only; the creator's words win.";
1951
- export function mercuryTesterReplyEnrichedPrompt(reply, personWords) {
629
+ export const MERCURY_SHARING_LINE = {
630
+ personal: "Each person's records live in their own object, keyed on its creator, and carry the object id so a friend could be invited later; a one-tap publish shares a chosen record with everyone who can open the app.",
631
+ group: "Memberships are written by the platform's HandleInput invite screen, which is part of this first build; shared objects show who made them with ViewerTag.",
632
+ publish: "The owner writes; everything they publish is readable by anyone with the link, signed in or not. Shaped so a co-owner could be let in later.",
633
+ intake: "Every visitor adds submissions to the shared list, signed in or not; a signed-in visitor edits only their own (a signed-out submission is create-only — the rules have no identity to check it against), and the signed-in owner reads and manages all of them. Shaped so staff could be let in later.",
634
+ open: "Everyone with the link reads the shared records — posts, votes, entries — signed in or not; every signed-in visitor adds their own and edits only their own, and the owner can remove anything.",
635
+ toy: "Anything players save — beats, levels, scores, creations — lands on one always-on public channel, readable by anyone with the link, signed in or not; each signed-in player edits only their own, and the owner can remove anything.",
636
+ };
637
+ export const MERCURY_SHARING_KEEPS_TO_THEMSELVES = "Each person's records are theirs alone, and a one-tap publish shares a chosen one with everyone who can open the app.";
638
+ export const MERCURY_SHARING_SENSITIVE = 'Each person\'s records are private to them from the first one, and sharing is theirs to turn on: a "Shared with followers" checkbox that starts unchecked, an add-buddies picker that grants one chosen handle, and a one-tap publish for a single finished item — never an always-on public channel for the records.';
639
+ export const MERCURY_SHARING_PREFIX = "Who sees what:";
640
+ export function mercurySharingLine(shape, personWords) {
641
+ const line = shape.keepsToThemselves === true
642
+ ? MERCURY_SHARING_KEEPS_TO_THEMSELVES
643
+ : mercuryNature(shape, personWords) === "sensitive"
644
+ ? MERCURY_SHARING_SENSITIVE
645
+ : MERCURY_SHARING_LINE[shape.archetype];
646
+ return `${MERCURY_SHARING_PREFIX} ${line}`;
647
+ }
648
+ export function mercuryTesterReplyEnrichedPrompt(reply, personWords, shape) {
1952
649
  return [
1953
650
  MERCURY_TESTER_REPLY_INTRO,
1954
651
  `<tester-reply>\n${reply}\n</tester-reply>`,
1955
652
  MERCURY_TESTER_REPLY_RULE,
653
+ mercurySharingLine(shape, personWords),
654
+ ...mercuryCardsBuilderLines(shape),
1956
655
  `The app creator's initial prompt: "${personWords}"`,
1957
656
  ].join("\n\n");
1958
657
  }
1959
- export function assembleMercuryThinkAloudCreateVibe(o) {
1960
- const s = o.draft.shape;
1961
- const nature = mercuryNature(s, o.personWords);
1962
- const rooms = sanitizeRoomShortlist("pouf", o.draft.rooms?.rooms);
1963
- return {
1964
- prompt: o.personWords,
1965
- enrichedPrompt: mercuryTesterReplyEnrichedPrompt(o.draft.reply, o.personWords),
1966
- skills: mercurySkills(s, { personWords: o.personWords }),
1967
- ...(o.draft.address.pairs.length === 0 ? {} : { pairs: o.draft.address.pairs.map((p) => ({ title: p.title, slug: p.slug })) }),
1968
- nature,
1969
- ...(nature === "sensitive" ? { privacySensitive: true } : {}),
1970
- ...(rooms === undefined ? {} : { rooms: [...rooms] }),
1971
- ...(o.draft.font?.font === undefined ? {} : { font: o.draft.font.font }),
1972
- };
1973
- }
1974
- export function mercuryThinkAloudPreamble(draft) {
1975
- return `I'm making ${draft.address.pairs[0]?.title ?? "your app"} for you: ${draft.shape.coreAction}.`;
658
+ export const MERCURY_COMPOSER_LINE = "The records come in through a sentence composer (Add and Ask) as the front door.";
659
+ export function mercuryCardsBuilderLines(shape) {
660
+ return [shape.flags.composer ? MERCURY_COMPOSER_LINE : "", KNOWN_VALUES_DIRECT_CONTROLS].filter((l) => l.length > 0);
1976
661
  }
1977
662
  export const MERCURY_CARDS_MARKER = "--- CARDS ---";
1978
663
  export const MERCURY_CARDS_APP_MARKER = "--- APP ---";
@@ -1988,10 +673,15 @@ export const MERCURY_CARDS_SHAPE = `As a <role>, I <action on a named record>, s
1988
673
  Accept: <another>
1989
674
  Data: <Record>{<field>, <field>, ...}
1990
675
  Rule: <a value this card depends on and how it comes about, said loosely in one plain English sentence a friend would understand>`;
676
+ export const MERCURY_ASK_WORDS_RULE = `Keep the ask's genre and capability words exactly as the person wrote them — 3D, first-person, email, a named recipient, a schedule, a fixed number of people — in the preamble and the cards.`;
677
+ export const MERCURY_TYPED_DATA_RULE = `When the person typed data — items, amounts, names, a list — the cards carry every piece of it verbatim, quantities included ("half a bag" stays "half a bag").`;
1991
678
  export const MERCURY_CARDS_RULES = `Rules:
1992
679
  - Use the person's own nouns for records, roles and actions.
1993
680
  - Give what the person asked for plus what those parts need in order to work.
1994
- - A line that rests on your assumption and not on the person's words ends with (guess). A line the person's words settle carries no label.`;
681
+ - A line that rests on your assumption and not on the person's words ends with (guess). A line the person's words settle carries no label.
682
+ - ${MERCURY_ASK_WORDS_RULE}
683
+ - ${MERCURY_TYPED_DATA_RULE}`;
684
+ const MERCURY_CARDS_CAPABILITIES = MERCURY_LEAN_CAPABILITIES.replace(MERCURY_FIRST_PERSON_TRIGGER, `${MERCURY_FIRST_PERSON_TRIGGER}\n${MERCURY_BLOCK_WORLD_TRIGGER}`);
1995
685
  export function mercuryCardsSystem(o = {}) {
1996
686
  const keys = {
1997
687
  ...MERCURY_CARDS_TAIL_HINTS,
@@ -2016,9 +706,9 @@ archetype, exactly one:
2016
706
  ${MERCURY_LEAN_ARCHETYPES}
2017
707
 
2018
708
  capabilities: each capability the request itself needs, from this list; none when it needs none:
2019
- ${MERCURY_LEAN_CAPABILITIES}
709
+ ${MERCURY_CARDS_CAPABILITIES}
2020
710
 
2021
- ${MERCURY_LEAN_ADDRESS}`;
711
+ ${MERCURY_ADDRESS.rule}`;
2022
712
  }
2023
713
  export function mercuryCardsMessages(o) {
2024
714
  const rooms = o.rooms === true;
@@ -2074,7 +764,7 @@ export function parseMercuryCards(text) {
2074
764
  const keys = {};
2075
765
  if (appMarkerAt >= 0) {
2076
766
  for (const line of lines.slice(appMarkerAt + 1)) {
2077
- const k = THINKALOUD_KEY_LINE.exec(line);
767
+ const k = TAIL_KEY_LINE.exec(line);
2078
768
  const name = k === null ? undefined : [...CARDS_TAIL_KEYS].find((x) => x.toLowerCase() === k[1].toLowerCase());
2079
769
  if (name !== undefined && k !== null && keys[name] === undefined)
2080
770
  keys[name] = k[2].trim();
@@ -2110,21 +800,21 @@ export function gateMercuryCards(raw, o) {
2110
800
  if (p.cardLines.length === 0)
2111
801
  miss("cards", "none");
2112
802
  const caps = [];
2113
- for (const c of thinkAloudList(p.keys.capabilities).map((x) => x.toLowerCase())) {
803
+ for (const c of tailList(p.keys.capabilities).map((x) => x.toLowerCase())) {
2114
804
  const flag = MERCURY_FLAGS.find((f) => f.toLowerCase() === c);
2115
805
  if (flag === undefined)
2116
806
  miss("capabilities", "unknown");
2117
807
  else if (!caps.includes(flag))
2118
808
  caps.push(flag);
2119
809
  }
2120
- const named = thinkAloudScalar(p.keys.archetype)
810
+ const named = tailScalar(p.keys.archetype)
2121
811
  .toLowerCase()
2122
812
  .match(/[a-z]+/)?.[0] ?? "";
2123
813
  const known = MERCURY_ARCHETYPES.find((a) => a === named);
2124
814
  if (known === undefined)
2125
815
  miss("archetype", "default");
2126
- const rShape = parseMercuryDraftShape({
2127
- archetype: known ?? MERCURY_THINKALOUD_DEFAULT_ARCHETYPE,
816
+ const rShape = parseMercuryBriefShape({
817
+ archetype: known ?? MERCURY_CARDS_DEFAULT_ARCHETYPE,
2128
818
  coreAction: o.personWords.trim(),
2129
819
  recordNames: p.dataRecords.slice(0, 5),
2130
820
  capabilities: caps,
@@ -2132,8 +822,8 @@ export function gateMercuryCards(raw, o) {
2132
822
  if (rShape.isErr())
2133
823
  return Result.Err(`classification:${rShape.Err().message}`);
2134
824
  const decoded = MERCURY_ADDRESS.decode({
2135
- ...(p.keys.title === undefined ? {} : { title: thinkAloudScalar(p.keys.title) }),
2136
- slugs: thinkAloudList(p.keys.slugs),
825
+ ...(p.keys.title === undefined ? {} : { title: tailScalar(p.keys.title) }),
826
+ slugs: tailList(p.keys.slugs),
2137
827
  });
2138
828
  if (decoded.isErr())
2139
829
  miss("pairs", decoded.Err().message);
@@ -2155,8 +845,8 @@ export function gateMercuryCards(raw, o) {
2155
845
  parsed: p,
2156
846
  address,
2157
847
  addressRepair: [...d.repairs, ...(address.fallback && address.pairs.length > 0 ? ["slug-fallback"] : [])],
2158
- ...(o.askRooms && p.keys.rooms !== undefined ? { rooms: gateMercuryRooms(thinkAloudList(p.keys.rooms)) } : {}),
2159
- ...(o.askRooms && p.keys.font !== undefined ? { font: gateMercuryFont(thinkAloudScalar(p.keys.font)) } : {}),
848
+ ...(o.askRooms && p.keys.rooms !== undefined ? { rooms: gateMercuryRooms(tailList(p.keys.rooms)) } : {}),
849
+ ...(o.askRooms && p.keys.font !== undefined ? { font: gateMercuryFont(tailScalar(p.keys.font)) } : {}),
2160
850
  softMisses: soft,
2161
851
  });
2162
852
  }
@@ -2286,7 +976,7 @@ async function mercuryFimCall(fetchImpl, o) {
2286
976
  return { kind: "failed", error: last.length === 0 ? "aborted" : last };
2287
977
  }
2288
978
  export async function mercuryCardsFim(vctx, o) {
2289
- const p = o.draft.parsed;
979
+ const p = o.brief.parsed;
2290
980
  const startedAt = Date.now();
2291
981
  const bodyEnd = p.appMarkerAt ?? p.lines.length;
2292
982
  const base = {
@@ -2300,9 +990,9 @@ export async function mercuryCardsFim(vctx, o) {
2300
990
  completionTokens: 0,
2301
991
  };
2302
992
  if (o.apiKey === undefined || o.apiKey.length === 0)
2303
- return { reply: o.draft.reply, receipt: { status: "no-key", ...base, ms: Date.now() - startedAt } };
993
+ return { reply: o.brief.reply, receipt: { status: "no-key", ...base, ms: Date.now() - startedAt } };
2304
994
  if (p.cardLines.length === 0)
2305
- return { reply: o.draft.reply, receipt: { status: "no-cards", ...base, ms: Date.now() - startedAt } };
995
+ return { reply: o.brief.reply, receipt: { status: "no-cards", ...base, ms: Date.now() - startedAt } };
2306
996
  const apiKey = o.apiKey;
2307
997
  const budgetMs = o.budgetMs ?? MERCURY_FIM_BUDGET_MS;
2308
998
  const callMs = o.deadlineMs ?? MERCURY_FIM_CALL_DEADLINE_MS;
@@ -2367,67 +1057,25 @@ export async function mercuryCardsFim(vctx, o) {
2367
1057
  };
2368
1058
  }
2369
1059
  export function assembleMercuryCardsCreateVibe(o) {
2370
- const s = o.draft.shape;
1060
+ const s = o.brief.shape;
2371
1061
  const nature = mercuryNature(s, o.personWords);
2372
- const rooms = sanitizeRoomShortlist("pouf", o.draft.rooms?.rooms);
1062
+ const rooms = sanitizeRoomShortlist("pouf", o.brief.rooms?.rooms);
2373
1063
  return {
2374
1064
  prompt: o.personWords,
2375
- enrichedPrompt: mercuryTesterReplyEnrichedPrompt(o.reply, o.personWords),
1065
+ enrichedPrompt: mercuryTesterReplyEnrichedPrompt(o.reply, o.personWords, s),
2376
1066
  skills: mercurySkills(s, { personWords: o.personWords }),
2377
- ...(o.draft.address.pairs.length === 0 ? {} : { pairs: o.draft.address.pairs.map((p) => ({ title: p.title, slug: p.slug })) }),
1067
+ ...(o.brief.address.pairs.length === 0 ? {} : { pairs: o.brief.address.pairs.map((p) => ({ title: p.title, slug: p.slug })) }),
2378
1068
  nature,
2379
1069
  ...(nature === "sensitive" ? { privacySensitive: true } : {}),
2380
1070
  ...(rooms === undefined ? {} : { rooms: [...rooms] }),
2381
- ...(o.draft.font?.font === undefined ? {} : { font: o.draft.font.font }),
2382
- };
2383
- }
2384
- export function mercuryCardsPreamble(draft, personWords) {
2385
- if (draft.parsed.preamble.length > 0)
2386
- return draft.parsed.preamble;
2387
- return `I'm making ${draft.address.pairs[0]?.title ?? "your app"} for you: ${personWords}.`;
2388
- }
2389
- function takeOf(g) {
2390
- return {
2391
- opening: g.texts.get("opening") ?? "",
2392
- access: g.texts.get("access") ?? "",
2393
- seed: g.texts.get("seed") ?? "",
2394
- look: g.texts.get("look") ?? "",
2395
- ...(g.records?.screens === undefined ? {} : { records: g.records.screens }),
2396
- ...(g.rules?.screens === undefined ? {} : { rules: g.rules.screens }),
2397
- ...(g.screens?.screens === undefined ? {} : { screens: g.screens.screens }),
2398
- ...(g.story?.story === undefined ? {} : { story: g.story.story }),
2399
- ...(g.preamble === undefined ? {} : { preamble: g.preamble }),
1071
+ ...(o.brief.font?.font === undefined ? {} : { font: o.brief.font.font }),
2400
1072
  };
2401
1073
  }
2402
- function owedBriefFields(gated) {
2403
- return MERCURY_BRIEF_FIELDS.filter((f) => (f === "pairs" ? gated.pairs === undefined : gated.texts.has(f) === false));
2404
- }
2405
- const replyObject = lazyType(() => type("Record<string, unknown>"));
2406
- export function coerceMercuryText(v) {
2407
- if (typeof v === "string")
2408
- return { text: v, coerced: false };
2409
- const leaves = (x) => {
2410
- if (typeof x === "string")
2411
- return x.trim().length === 0 ? undefined : x;
2412
- if (Array.isArray(x)) {
2413
- const parts = x.map(leaves).filter((t) => t !== undefined);
2414
- return parts.length === 0 ? undefined : parts.join("; ");
2415
- }
2416
- if (x !== null && typeof x === "object") {
2417
- const parts = Object.entries(x).flatMap(([k, val]) => {
2418
- const t = leaves(val);
2419
- return t === undefined ? [] : [`${k}: ${t}`];
2420
- });
2421
- return parts.length === 0 ? undefined : parts.join("; ");
2422
- }
2423
- return undefined;
2424
- };
2425
- if (v === null || typeof v !== "object")
2426
- return { coerced: false };
2427
- const text = leaves(v);
2428
- return text === undefined ? { coerced: false } : { text, coerced: true };
1074
+ export function mercuryCardsPreamble(brief, personWords) {
1075
+ if (brief.parsed.preamble.length > 0)
1076
+ return brief.parsed.preamble;
1077
+ return `I'm making ${brief.address.pairs[0]?.title ?? "your app"} for you: ${personWords}.`;
2429
1078
  }
2430
- export const MERCURY_DROP_OVER_BOUND = 3;
2431
1079
  function titleFromSlug(slug) {
2432
1080
  return slug
2433
1081
  .split("-")
@@ -2447,164 +1095,6 @@ function looseMercuryAddress(title, slugs) {
2447
1095
  const addresses = kept.length > 0 ? kept : [kebab(name)];
2448
1096
  return { pairs: addresses.map((slug) => ({ title: sent, slug })), fallback: kept.length === 0, tagged: cut.repaired };
2449
1097
  }
2450
- function acceptBriefFields(json, o) {
2451
- const parsed = replyObject(json);
2452
- const notAnObject = Array.isArray(json) || parsed instanceof type.errors;
2453
- const salvaged = notAnObject ? MERCURY_ADDRESS.salvage(json) : undefined;
2454
- const body = notAnObject ? (salvaged ?? {}) : parsed;
2455
- const failed = [];
2456
- const keepsNothing = o.shape.archetype === "toy" && o.shape.recordNames.length === 0;
2457
- const soft = (field, misses) => {
2458
- if (misses.length === 0)
2459
- return;
2460
- o.into.softMisses.set(field, [...(o.into.softMisses.get(field) ?? []), ...misses]);
2461
- };
2462
- const text = (key) => {
2463
- const c = coerceMercuryText(body[key]);
2464
- if (c.coerced)
2465
- o.into.coerced.add(key);
2466
- return c.text;
2467
- };
2468
- if (o.roomsOwed === true && "rooms" in body)
2469
- o.into.rooms = gateMercuryRooms(body.rooms);
2470
- if (o.fontOwed === true && "font" in body)
2471
- o.into.font = gateMercuryFont(body.font);
2472
- if (o.screensOwed === true && "screens" in body) {
2473
- const t = text("screens");
2474
- o.into.screens = gateMercuryScreens(t ?? body.screens);
2475
- }
2476
- if (o.recordsOwed === true) {
2477
- for (const key of ["records", "rules"]) {
2478
- if (o.into[key] !== undefined)
2479
- continue;
2480
- if (!(key in body)) {
2481
- soft(key, ["not-a-string"]);
2482
- continue;
2483
- }
2484
- const t = text(key);
2485
- o.into[key] = gateMercuryScreens(t ?? body[key]);
2486
- }
2487
- }
2488
- if (o.storyOwed === true && "story" in body) {
2489
- const t = text("story");
2490
- const g = gateMercuryStory(t ?? body.story);
2491
- o.into.story = g;
2492
- soft("story", g.misses ?? []);
2493
- }
2494
- if (o.preambleOwed === true && "preamble" in body) {
2495
- const t = text("preamble");
2496
- if (t === undefined || cleanMercuryText(t).length === 0)
2497
- soft("preamble", ["empty"]);
2498
- else {
2499
- soft("preamble", gateMercuryWholePreamble(t));
2500
- o.into.preamble = cleanMercuryText(t);
2501
- }
2502
- }
2503
- for (const f of o.owed) {
2504
- if (f === "pairs") {
2505
- const decoded = MERCURY_ADDRESS.decode(body);
2506
- if (decoded.isErr()) {
2507
- failed.push(`pairs:${decoded.Err().message}`);
2508
- continue;
2509
- }
2510
- const d = decoded.Ok();
2511
- if (salvaged !== undefined)
2512
- o.into.addressRepair.add("salvage");
2513
- for (const r of d.repairs)
2514
- o.into.addressRepair.add(r);
2515
- const slugs = d.slugs.length > 0 ? d.slugs : (o.into.heldSlugs ?? []);
2516
- if (d.title === undefined && d.slugs.length > 0)
2517
- o.into.heldSlugs = d.slugs;
2518
- if (d.title === undefined && failed.length > 0) {
2519
- failed.push("pairs:title-missing");
2520
- continue;
2521
- }
2522
- const strict = d.title === undefined ? Result.Err("title-missing") : gateMercuryAddress({ title: d.title, slugs });
2523
- const address = strict.isOk() ? strict.Ok() : looseMercuryAddress(d.title, slugs);
2524
- if (address === undefined) {
2525
- failed.push(`pairs:${strict.isErr() ? strict.Err().message : "empty"}`);
2526
- continue;
2527
- }
2528
- if (strict.isErr()) {
2529
- soft("pairs", [strict.Err().message]);
2530
- if (d.title === undefined)
2531
- o.into.addressRepair.add("title-from-slug");
2532
- }
2533
- if (address.tagged)
2534
- o.into.misses.add("archetype-tag");
2535
- if (address.fallback)
2536
- o.into.addressRepair.add("slug-fallback");
2537
- o.into.pairs = address.pairs;
2538
- continue;
2539
- }
2540
- const v = text(replyKey(f));
2541
- if (v === undefined) {
2542
- if (f === "opening")
2543
- failed.push("opening:not-a-string");
2544
- else
2545
- soft(f, ["not-a-string"]);
2546
- continue;
2547
- }
2548
- const prepared = prepareMercuryField(f, v);
2549
- const fails = gateMercuryField({ field: f, value: prepared.gate, shape: o.shape });
2550
- if (f === "opening" && cleanMercuryText(prepared.value).length === 0) {
2551
- failed.push("opening:empty");
2552
- continue;
2553
- }
2554
- const [, hi] = FIELD_BOUNDS[f];
2555
- if (cleanMercuryText(prepared.value).length > hi * MERCURY_DROP_OVER_BOUND) {
2556
- if (f === "opening") {
2557
- failed.push("opening:long");
2558
- continue;
2559
- }
2560
- o.into.fieldsDropped.add(f);
2561
- o.into.texts.set(f, "");
2562
- continue;
2563
- }
2564
- soft(f, fails);
2565
- o.into.texts.set(f, prepared.value);
2566
- if (f !== "seed" || keepsNothing === false)
2567
- for (const m of prepared.misses)
2568
- o.into.misses.add(m);
2569
- }
2570
- return failed;
2571
- }
2572
- function softMissCount(g) {
2573
- return [...g.softMisses.values()].reduce((n, m) => n + m.length, 0);
2574
- }
2575
- export const MERCURY_BRIEF_FANOUT = 4;
2576
- export const MERCURY_SHAPE_MAX_TOKENS = 800;
2577
- export const MERCURY_PICK_WAIT_MS = 1500;
2578
- export const MERCURY_PICK_COUNT = 2;
2579
- export const MERCURY_DUO_PICK_COUNT = 3;
2580
- export const MERCURY_ONECALL_EXTRA_TOKENS = 400;
2581
- const BREADTH_CTRL = /\b(button|tap|taps|toggle|picker|stepper|chip|chips|slider|drag|swipe|stars|ribbon|stamp|badge|bars?|search|filter|tabs?|headline|confetti|animation|counter|dial|segmented|checkbox|strip|gauge|sparkline|progress|swatch|card|cards|input|field|dropdown|menu|list)\b/gi;
2582
- const BREADTH_STATE = /\b(overdue|sold out|resolved|streak|late|red|amber|green|dims|highlight\w*|pinned|large|big|biggest|newest|empty|disabled|colou?r|glow\w*|pulse\w*|flash\w*|turns|changes|pending|confirmed|done|active|fermenting|winning|leading|second|behind|waiting|missing|ahead|nearly|just|still|recently)\b/gi;
2583
- const BREADTH_DEEP = /\b(undo|long[- ]press|press and hold|drag(s|ged|ging)? to reorder|reorder|inline edit|edit(s|ed)? inline|in place|filter chips?|swipe)\b/gi;
2584
- function breadthNorm(w) {
2585
- const l = w.toLowerCase();
2586
- return l.length > 4 ? l.replace(/(es|s)$/, "") : l;
2587
- }
2588
- function distinctTerms(re, text) {
2589
- return new Set([...text.matchAll(re)].map((m) => breadthNorm(m[0]))).size;
2590
- }
2591
- export function mercuryBreadth(o) {
2592
- const t = o.screens ?? [o.opening, o.seed ?? "", o.access ?? ""].filter((p) => p.length > 0).join(" ");
2593
- return distinctTerms(BREADTH_CTRL, t) + distinctTerms(BREADTH_STATE, t) + 2 * distinctTerms(BREADTH_DEEP, t);
2594
- }
2595
- function freshGated() {
2596
- return {
2597
- texts: new Map(),
2598
- misses: new Set(),
2599
- addressRepair: new Set(),
2600
- softMisses: new Map(),
2601
- coerced: new Set(),
2602
- fieldsDropped: new Set(),
2603
- };
2604
- }
2605
- function gatedCount(g) {
2606
- return MERCURY_BRIEF_FIELDS.length - owedBriefFields(g).length;
2607
- }
2608
1098
  function billedTotals(usage, aborted) {
2609
1099
  const sum = (f) => usage.reduce((n, u) => n + f(u), 0);
2610
1100
  const scale = usage.length === 0 ? 0 : (usage.length + aborted) / usage.length;
@@ -2617,437 +1107,28 @@ function billedTotals(usage, aborted) {
2617
1107
  estimated: usage.length === 0 ? 0 : aborted,
2618
1108
  };
2619
1109
  }
2620
- async function mercuryBriefCall(vctx, o) {
2621
- const startedAt = Date.now();
2622
- const cancel = new AbortController();
2623
- const drafts = Array.from({ length: o.fanout }, (_, index) => ({
2624
- index,
2625
- words: index === 0 || o.words.length === 0 ? o.words : o.wordBag(o.words.length),
2626
- gated: freshGated(),
2627
- log: mercurySendLog(),
2628
- totals: { usage: [] },
2629
- valid: false,
2630
- aborted: false,
2631
- }));
2632
- const roomsOwed = (g) => o.askRooms && g.rooms === undefined;
2633
- const fontOwed = (g) => o.askRooms && g.font === undefined;
2634
- const screensOwed = (g) => o.depth === "sub-feature" && g.screens === undefined;
2635
- const storyOwed = (g) => o.reply === "whole" && g.story === undefined;
2636
- const preambleOwed = (g) => o.reply === "whole" && g.preamble === undefined;
2637
- const recordsOwed = (g) => o.recordsRules && (g.records === undefined || g.rules === undefined);
2638
- const request = (g, words, draft) => () => {
2639
- const owed = owedBriefFields(g);
2640
- const rooms = roomsOwed(g);
2641
- const font = fontOwed(g);
2642
- const screens = screensOwed(g);
2643
- const story = storyOwed(g);
2644
- const preamble = preambleOwed(g);
2645
- const records = recordsOwed(g);
2646
- const shape = g.shape ?? o.shape;
2647
- const opts = {
2648
- personWords: o.personWords,
2649
- fields: owed,
2650
- words,
2651
- style: o.style,
2652
- rooms,
2653
- font,
2654
- ...(o.juice === "examples" ? {} : { juice: o.juice }),
2655
- ...(o.depth === "none" ? {} : { depth: o.depth }),
2656
- screens,
2657
- ...(o.reply === "split" ? {} : { reply: o.reply, story, preamble }),
2658
- ...(o.recordsRules ? { recordsRules: true, recordsOwed: records } : {}),
2659
- };
2660
- return {
2661
- schema: mercuryBriefSchema(owed, {
2662
- rooms,
2663
- font,
2664
- ...(screens ? { screens } : {}),
2665
- ...(records ? { recordsRules: true } : {}),
2666
- ...(story ? { story } : {}),
2667
- ...(preamble ? { preamble } : {}),
2668
- ...(shape === undefined ? { classify: true } : {}),
2669
- }),
2670
- messages: shape === undefined ? mercuryOneCallBriefMessages({ ...opts, draft }) : mercuryBriefMessages({ ...opts, shape }),
2671
- };
2672
- };
2673
- const gate = (json, g) => {
2674
- if (g.shape === undefined && o.shape === undefined) {
2675
- const parsed = parseMercuryDraftShape(json, o.personWords);
2676
- if (parsed.isErr())
2677
- return [`classification:${parsed.Err().message}`];
2678
- g.shape = parsed.Ok();
2679
- }
2680
- const shape = g.shape ?? o.shape;
2681
- if (shape === undefined)
2682
- return ["classification:missing"];
2683
- return acceptBriefFields(json, {
2684
- owed: owedBriefFields(g),
2685
- shape,
2686
- into: g,
2687
- roomsOwed: roomsOwed(g),
2688
- fontOwed: fontOwed(g),
2689
- screensOwed: screensOwed(g),
2690
- storyOwed: storyOwed(g),
2691
- preambleOwed: preambleOwed(g),
2692
- recordsOwed: recordsOwed(g),
2693
- });
2694
- };
2695
- const maxTokens = o.planner === "opus"
2696
- ? MERCURY_OPUS_PLANNER_MAX_TOKENS
2697
- : (o.reply === "whole" ? 2400 : 1600) + (o.shape === undefined ? MERCURY_ONECALL_EXTRA_TOKENS : 0);
2698
- const attempt = {
2699
- call: "brief",
2700
- deadlineMs: o.deadlineMs,
2701
- budget: o.budget,
2702
- maxTokens,
2703
- ...(o.planner === "opus" ? { model: plannerModel("opus") } : {}),
2704
- };
2705
- let winner;
2706
- let winnerMs;
2707
- const candidates = [];
2708
- let pickTimer;
2709
- let pickWaitMs;
2710
- let partner;
2711
- let duoConflict;
2712
- const pickCount = o.duo ? o.duoPool : MERCURY_PICK_COUNT;
2713
- const pickReserveMs = o.reply === "whole" ? 0 : MERCURY_MIN_ATTEMPT_MS;
2714
- let won = () => undefined;
2715
- const firstValid = new Promise((resolve) => {
2716
- won = resolve;
2717
- });
2718
- const pick = () => {
2719
- if (winner !== undefined || candidates.length === 0)
2720
- return;
2721
- if (pickTimer !== undefined)
2722
- clearTimeout(pickTimer);
2723
- const ranked = [...candidates].sort((a, b) => a.softMisses - b.softMisses || b.breadth - a.breadth);
2724
- winner = ranked[0].draft;
2725
- if (o.duo && ranked.length > 1) {
2726
- const second = ranked[1].draft;
2727
- const shapeA = winner.gated.shape ?? o.shape;
2728
- const shapeB = second.gated.shape ?? o.shape;
2729
- duoConflict =
2730
- o.recordsRules && shapeA !== undefined && shapeB !== undefined
2731
- ? mercuryDuoConflict({ shape: shapeA, access: winner.gated.texts.get("access") ?? "" }, { shape: shapeB, access: second.gated.texts.get("access") ?? "" }, o.personWords)
2732
- : undefined;
2733
- if (duoConflict === undefined)
2734
- partner = { draft: second, breadth: ranked[1].breadth };
2735
- }
2736
- winnerMs = Date.now() - startedAt;
2737
- for (const s of drafts)
2738
- if (s.ended === undefined && s.log.inFlight === true)
2739
- s.aborted = true;
2740
- cancel.abort();
2741
- won();
2742
- };
2743
- const everyDraft = Promise.allSettled(drafts.map(async function runDraft(d) {
2744
- const label = `draft ${d.index}: `;
2745
- const a = await mercuryAttempt(vctx, { ...attempt, request: request(d.gated, d.words, d.index), cancel: cancel.signal, label, transientRetries: 0 }, d.log);
2746
- if (winner !== undefined)
2747
- return;
2748
- d.ended = a.kind;
2749
- if (a.kind === "cancelled" && a.inFlight)
2750
- d.aborted = true;
2751
- if (a.kind !== "reply")
2752
- return;
2753
- absorbReply(d.totals, a.reply);
2754
- d.text = a.reply.text;
2755
- const failed = gate(a.reply.json, d.gated);
2756
- if (failed.length === 0) {
2757
- d.valid = true;
2758
- const breadth = mercuryBreadth({
2759
- ...(d.gated.screens?.screens === undefined ? {} : { screens: d.gated.screens.screens }),
2760
- opening: d.gated.texts.get("opening") ?? "",
2761
- seed: d.gated.texts.get("seed") ?? "",
2762
- access: d.gated.texts.get("access") ?? "",
2763
- });
2764
- candidates.push({ draft: d, breadth, softMisses: softMissCount(d.gated), ms: Date.now() - startedAt });
2765
- if (candidates.length >= pickCount)
2766
- pick();
2767
- else if (candidates.length === 1) {
2768
- const blind = !o.duo && candidates[0].breadth === 0 && candidates[0].softMisses === 0;
2769
- pickWaitMs = blind ? 0 : Math.max(0, Math.min(MERCURY_PICK_WAIT_MS, o.budget.remainingMs() - pickReserveMs));
2770
- if (pickWaitMs === 0)
2771
- pick();
2772
- else
2773
- pickTimer = setTimeout(pick, pickWaitMs);
2774
- }
2775
- return;
2776
- }
2777
- d.log.lastError = `mercury brief: gate ${failed.join(", ")}`;
2778
- d.log.cause = "gate";
2779
- noteSend(d.log, label);
2780
- }));
2781
- await Promise.race([firstValid, everyDraft]);
2782
- pick();
2783
- const totals = { usage: drafts.flatMap((d) => d.totals.usage) };
2784
- for (const d of drafts) {
2785
- if (d.totals.servedModel !== undefined)
2786
- totals.servedModel = d.totals.servedModel;
2787
- if (d.totals.reasoningTokens !== undefined)
2788
- totals.reasoningTokens = (totals.reasoningTokens ?? 0) + d.totals.reasoningTokens;
2789
- }
2790
- const log = {
2791
- ...mercurySendLog(),
2792
- sends: drafts.reduce((n, d) => n + d.log.sends, 0),
2793
- retries: drafts.reduce((n, d) => n + d.log.retries, 0),
2794
- history: drafts.flatMap((d) => d.log.history),
2795
- };
2796
- const chosen = winner ?? drafts.reduce((best, d) => (gatedCount(d.gated) > gatedCount(best.gated) ? d : best), drafts[0]);
2797
- log.cause = chosen.log.cause;
2798
- log.lastError = chosen.log.lastError;
2799
- const draftUsage = totals.usage.length;
2800
- let attempts = drafts.some((d) => d.ended === "reply" || d.ended === "failed") ? 1 : 0;
2801
- let ok = winner !== undefined;
2802
- let text = winner?.text;
2803
- let partialText;
2804
- const throttled = drafts.filter((d) => d.ended === "failed" && (d.log.cause === "rate-limited" || d.log.cause === "upstream-error")).length;
2805
- let throttleStop = false;
2806
- if (ok === false && throttled === o.fanout) {
2807
- const retryAfters = drafts.flatMap((d) => (d.log.retryAfterMs === undefined ? [] : [d.log.retryAfterMs]));
2808
- const wait = retryAfters.length > 0 ? Math.max(...retryAfters) : (mercuryRetryWaitMs(1, undefined) ?? 0);
2809
- if (wait > o.budget.remainingMs() - MERCURY_MIN_ATTEMPT_MS) {
2810
- throttleStop = true;
2811
- log.lastError = `${chosen.log.lastError} (retry-after ${wait}ms; falling back inside the stage budget)`;
2812
- }
2813
- else {
2814
- await waitMs(wait);
2815
- log.retries += 1;
2816
- }
2817
- }
2818
- if (ok === false && attempts === 1 && throttleStop === false) {
2819
- const reask = mercurySendLog();
2820
- reask.cause = chosen.log.cause;
2821
- reask.lastError = chosen.log.lastError;
2822
- reask.carried = true;
2823
- const a = await mercuryAttempt(vctx, { ...attempt, request: request(chosen.gated, chosen.words, chosen.index) }, reask);
2824
- log.sends += reask.sends;
2825
- log.retries += reask.retries;
2826
- log.history.push(...reask.history);
2827
- log.cause = reask.cause;
2828
- log.lastError = reask.lastError;
2829
- if (a.kind === "reply" || a.kind === "failed")
2830
- attempts += 1;
2831
- if (a.kind === "reply") {
2832
- absorbReply(totals, a.reply);
2833
- const failed = gate(a.reply.json, chosen.gated);
2834
- if (failed.length === 0) {
2835
- ok = true;
2836
- text = a.reply.text;
2837
- partialText = chosen.text;
2838
- }
2839
- else {
2840
- log.lastError = `mercury brief: gate ${failed.join(", ")}`;
2841
- log.cause = "gate";
2842
- noteSend(log);
2843
- }
2844
- }
2845
- }
2846
- const aborted = drafts.filter((d) => d.aborted).length;
2847
- const pickCandidates = candidates.map((c) => ({
2848
- draft: c.draft.index,
2849
- breadth: c.breadth,
2850
- softMisses: c.softMisses,
2851
- ms: c.ms,
2852
- ...(c.draft.gated.shape === undefined ? {} : { archetype: c.draft.gated.shape.archetype }),
2853
- }));
2854
- const billedDrafts = billedTotals(totals.usage.slice(0, draftUsage), aborted);
2855
- const reaskUsage = totals.usage.slice(draftUsage);
2856
- const fanout = {
2857
- drafts: drafts.filter((d) => d.log.sends > 0).length,
2858
- valid: drafts.filter((d) => d.valid).length,
2859
- ...(winner === undefined ? {} : { winner: winner.index }),
2860
- ...(winnerMs === undefined ? {} : { winnerMs }),
2861
- ...(winner === undefined
2862
- ? {}
2863
- : {
2864
- picked: winner.index,
2865
- candidates: pickCandidates,
2866
- ...(pickWaitMs === undefined ? {} : { pickWaitMs }),
2867
- }),
2868
- aborted,
2869
- ...(throttled === 0 ? {} : { throttled }),
2870
- billed: reaskUsage.length === 0
2871
- ? billedDrafts
2872
- : {
2873
- ...billedDrafts,
2874
- prompt_tokens: billedDrafts.prompt_tokens + reaskUsage.reduce((n, u) => n + u.prompt_tokens, 0),
2875
- completion_tokens: billedDrafts.completion_tokens + reaskUsage.reduce((n, u) => n + u.completion_tokens, 0),
2876
- total_tokens: billedDrafts.total_tokens + reaskUsage.reduce((n, u) => n + u.total_tokens, 0),
2877
- ...(billedDrafts.cost === undefined && reaskUsage.every((u) => u.cost === undefined)
2878
- ? {}
2879
- : { cost: (billedDrafts.cost ?? 0) + reaskUsage.reduce((n, u) => n + (u.cost ?? 0), 0) }),
2880
- },
2881
- };
2882
- const g = chosen.gated;
2883
- const pickedBreadth = candidates.find((c) => c.draft === winner)?.breadth;
2884
- const pair = winner === undefined ? [] : [winner, ...(partner === undefined ? [] : [partner.draft])];
2885
- const pairArchetypes = pair.flatMap((d) => (d.gated.shape === undefined ? [] : [d.gated.shape.archetype]));
2886
- const duo = o.duo && winner !== undefined && pickedBreadth !== undefined
2887
- ? {
2888
- drafts: pair.map((d) => d.index),
2889
- breadth: [pickedBreadth, ...(partner === undefined ? [] : [partner.breadth])],
2890
- ...(o.shape === undefined
2891
- ? {
2892
- pool: o.duoPool,
2893
- candidates: pickCandidates,
2894
- archetypes: pairArchetypes,
2895
- archetypeSplit: new Set(pairArchetypes).size > 1,
2896
- }
2897
- : {}),
2898
- ...(duoConflict === undefined ? {} : { conflict: duoConflict }),
2899
- }
2900
- : undefined;
2901
- const backendWorded = mercuryBackendWordsNamed(o.personWords);
2902
- const backendCleared = backendWorded
2903
- ? []
2904
- : drafts.filter((d) => d !== chosen && d.gated.shape?.flags.backend === true).map((d) => d.index);
2905
- const oneCall = o.shape === undefined
2906
- ? {
2907
- draftArchetypes: drafts.map((d) => d.gated.shape?.archetype ?? null),
2908
- archetypeSplit: new Set(drafts.flatMap((d) => (d.gated.shape === undefined ? [] : [d.gated.shape.archetype]))).size > 1,
2909
- flagsWinner: g.shape === undefined ? [] : setFlags(g.shape.flags),
2910
- flagsUnion: MERCURY_FLAGS.filter((f) => drafts.some((d) => d.gated.shape?.flags[f] === true && (f !== "backend" || backendWorded || d === chosen))),
2911
- ...(backendCleared.length === 0 ? {} : { backendFilterCleared: backendCleared }),
2912
- ...(backendWorded || g.shape?.flags.backend !== true ? {} : { backendFilterKept: chosen.index }),
2913
- }
2914
- : undefined;
2915
- const extra = {
2916
- passedThrough: [...g.misses],
2917
- addressRepair: [...g.addressRepair],
2918
- ...(g.pairs === undefined ? {} : { slugsWithWord: mercurySlugsWithWord(g.pairs, chosen.words) }),
2919
- ...(g.rooms === undefined ? {} : { roomsDropped: g.rooms.dropped }),
2920
- ...(g.font === undefined ? {} : { fontDropped: g.font.dropped }),
2921
- ...(g.screens === undefined ? {} : { screensDropped: g.screens.dropped }),
2922
- fanout,
2923
- juice: o.juice,
2924
- depth: o.depth,
2925
- ...(o.reply === "split"
2926
- ? {}
2927
- : {
2928
- reply: o.reply,
2929
- preambleFrom: "brief",
2930
- storyChars: g.story?.story === undefined ? 0 : cleanMercuryText(g.story.story).length,
2931
- storyDropped: g.story?.dropped ?? 0,
2932
- ...(g.preamble === undefined ? {} : { preambleChars: g.preamble.length }),
2933
- }),
2934
- ...(duo === undefined ? {} : { duo: true, duoDrafts: duo.drafts, duoBreadth: duo.breadth }),
2935
- ...(duo?.pool === undefined ? {} : { duoPool: duo.pool }),
2936
- ...(duo?.archetypes === undefined ? {} : { duoArchetypes: duo.archetypes }),
2937
- ...(duo?.conflict === undefined ? {} : { duoConflict: true, duoConflictReason: duo.conflict }),
2938
- ...(o.recordsRules
2939
- ? {
2940
- recordsRules: true,
2941
- ...(g.records === undefined ? {} : { recordsDropped: g.records.dropped }),
2942
- ...(g.rules === undefined ? {} : { rulesDropped: g.rules.dropped }),
2943
- }
2944
- : {}),
2945
- ...(oneCall === undefined ? {} : { oneCall: true, shapeCall: false, ...oneCall }),
2946
- ...(g.softMisses.size === 0 ? {} : { softMisses: Object.fromEntries(g.softMisses) }),
2947
- ...(g.coerced.size === 0 ? {} : { coerced: [...g.coerced] }),
2948
- ...(g.fieldsDropped.size === 0 ? {} : { fieldsDropped: [...g.fieldsDropped] }),
2949
- ...(o.reply === "whole" && g.preamble === undefined ? { preambleFallback: 1 } : {}),
2950
- ...(o.planner === "opus" ? { planner: "opus" } : {}),
2951
- ...(text === undefined ? {} : { text }),
2952
- ...(partialText === undefined ? {} : { partialText }),
2953
- ...(duo === undefined ? {} : { duoTexts: pair.flatMap((d) => (d.text === undefined ? [] : [d.text])) }),
2954
- };
2955
- const call = { call: "brief", row: o.row, attempts, startedAt, totals, log, extra };
2956
- if (ok === false)
2957
- return failCall(vctx, call);
2958
- o.onCounts(log.sends, log.retries);
2959
- await recordMercuryRow(vctx, { ...o.row, record: callRecord({ ...call, ok: true }) });
2960
- return Result.Ok({
2961
- gated: g,
2962
- words: chosen.words,
2963
- ...(partner === undefined || winner === undefined ? {} : { partner: partner.draft.gated }),
2964
- ...(duo === undefined ? {} : { duo }),
2965
- ...(oneCall === undefined ? {} : { oneCall }),
2966
- });
2967
- }
2968
- export const MERCURY_PROSE_MAX_TOKENS = 3200;
2969
- async function mercuryProseBriefCall(vctx, o) {
2970
- return mercuryFirstValidCall(vctx, {
2971
- ...o,
2972
- maxTokens: MERCURY_PROSE_MAX_TOKENS,
2973
- request: (words) => ({
2974
- schema: mercuryProseBriefSchema({ rooms: o.askRooms, font: o.askRooms }),
2975
- messages: mercuryProseBriefMessages({ personWords: o.personWords, words, rooms: o.askRooms, font: o.askRooms }),
2976
- }),
2977
- gate: (json) => gateMercuryProseDraft(json, { personWords: o.personWords, askRooms: o.askRooms }),
2978
- extra: () => ({ format: "prose" }),
2979
- });
2980
- }
2981
- export const MERCURY_THINKALOUD_MAX_TOKENS = 16_000;
2982
- export const MERCURY_THINKALOUD_DEADLINE_MS = 20_000;
2983
- export const MERCURY_THINKALOUD_BUDGET_MS = 25_000;
2984
- async function mercuryThinkAloudBriefCall(vctx, o) {
2985
- return mercuryFirstValidCall(vctx, {
2986
- ...o,
2987
- persistent: true,
2988
- maxTokens: MERCURY_THINKALOUD_MAX_TOKENS,
2989
- request: (words) => ({
2990
- messages: mercuryThinkAloudMessages({ personWords: o.personWords, words, rooms: o.askRooms, font: o.askRooms }),
2991
- }),
2992
- gate: (raw) => gateMercuryThinkAloud(raw, { personWords: o.personWords, askRooms: o.askRooms }),
2993
- extra: (d) => ({
2994
- format: "thinkaloud",
2995
- ...(d === undefined
2996
- ? {}
2997
- : {
2998
- ...(Object.keys(d.softMisses).length === 0 ? {} : { softMisses: d.softMisses }),
2999
- thinkAloud: { narrationChars: d.narration.length, specChars: d.spec.length, guessLines: d.guessLines },
3000
- }),
3001
- }),
3002
- });
3003
- }
1110
+ export const MERCURY_CARDS_MAX_TOKENS = 16_000;
3004
1111
  async function mercuryCardsBriefCall(vctx, o) {
3005
- return mercuryFirstValidCall(vctx, {
3006
- ...o,
3007
- persistent: true,
3008
- maxTokens: MERCURY_THINKALOUD_MAX_TOKENS,
3009
- request: (words) => ({
3010
- messages: mercuryCardsMessages({ personWords: o.personWords, words, rooms: o.askRooms, font: o.askRooms }),
3011
- }),
3012
- gate: (raw) => gateMercuryCards(raw, { personWords: o.personWords, askRooms: o.askRooms }),
3013
- extra: (d) => ({
3014
- format: o.format,
3015
- ...(o.planner === "mercury" ? {} : { planner: o.planner }),
3016
- ...(d === undefined
3017
- ? {}
3018
- : {
3019
- ...(Object.keys(d.softMisses).length === 0 ? {} : { softMisses: d.softMisses }),
3020
- cards: {
3021
- preambleChars: d.parsed.preamble.length,
3022
- cards: d.parsed.cardLines.length,
3023
- acceptLines: d.parsed.acceptLines,
3024
- ruleLines: d.parsed.ruleLines,
3025
- guessLines: d.parsed.guessLines,
3026
- dataRecords: d.parsed.dataRecords.length,
3027
- },
3028
- }),
3029
- }),
3030
- });
3031
- }
3032
- async function mercuryFirstValidCall(vctx, o) {
3033
1112
  const startedAt = Date.now();
3034
1113
  const cancel = new AbortController();
3035
- const drafts = Array.from({ length: o.persistent === true ? MERCURY_TESTER_FANOUT : MERCURY_BRIEF_FANOUT }, (_, index) => ({
1114
+ const briefs = Array.from({ length: MERCURY_TESTER_FANOUT }, (_, index) => ({
3036
1115
  index,
3037
1116
  words: index === 0 || o.words.length === 0 ? o.words : o.wordBag(o.words.length),
3038
1117
  log: mercurySendLog(),
3039
1118
  totals: { usage: [] },
3040
1119
  aborted: false,
3041
1120
  }));
3042
- const request = (words) => () => o.request(words);
3043
- const gate = o.gate;
1121
+ const request = (words) => () => ({
1122
+ messages: mercuryCardsMessages({ personWords: o.personWords, words, rooms: o.askRooms, font: o.askRooms }),
1123
+ });
1124
+ const gate = (raw) => gateMercuryCards(raw, { personWords: o.personWords, askRooms: o.askRooms });
3044
1125
  const attempt = {
3045
1126
  call: "brief",
3046
1127
  deadlineMs: o.deadlineMs,
3047
1128
  budget: o.budget,
3048
- maxTokens: o.maxTokens,
3049
- ...(o.planner === undefined || o.planner === "mercury" ? {} : { model: plannerModel(o.planner) }),
3050
- ...(o.persistent === true ? { persistent: true } : {}),
1129
+ maxTokens: MERCURY_CARDS_MAX_TOKENS,
1130
+ ...(o.planner === "mercury" ? {} : { model: plannerModel(o.planner) }),
1131
+ persistent: true,
3051
1132
  };
3052
1133
  let winner;
3053
1134
  let valid = 0;
@@ -3055,15 +1136,9 @@ async function mercuryFirstValidCall(vctx, o) {
3055
1136
  const firstValid = new Promise((resolve) => {
3056
1137
  won = resolve;
3057
1138
  });
3058
- const everyDraft = Promise.allSettled(drafts.map(async function runDraft(d) {
3059
- const label = `draft ${d.index}: `;
3060
- const a = await mercuryAttempt(vctx, {
3061
- ...attempt,
3062
- request: request(d.words),
3063
- cancel: cancel.signal,
3064
- label,
3065
- ...(o.persistent === true ? {} : { transientRetries: 0 }),
3066
- }, d.log);
1139
+ async function runBrief(d, once = false) {
1140
+ const label = `brief ${d.index}: `;
1141
+ const a = await mercuryAttempt(vctx, { ...attempt, request: request(d.words), cancel: cancel.signal, label, persistent: once === false }, d.log);
3067
1142
  if (winner !== undefined)
3068
1143
  return;
3069
1144
  d.ended = a.kind;
@@ -3072,11 +1147,11 @@ async function mercuryFirstValidCall(vctx, o) {
3072
1147
  if (a.kind !== "reply")
3073
1148
  return;
3074
1149
  absorbReply(d.totals, a.reply);
3075
- const g = gate(a.reply.json);
1150
+ const g = gate(a.reply.text);
3076
1151
  if (g.isOk()) {
3077
1152
  valid += 1;
3078
- winner = { from: d, draft: g.Ok(), ms: Date.now() - startedAt, text: a.reply.text };
3079
- for (const s of drafts)
1153
+ winner = { from: d, brief: g.Ok(), ms: Date.now() - startedAt, text: a.reply.text };
1154
+ for (const s of briefs)
3080
1155
  if (s.ended === undefined && s.log.inFlight === true)
3081
1156
  s.aborted = true;
3082
1157
  cancel.abort();
@@ -3086,33 +1161,65 @@ async function mercuryFirstValidCall(vctx, o) {
3086
1161
  d.log.lastError = `mercury brief: gate ${g.Err().message}`;
3087
1162
  d.log.cause = "gate";
3088
1163
  noteSend(d.log, label);
3089
- }));
3090
- await Promise.race([firstValid, everyDraft]);
3091
- const totals = { usage: drafts.flatMap((d) => d.totals.usage) };
3092
- for (const d of drafts) {
1164
+ }
1165
+ let hedgedMs;
1166
+ const firstEnded = new AbortController();
1167
+ async function hedgeWhenHung() {
1168
+ if (o.hedgeAfterMs === undefined)
1169
+ return;
1170
+ const first = briefs[0];
1171
+ await waitOrCancel(o.hedgeAfterMs, eitherSignal(eitherSignal(cancel.signal, o.budget.cancel), firstEnded.signal));
1172
+ if (winner !== undefined || isCancelled(cancel.signal) || o.budget.cancel?.aborted === true)
1173
+ return;
1174
+ if (first.ended !== undefined || first.log.inFlight !== true || first.log.sends !== 1)
1175
+ return;
1176
+ if (o.budget.remainingMs() < MERCURY_MIN_ATTEMPT_MS)
1177
+ return;
1178
+ const hedge = {
1179
+ index: briefs.length,
1180
+ words: o.words.length === 0 ? o.words : o.wordBag(o.words.length),
1181
+ log: mercurySendLog(),
1182
+ totals: { usage: [] },
1183
+ aborted: false,
1184
+ };
1185
+ briefs.push(hedge);
1186
+ hedgedMs = Date.now() - startedAt;
1187
+ await runBrief(hedge, true);
1188
+ }
1189
+ const everyBrief = Promise.allSettled([
1190
+ ...briefs.map(async (d) => {
1191
+ await runBrief(d);
1192
+ if (d.index === 0)
1193
+ firstEnded.abort();
1194
+ }),
1195
+ hedgeWhenHung(),
1196
+ ]);
1197
+ await Promise.race([firstValid, everyBrief]);
1198
+ const totals = { usage: briefs.flatMap((d) => d.totals.usage) };
1199
+ for (const d of briefs) {
3093
1200
  if (d.totals.servedModel !== undefined)
3094
1201
  totals.servedModel = d.totals.servedModel;
3095
1202
  if (d.totals.reasoningTokens !== undefined)
3096
1203
  totals.reasoningTokens = (totals.reasoningTokens ?? 0) + d.totals.reasoningTokens;
3097
1204
  }
3098
- const last = drafts.find((d) => d.ended === "reply") ?? drafts[0];
1205
+ const last = briefs.find((d) => d.ended === "reply") ?? briefs[0];
3099
1206
  const log = {
3100
1207
  ...mercurySendLog(),
3101
- sends: drafts.reduce((n, d) => n + d.log.sends, 0),
3102
- retries: drafts.reduce((n, d) => n + d.log.retries, 0),
3103
- history: drafts.flatMap((d) => d.log.history),
1208
+ sends: briefs.reduce((n, d) => n + d.log.sends, 0),
1209
+ retries: briefs.reduce((n, d) => n + d.log.retries, 0),
1210
+ history: briefs.flatMap((d) => d.log.history),
3104
1211
  cause: last.log.cause,
3105
1212
  lastError: last.log.lastError,
3106
1213
  };
3107
- const draftUsage = totals.usage.length;
3108
- let attempts = drafts.some((d) => d.ended === "reply" || d.ended === "failed") ? 1 : 0;
1214
+ const briefUsage = totals.usage.length;
1215
+ let attempts = briefs.some((d) => d.ended === "reply" || d.ended === "failed") ? 1 : 0;
3109
1216
  let chosen = winner;
3110
1217
  if (chosen === undefined && attempts === 1) {
3111
1218
  const reask = mercurySendLog();
3112
1219
  reask.cause = log.cause;
3113
1220
  reask.lastError = log.lastError;
3114
1221
  reask.carried = true;
3115
- const a = await mercuryAttempt(vctx, { ...attempt, request: request(drafts[0].words) }, reask);
1222
+ const a = await mercuryAttempt(vctx, { ...attempt, request: request(briefs[0].words) }, reask);
3116
1223
  log.sends += reask.sends;
3117
1224
  log.retries += reask.retries;
3118
1225
  log.history.push(...reask.history);
@@ -3122,10 +1229,10 @@ async function mercuryFirstValidCall(vctx, o) {
3122
1229
  attempts += 1;
3123
1230
  if (a.kind === "reply") {
3124
1231
  absorbReply(totals, a.reply);
3125
- const g = gate(a.reply.json);
1232
+ const g = gate(a.reply.text);
3126
1233
  if (g.isOk()) {
3127
1234
  valid += 1;
3128
- chosen = { from: drafts[0], draft: g.Ok(), ms: Date.now() - startedAt, text: a.reply.text };
1235
+ chosen = { from: briefs[0], brief: g.Ok(), ms: Date.now() - startedAt, text: a.reply.text };
3129
1236
  }
3130
1237
  else {
3131
1238
  log.lastError = `mercury brief: gate ${g.Err().message}`;
@@ -3134,37 +1241,44 @@ async function mercuryFirstValidCall(vctx, o) {
3134
1241
  }
3135
1242
  }
3136
1243
  }
3137
- const aborted = drafts.filter((d) => d.aborted).length;
3138
- const billedDrafts = billedTotals(totals.usage.slice(0, draftUsage), aborted);
3139
- const reaskUsage = totals.usage.slice(draftUsage);
1244
+ const aborted = briefs.filter((d) => d.aborted).length;
1245
+ const billedBriefs = billedTotals(totals.usage.slice(0, briefUsage), aborted);
1246
+ const reaskUsage = totals.usage.slice(briefUsage);
3140
1247
  const fanout = {
3141
- drafts: drafts.filter((d) => d.log.sends > 0).length,
1248
+ briefs: briefs.filter((d) => d.log.sends > 0).length,
3142
1249
  valid,
3143
1250
  ...(winner === undefined ? {} : { winner: winner.from.index, winnerMs: winner.ms, picked: winner.from.index }),
3144
1251
  aborted,
1252
+ ...(hedgedMs === undefined ? {} : { hedgedMs }),
3145
1253
  billed: reaskUsage.length === 0
3146
- ? billedDrafts
1254
+ ? billedBriefs
3147
1255
  : {
3148
- ...billedDrafts,
3149
- prompt_tokens: billedDrafts.prompt_tokens + reaskUsage.reduce((n, u) => n + u.prompt_tokens, 0),
3150
- completion_tokens: billedDrafts.completion_tokens + reaskUsage.reduce((n, u) => n + u.completion_tokens, 0),
3151
- total_tokens: billedDrafts.total_tokens + reaskUsage.reduce((n, u) => n + u.total_tokens, 0),
3152
- ...(billedDrafts.cost === undefined && reaskUsage.every((u) => u.cost === undefined)
1256
+ ...billedBriefs,
1257
+ prompt_tokens: billedBriefs.prompt_tokens + reaskUsage.reduce((n, u) => n + u.prompt_tokens, 0),
1258
+ completion_tokens: billedBriefs.completion_tokens + reaskUsage.reduce((n, u) => n + u.completion_tokens, 0),
1259
+ total_tokens: billedBriefs.total_tokens + reaskUsage.reduce((n, u) => n + u.total_tokens, 0),
1260
+ ...(billedBriefs.cost === undefined && reaskUsage.every((u) => u.cost === undefined)
3153
1261
  ? {}
3154
- : { cost: (billedDrafts.cost ?? 0) + reaskUsage.reduce((n, u) => n + (u.cost ?? 0), 0) }),
1262
+ : { cost: (billedBriefs.cost ?? 0) + reaskUsage.reduce((n, u) => n + (u.cost ?? 0), 0) }),
3155
1263
  },
3156
1264
  };
3157
- const c = chosen?.draft;
1265
+ const c = chosen?.brief;
3158
1266
  const extra = {
3159
- ...o.extra(c),
1267
+ format: o.format,
1268
+ ...(o.planner === "mercury" ? {} : { planner: o.planner }),
3160
1269
  fanout,
3161
- reply: "whole",
3162
- preambleFrom: "classification",
3163
- oneCall: true,
3164
- shapeCall: false,
3165
1270
  ...(c === undefined
3166
1271
  ? {}
3167
1272
  : {
1273
+ ...(Object.keys(c.softMisses).length === 0 ? {} : { softMisses: c.softMisses }),
1274
+ cards: {
1275
+ preambleChars: c.parsed.preamble.length,
1276
+ cards: c.parsed.cardLines.length,
1277
+ acceptLines: c.parsed.acceptLines,
1278
+ ruleLines: c.parsed.ruleLines,
1279
+ guessLines: c.parsed.guessLines,
1280
+ dataRecords: c.parsed.dataRecords.length,
1281
+ },
3168
1282
  addressRepair: [...c.addressRepair],
3169
1283
  slugsWithWord: mercurySlugsWithWord(c.address.pairs, chosen?.from.words ?? []),
3170
1284
  ...(c.rooms === undefined ? {} : { roomsDropped: c.rooms.dropped }),
@@ -3178,16 +1292,11 @@ async function mercuryFirstValidCall(vctx, o) {
3178
1292
  return failCall(vctx, call);
3179
1293
  o.onCounts(log.sends, log.retries);
3180
1294
  await recordMercuryRow(vctx, { ...o.row, record: callRecord({ ...call, ok: true }) });
3181
- return Result.Ok({ draft: chosen.draft, words: chosen.from.words });
1295
+ return Result.Ok({ brief: chosen.brief, words: chosen.from.words });
3182
1296
  }
3183
1297
  export async function runMercuryFirstBuild(vctx, args) {
3184
1298
  const startedAt = Date.now();
3185
- const breaker = args.breaker ??
3186
- (args.planner === "opus"
3187
- ? MERCURY_OPUS_PLANNER_BREAKER
3188
- : args.planner === "haiku"
3189
- ? MERCURY_HAIKU_PLANNER_BREAKER
3190
- : MERCURY_BREAKER);
1299
+ const breaker = args.breaker ?? (args.planner === "haiku" ? MERCURY_HAIKU_PLANNER_BREAKER : MERCURY_BREAKER);
3191
1300
  const stage = { sends: 0, retries: 0 };
3192
1301
  if (args.personWords.trim().length === 0)
3193
1302
  return stageErr("mercury: no words to brief from", "error", stage, startedAt);
@@ -3216,9 +1325,8 @@ export async function runMercuryFirstBuild(vctx, args) {
3216
1325
  }
3217
1326
  return out;
3218
1327
  }
3219
- function stageErr(message, cause, stage, startedAt, call) {
1328
+ function stageErr(message, cause, stage, startedAt) {
3220
1329
  return Result.Err(new MercuryStageError(message, cause, {
3221
- ...(call === undefined ? {} : { call }),
3222
1330
  sends: stage.sends,
3223
1331
  retries: stage.retries,
3224
1332
  elapsedMs: Date.now() - startedAt,
@@ -3239,277 +1347,72 @@ async function pipeline(vctx, args, stage) {
3239
1347
  },
3240
1348
  };
3241
1349
  const named = () => (keepText && lastRow !== undefined ? { rowPromptId: lastRow } : {});
3242
- const opus = args.planner === "opus";
3243
- const haiku = args.planner === "haiku";
3244
- const thinkAloud = args.format === "thinkaloud";
3245
- const cards = args.format === "cards" || args.format === "cards-fim";
3246
- const freeText = thinkAloud || cards;
3247
- const deadlineMs = args.deadlineMs ??
3248
- (opus ? MERCURY_OPUS_PLANNER_DEADLINE_MS : haiku || freeText ? MERCURY_TESTER_REQUEST_DEADLINE_MS : MERCURY_CALL_DEADLINE_MS);
3249
- const budget = mercuryBudget(args.budgetMs ??
3250
- (opus ? MERCURY_OPUS_PLANNER_BUDGET_MS : haiku || freeText ? MERCURY_TESTER_STAGE_BUDGET_MS : MERCURY_STAGE_BUDGET_MS), Date.now, args.rateLimitedBudgetMs, args.signal);
3251
- const wordBag = args.wordBag ?? slugWordBag;
3252
- const style = args.style ?? "base";
3253
- const askRooms = args.rooms === true;
3254
- const reply = args.reply ?? "split";
3255
- const recordsRules = reply === "whole" && args.recordsRules === true;
3256
- function tally(r) {
3257
- if (r.isOk())
3258
- return r;
3259
- const e = r.Err();
1350
+ const planner = args.planner ?? "mercury";
1351
+ const format = args.format;
1352
+ const budget = mercuryBudget(args.budgetMs ?? MERCURY_TESTER_STAGE_BUDGET_MS, Date.now, args.signal);
1353
+ const rCards = await mercuryCardsBriefCall(vctx, {
1354
+ row,
1355
+ deadlineMs: args.deadlineMs ?? MERCURY_TESTER_REQUEST_DEADLINE_MS,
1356
+ budget,
1357
+ personWords: args.personWords,
1358
+ askRooms: args.rooms === true,
1359
+ words: args.words,
1360
+ wordBag: args.wordBag ?? slugWordBag,
1361
+ onCounts: (sends, retries) => {
1362
+ stage.sends += sends;
1363
+ stage.retries += retries;
1364
+ },
1365
+ ...(args.hedgeAfterMs === undefined ? {} : { hedgeAfterMs: args.hedgeAfterMs }),
1366
+ format,
1367
+ planner,
1368
+ });
1369
+ if (rCards.isErr()) {
1370
+ const e = rCards.Err();
3260
1371
  stage.sends += e.detail.sends;
3261
1372
  stage.retries += e.detail.retries;
3262
- const rateLimitCapMs = budget.rateLimitCapMs();
3263
1373
  return Result.Err(new MercuryStageError(e.message, e.fallbackCause, {
3264
1374
  ...(e.detail.call === undefined ? {} : { call: e.detail.call }),
3265
1375
  sends: stage.sends,
3266
1376
  retries: stage.retries,
3267
1377
  elapsedMs: Date.now() - startedAt,
3268
- ...(rateLimitCapMs === undefined ? {} : { rateLimitCapMs }),
3269
- }));
3270
- }
3271
- const counted = (sends, retries) => {
3272
- stage.sends += sends;
3273
- stage.retries += retries;
3274
- };
3275
- if (args.format === "prose") {
3276
- const rProse = tally(await mercuryProseBriefCall(vctx, {
3277
- row,
3278
- deadlineMs,
3279
- budget,
3280
- personWords: args.personWords,
3281
- askRooms,
3282
- words: args.words,
3283
- wordBag,
3284
- onCounts: counted,
3285
1378
  }));
3286
- if (rProse.isErr())
3287
- return Result.Err(rProse.Err());
3288
- const { draft } = rProse.Ok();
3289
- return Result.Ok({
3290
- input: assembleMercuryProseCreateVibe({ personWords: args.personWords, draft }),
3291
- preamble: mercuryProsePreamble(draft),
3292
- preambleFrom: "classification",
3293
- format: "prose",
3294
- shape: draft.shape,
3295
- elapsedMs: Date.now() - startedAt,
3296
- sends: stage.sends,
3297
- retries: stage.retries,
3298
- ...named(),
3299
- });
3300
1379
  }
3301
- if (thinkAloud) {
3302
- const rThink = tally(await mercuryThinkAloudBriefCall(vctx, {
3303
- row,
3304
- deadlineMs,
3305
- budget,
1380
+ const { brief } = rCards.Ok();
1381
+ let reply = brief.reply;
1382
+ let fim;
1383
+ if (format === "cards-fim") {
1384
+ const filled = await mercuryCardsFim(vctx, {
1385
+ brief,
3306
1386
  personWords: args.personWords,
3307
- askRooms,
3308
- words: args.words,
3309
- wordBag,
3310
- onCounts: counted,
3311
- }));
3312
- if (rThink.isErr())
3313
- return Result.Err(rThink.Err());
3314
- const { draft } = rThink.Ok();
3315
- return Result.Ok({
3316
- input: assembleMercuryThinkAloudCreateVibe({ personWords: args.personWords, draft }),
3317
- preamble: mercuryThinkAloudPreamble(draft),
3318
- preambleFrom: "classification",
3319
- format: "thinkaloud",
3320
- shape: draft.shape,
3321
- elapsedMs: Date.now() - startedAt,
3322
- sends: stage.sends,
3323
- retries: stage.retries,
3324
- ...named(),
1387
+ ...(args.fimApiKey !== undefined
1388
+ ? { apiKey: args.fimApiKey }
1389
+ : vctx.inceptionApiKey === undefined
1390
+ ? {}
1391
+ : { apiKey: vctx.inceptionApiKey }),
1392
+ ...(args.fimUrl === undefined ? {} : { url: args.fimUrl }),
1393
+ ...(args.signal === undefined ? {} : { cancel: args.signal }),
3325
1394
  });
3326
- }
3327
- if (cards && (args.format === "cards" || args.format === "cards-fim")) {
3328
- const format = args.format;
3329
- const rCards = tally(await mercuryCardsBriefCall(vctx, {
3330
- row,
3331
- deadlineMs,
3332
- budget,
3333
- personWords: args.personWords,
3334
- askRooms,
3335
- words: args.words,
3336
- wordBag,
3337
- onCounts: counted,
3338
- format,
3339
- planner: args.planner ?? "mercury",
3340
- }));
3341
- if (rCards.isErr())
3342
- return Result.Err(rCards.Err());
3343
- const { draft } = rCards.Ok();
3344
- let reply = draft.reply;
3345
- let fim;
3346
- if (format === "cards-fim") {
3347
- const filled = await mercuryCardsFim(vctx, {
3348
- draft,
3349
- personWords: args.personWords,
3350
- ...(args.fimApiKey !== undefined
3351
- ? { apiKey: args.fimApiKey }
3352
- : vctx.inceptionApiKey === undefined
3353
- ? {}
3354
- : { apiKey: vctx.inceptionApiKey }),
3355
- ...(args.fimUrl === undefined ? {} : { url: args.fimUrl }),
3356
- ...(args.signal === undefined ? {} : { cancel: args.signal }),
1395
+ reply = filled.reply;
1396
+ fim = filled.receipt;
1397
+ const fimRound = mercuryFimUsageRound(fim);
1398
+ if (fimRound !== undefined)
1399
+ args.onUsage?.([fimRound]);
1400
+ if (lastRow !== undefined)
1401
+ await recordMercuryFim(vctx, {
1402
+ chatId: args.chatId,
1403
+ promptId: lastRow,
1404
+ fim,
1405
+ ...(keepText && fim.filled > 0 ? { filledText: reply } : {}),
3357
1406
  });
3358
- reply = filled.reply;
3359
- fim = filled.receipt;
3360
- const fimRound = mercuryFimUsageRound(fim);
3361
- if (fimRound !== undefined)
3362
- args.onUsage?.([fimRound]);
3363
- if (lastRow !== undefined)
3364
- await recordMercuryFim(vctx, {
3365
- chatId: args.chatId,
3366
- promptId: lastRow,
3367
- fim,
3368
- ...(keepText && fim.filled > 0 ? { filledText: reply } : {}),
3369
- });
3370
- }
3371
- return Result.Ok({
3372
- input: assembleMercuryCardsCreateVibe({ personWords: args.personWords, draft, reply }),
3373
- preamble: mercuryCardsPreamble(draft, args.personWords),
3374
- preambleFrom: draft.parsed.preamble.length > 0 ? "brief" : "classification",
3375
- format,
3376
- ...(args.planner === undefined || args.planner === "mercury" ? {} : { planner: args.planner }),
3377
- ...(fim === undefined ? {} : { fim }),
3378
- shape: draft.shape,
3379
- elapsedMs: Date.now() - startedAt,
3380
- sends: stage.sends,
3381
- retries: stage.retries,
3382
- ...named(),
3383
- });
3384
- }
3385
- const oneCall = args.oneCall === true;
3386
- const callShape = async () => tally(await mercuryCall(vctx, {
3387
- call: "shape",
3388
- row,
3389
- deadlineMs,
3390
- budget,
3391
- maxTokens: MERCURY_SHAPE_MAX_TOKENS,
3392
- onCounts: counted,
3393
- request: () => ({
3394
- schema: mercuryShapeSchema(),
3395
- messages: mercuryShapeMessages({ personWords: args.personWords }),
3396
- }),
3397
- accept: (json) => {
3398
- const parsed = parseMercuryShape(json);
3399
- return parsed.isErr() ? parsed : Result.Ok(fixMercuryShape(parsed.Ok(), args.personWords));
3400
- },
3401
- }));
3402
- let called;
3403
- if (!oneCall) {
3404
- const rShape = await callShape();
3405
- if (rShape.isErr())
3406
- return Result.Err(rShape.Err());
3407
- called = rShape.Ok();
3408
- }
3409
- const rBrief = tally(await mercuryBriefCall(vctx, {
3410
- row,
3411
- deadlineMs,
3412
- budget,
3413
- personWords: args.personWords,
3414
- ...(called === undefined ? {} : { shape: called }),
3415
- style,
3416
- juice: args.juice ?? "examples",
3417
- depth: args.depth ?? "none",
3418
- reply,
3419
- duo: reply === "whole" && args.duo === true,
3420
- duoPool: args.duoPool ?? MERCURY_DUO_PICK_COUNT,
3421
- recordsRules,
3422
- askRooms,
3423
- words: args.words,
3424
- wordBag,
3425
- onCounts: counted,
3426
- fanout: opus ? MERCURY_OPUS_PLANNER_FANOUT : MERCURY_BRIEF_FANOUT,
3427
- planner: opus ? "opus" : "mercury",
3428
- }));
3429
- if (rBrief.isErr())
3430
- return Result.Err(rBrief.Err());
3431
- const { gated, partner, duo, oneCall: oneCallReceipt } = rBrief.Ok();
3432
- const shape = called ?? gated.shape;
3433
- if (shape === undefined)
3434
- return stageErr("mercury: no classification parsed", "gate", stage, startedAt, "brief");
3435
- const union = oneCallReceipt === undefined
3436
- ? undefined
3437
- : Object.fromEntries(MERCURY_FLAGS.map((f) => [f, oneCallReceipt.flagsUnion.includes(f)]));
3438
- const assembly = {
3439
- personWords: args.personWords,
3440
- shape,
3441
- ...(union === undefined ? {} : { capabilityFlags: union }),
3442
- fields: {
3443
- opening: gated.texts.get("opening") ?? "",
3444
- access: gated.texts.get("access") ?? "",
3445
- seed: gated.texts.get("seed") ?? "",
3446
- look: gated.texts.get("look") ?? "",
3447
- icon: gated.texts.get("icon") ?? "",
3448
- pairs: gated.pairs ?? [],
3449
- },
3450
- ...(gated.rooms?.rooms === undefined ? {} : { rooms: gated.rooms.rooms }),
3451
- ...(gated.font?.font === undefined ? {} : { font: gated.font.font }),
3452
- ...(gated.screens?.screens === undefined ? {} : { screens: gated.screens.screens }),
3453
- ...(reply === "split"
3454
- ? {}
3455
- : {
3456
- reply,
3457
- ...(gated.story?.story === undefined ? {} : { story: gated.story.story }),
3458
- ...(gated.preamble === undefined ? {} : { preamble: gated.preamble }),
3459
- ...(partner === undefined ? {} : { partner: takeOf(partner) }),
3460
- ...(recordsRules
3461
- ? {
3462
- recordsRules: true,
3463
- ...(gated.records?.screens === undefined ? {} : { records: gated.records.screens }),
3464
- ...(gated.rules?.screens === undefined ? {} : { rules: gated.rules.screens }),
3465
- }
3466
- : {}),
3467
- }),
3468
- };
3469
- const input = assembleMercuryCreateVibe(assembly);
3470
- if (reply === "whole") {
3471
- return Result.Ok({
3472
- input,
3473
- preamble: gated.preamble ?? cleanMercuryText(gated.texts.get("opening") ?? ""),
3474
- preambleFrom: "brief",
3475
- storyChars: gated.story?.story === undefined ? 0 : cleanMercuryText(gated.story.story).length,
3476
- ...(duo === undefined ? {} : { duo }),
3477
- ...(oneCallReceipt === undefined ? {} : { oneCall: oneCallReceipt }),
3478
- ...(opus ? { planner: "opus" } : {}),
3479
- shape,
3480
- elapsedMs: Date.now() - startedAt,
3481
- sends: stage.sends,
3482
- retries: stage.retries,
3483
- ...named(),
3484
- });
3485
1407
  }
3486
- const enrichedPrompt = mercuryPreambleBrief(assembly);
3487
- const rPreamble = tally(await mercuryCall(vctx, {
3488
- call: "preamble",
3489
- row,
3490
- deadlineMs,
3491
- budget,
3492
- maxTokens: 300,
3493
- onCounts: counted,
3494
- request: () => ({
3495
- schema: mercuryPreambleSchema(),
3496
- messages: mercuryPreambleMessages({ personWords: args.personWords, shape, enrichedPrompt, style }),
3497
- }),
3498
- accept: (json) => {
3499
- const p = preambleParsed(json);
3500
- if (p instanceof type.errors)
3501
- return Result.Err(p.summary);
3502
- const fails = gateMercuryPreamble({ text: p.preamble, enrichedPrompt });
3503
- return fails.length === 0 ? Result.Ok(cleanMercuryText(p.preamble)) : Result.Err(`gate ${fails.join("+")}`);
3504
- },
3505
- }));
3506
- if (rPreamble.isErr())
3507
- return Result.Err(rPreamble.Err());
3508
1408
  return Result.Ok({
3509
- input,
3510
- preamble: rPreamble.Ok(),
3511
- ...(oneCallReceipt === undefined ? {} : { oneCall: oneCallReceipt }),
3512
- shape,
1409
+ input: assembleMercuryCardsCreateVibe({ personWords: args.personWords, brief, reply }),
1410
+ preamble: mercuryCardsPreamble(brief, args.personWords),
1411
+ preambleFrom: brief.parsed.preamble.length > 0 ? "brief" : "classification",
1412
+ format,
1413
+ ...(planner === "mercury" ? {} : { planner }),
1414
+ ...(fim === undefined ? {} : { fim }),
1415
+ shape: brief.shape,
3513
1416
  elapsedMs: Date.now() - startedAt,
3514
1417
  sends: stage.sends,
3515
1418
  retries: stage.retries,