@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.
- package/evento-handler-manifest.js +0 -4
- package/evento-handler-manifest.js.map +1 -1
- package/intern/agent-assembly.js +1 -4
- package/intern/agent-assembly.js.map +1 -1
- package/intern/agent-belt.js +10 -10
- package/intern/agent-belt.js.map +1 -1
- package/intern/agent-dispatcher.js +16 -41
- package/intern/agent-dispatcher.js.map +1 -1
- package/intern/agent-router.js.map +1 -1
- package/intern/ensure-chat-id.d.ts +0 -2
- package/intern/ensure-chat-id.js +25 -57
- package/intern/ensure-chat-id.js.map +1 -1
- package/intern/ensure-slug-binding.js.map +1 -1
- package/intern/first-brief-pairs.d.ts +13 -0
- package/intern/first-brief-pairs.js +138 -0
- package/intern/first-brief-pairs.js.map +1 -0
- package/intern/first-brief.d.ts +4 -0
- package/intern/first-brief.js +1 -0
- package/intern/first-brief.js.map +1 -1
- package/intern/first-build-race.d.ts +2 -0
- package/intern/first-build-race.js +6 -0
- package/intern/first-build-race.js.map +1 -1
- package/intern/first-build-request.js +1 -3
- package/intern/first-build-request.js.map +1 -1
- package/intern/get-model-defaults.d.ts +1 -0
- package/intern/get-model-defaults.js +2 -1
- package/intern/get-model-defaults.js.map +1 -1
- package/intern/guest-principal-store.js +2 -31
- package/intern/guest-principal-store.js.map +1 -1
- package/intern/instant-build.d.ts +154 -0
- package/intern/instant-build.js +443 -0
- package/intern/instant-build.js.map +1 -0
- package/intern/instruction-variant.d.ts +4 -13
- package/intern/instruction-variant.js +13 -153
- package/intern/instruction-variant.js.map +1 -1
- package/intern/mercury-first-build-seam.d.ts +2 -41
- package/intern/mercury-first-build-seam.js +4 -98
- package/intern/mercury-first-build-seam.js.map +1 -1
- package/intern/mercury-first-build.d.ts +34 -407
- package/intern/mercury-first-build.js +243 -2340
- package/intern/mercury-first-build.js.map +1 -1
- package/intern/mercury-resilience.d.ts +4 -11
- package/intern/mercury-resilience.js +4 -22
- package/intern/mercury-resilience.js.map +1 -1
- package/intern/mint-decision.d.ts +4 -0
- package/intern/mint-decision.js +11 -7
- package/intern/mint-decision.js.map +1 -1
- package/intern/mint-job-store.js +2 -2
- package/intern/mint-job-store.js.map +1 -1
- package/intern/onboarding-handle.d.ts +1 -0
- package/intern/onboarding-handle.js +1 -1
- package/intern/onboarding-handle.js.map +1 -1
- package/intern/prompt-assembly.d.ts +20 -0
- package/intern/prompt-assembly.js +12 -19
- package/intern/prompt-assembly.js.map +1 -1
- package/intern/provisional-handle.d.ts +8 -0
- package/intern/provisional-handle.js +56 -0
- package/intern/provisional-handle.js.map +1 -0
- package/intern/subagent-model.d.ts +0 -7
- package/intern/subagent-model.js +0 -6
- package/intern/subagent-model.js.map +1 -1
- package/package.json +13 -13
- package/public/agent-turn-action.js +18 -22
- package/public/agent-turn-action.js.map +1 -1
- package/public/app-setting-items.d.ts +2 -0
- package/public/app-setting-items.js +32 -2
- package/public/app-setting-items.js.map +1 -1
- package/public/codegen-turn-route.d.ts +9 -2
- package/public/codegen-turn-route.js +46 -20
- package/public/codegen-turn-route.js.map +1 -1
- package/public/ensure-app-settings.js +7 -7
- package/public/ensure-app-settings.js.map +1 -1
- package/public/get-app-by-fsid.js +2 -2
- package/public/get-app-by-fsid.js.map +1 -1
- package/public/get-chat-details.js +3 -4
- package/public/get-chat-details.js.map +1 -1
- package/public/get-chat-response.js +3 -4
- package/public/get-chat-response.js.map +1 -1
- package/public/handle-prompt-context.d.ts +3 -2
- package/public/handle-prompt-context.js +28 -76
- package/public/handle-prompt-context.js.map +1 -1
- package/public/handler-llm-request.d.ts +6 -1
- package/public/handler-llm-request.js +11 -9
- package/public/handler-llm-request.js.map +1 -1
- package/public/list-recent-vibes.js +1 -57
- package/public/list-recent-vibes.js.map +1 -1
- package/public/open-chat.js +1 -2
- package/public/open-chat.js.map +1 -1
- package/public/prompt-chat-section.js +2 -13
- package/public/prompt-chat-section.js.map +1 -1
- package/public/prompt-section-actions.d.ts +0 -2
- package/public/prompt-section-actions.js +24 -1
- package/public/prompt-section-actions.js.map +1 -1
- package/public/prompt-turn-terminal.js +1 -5
- package/public/prompt-turn-terminal.js.map +1 -1
- package/public/prompt-turn-types.d.ts +2 -0
- package/public/publish-app.js +2 -2
- package/public/publish-app.js.map +1 -1
- package/public/select-app.d.ts +1 -1
- package/public/select-app.js +1 -1
- package/public/select-app.js.map +1 -1
- package/public/shared-mint.js +1 -2
- package/public/shared-mint.js.map +1 -1
- package/public/stream-assembly.d.ts +5 -2
- package/public/stream-assembly.js +5 -2
- package/public/stream-assembly.js.map +1 -1
- package/usage-report/codegen-delivery-alert-util.d.ts +4 -4
- package/usage-report/codegen-delivery-alert-util.js +17 -17
- package/usage-report/codegen-delivery-alert-util.js.map +1 -1
- package/usage-report/first-build-no-app-alarm.js.map +1 -1
- package/usage-report/{draft-output-alert.d.ts → new-user-alert-post.d.ts} +0 -6
- package/usage-report/new-user-alert-post.js +14 -0
- package/usage-report/new-user-alert-post.js.map +1 -0
- package/usage-report/new-user-alert-util.d.ts +0 -1
- package/usage-report/new-user-alert-util.js +3 -16
- package/usage-report/new-user-alert-util.js.map +1 -1
- package/usage-report/new-user-alert.js +1 -6
- package/usage-report/new-user-alert.js.map +1 -1
- package/intern/attach-draft-handle.d.ts +0 -18
- package/intern/attach-draft-handle.js +0 -342
- package/intern/attach-draft-handle.js.map +0 -1
- package/intern/automatic-draft-attachment.d.ts +0 -18
- package/intern/automatic-draft-attachment.js +0 -31
- package/intern/automatic-draft-attachment.js.map +0 -1
- package/intern/draft-chat-store.d.ts +0 -99
- package/intern/draft-chat-store.js +0 -86
- package/intern/draft-chat-store.js.map +0 -1
- package/intern/draft-runtime-store.d.ts +0 -8
- package/intern/draft-runtime-store.js +0 -31
- package/intern/draft-runtime-store.js.map +0 -1
- package/public/attach-draft-handle.d.ts +0 -3
- package/public/attach-draft-handle.js +0 -53
- package/public/attach-draft-handle.js.map +0 -1
- package/public/draft-runtime.d.ts +0 -5
- package/public/draft-runtime.js +0 -302
- package/public/draft-runtime.js.map +0 -1
- package/usage-report/draft-output-alert.js +0 -24
- 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,
|
|
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|
|
|
156
|
-
export function fixMercuryShape(shape, personWords
|
|
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,
|
|
70
|
+
flags: { ...shape.flags, composer, threeD: shape.flags.threeD || mercuryBlockWorldNamed(personWords) },
|
|
176
71
|
...(keepsToThemselves ? { keepsToThemselves: true } : {}),
|
|
177
72
|
};
|
|
178
73
|
}
|
|
179
|
-
const
|
|
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
|
|
186
|
-
|
|
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(),
|
|
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
|
-
|
|
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)\
|
|
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
|
|
515
|
-
export
|
|
516
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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 =
|
|
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 ?
|
|
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
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
-
|
|
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
|
|
1860
|
-
const t =
|
|
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
|
|
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
|
|
1960
|
-
|
|
1961
|
-
|
|
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
|
-
${
|
|
709
|
+
${MERCURY_CARDS_CAPABILITIES}
|
|
2020
710
|
|
|
2021
|
-
${
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
2127
|
-
archetype: known ??
|
|
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:
|
|
2136
|
-
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(
|
|
2159
|
-
...(o.askRooms && p.keys.font !== undefined ? { font: gateMercuryFont(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1060
|
+
const s = o.brief.shape;
|
|
2371
1061
|
const nature = mercuryNature(s, o.personWords);
|
|
2372
|
-
const rooms = sanitizeRoomShortlist("pouf", o.
|
|
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.
|
|
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.
|
|
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
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
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
|
-
|
|
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
|
|
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) => () =>
|
|
3043
|
-
|
|
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:
|
|
3049
|
-
...(o.planner ===
|
|
3050
|
-
|
|
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
|
-
|
|
3059
|
-
const label = `
|
|
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.
|
|
1150
|
+
const g = gate(a.reply.text);
|
|
3076
1151
|
if (g.isOk()) {
|
|
3077
1152
|
valid += 1;
|
|
3078
|
-
winner = { from: d,
|
|
3079
|
-
for (const s of
|
|
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
|
-
|
|
3091
|
-
const
|
|
3092
|
-
|
|
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 =
|
|
1205
|
+
const last = briefs.find((d) => d.ended === "reply") ?? briefs[0];
|
|
3099
1206
|
const log = {
|
|
3100
1207
|
...mercurySendLog(),
|
|
3101
|
-
sends:
|
|
3102
|
-
retries:
|
|
3103
|
-
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
|
|
3108
|
-
let attempts =
|
|
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(
|
|
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.
|
|
1232
|
+
const g = gate(a.reply.text);
|
|
3126
1233
|
if (g.isOk()) {
|
|
3127
1234
|
valid += 1;
|
|
3128
|
-
chosen = { from:
|
|
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 =
|
|
3138
|
-
const
|
|
3139
|
-
const reaskUsage = totals.usage.slice(
|
|
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
|
-
|
|
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
|
-
?
|
|
1254
|
+
? billedBriefs
|
|
3147
1255
|
: {
|
|
3148
|
-
...
|
|
3149
|
-
prompt_tokens:
|
|
3150
|
-
completion_tokens:
|
|
3151
|
-
total_tokens:
|
|
3152
|
-
...(
|
|
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: (
|
|
1262
|
+
: { cost: (billedBriefs.cost ?? 0) + reaskUsage.reduce((n, u) => n + (u.cost ?? 0), 0) }),
|
|
3155
1263
|
},
|
|
3156
1264
|
};
|
|
3157
|
-
const c = chosen?.
|
|
1265
|
+
const c = chosen?.brief;
|
|
3158
1266
|
const extra = {
|
|
3159
|
-
|
|
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({
|
|
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
|
|
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
|
|
3243
|
-
const
|
|
3244
|
-
const
|
|
3245
|
-
const
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
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
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
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
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
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
|
-
|
|
3328
|
-
const
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
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:
|
|
3511
|
-
|
|
3512
|
-
|
|
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,
|