shotops-mcp 0.9.5 → 0.9.7
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/README.md +27 -10
- package/dist/local.js +1363 -467
- package/harness-dist/assets/index--FFK3psO.js +1 -0
- package/harness-dist/index.html +1 -1
- package/package.json +5 -2
- package/widget-dist/bundle.js +4 -4
- package/widget-dist/panels.js +4 -4
- package/harness-dist/assets/index-gGXe4d5n.js +0 -1
package/dist/local.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
await visit('')
|
|
26
26
|
return hash.digest('hex')
|
|
27
|
-
})(sourceDirectory) !== "
|
|
27
|
+
})(sourceDirectory) !== "1b56a11440edcc23083bfa4a283fe2d972acfb3a17820247bc951bcd5cc3810d") {
|
|
28
28
|
console.error('shotops-mcp: dist/local.js is stale; run npm run build in shotops-mcp.');
|
|
29
29
|
}
|
|
30
30
|
} catch { /* An unreadable checkout must not break an otherwise working CLI. */ }
|
|
@@ -1892,6 +1892,12 @@ var init_fonts = __esm({
|
|
|
1892
1892
|
});
|
|
1893
1893
|
|
|
1894
1894
|
// ../mockup-engine/appstore/refineIntent.ts
|
|
1895
|
+
function requestsBroaderDeviceScope(instruction) {
|
|
1896
|
+
return BROADER_DEVICE_SCOPE.test(instruction) || NUMBERED_SIBLING_SCOPE.test(instruction);
|
|
1897
|
+
}
|
|
1898
|
+
function canRepairLayoutForFocus(focus, instruction) {
|
|
1899
|
+
return focus?.kind !== "device" || requestsBroaderDeviceScope(instruction);
|
|
1900
|
+
}
|
|
1895
1901
|
function structuralOperationTargets(operation) {
|
|
1896
1902
|
switch (operation.op) {
|
|
1897
1903
|
case "frame.add":
|
|
@@ -1931,7 +1937,7 @@ function resolveAiNumber(input, current, range) {
|
|
|
1931
1937
|
if (raw === void 0 || !Number.isFinite(raw)) return void 0;
|
|
1932
1938
|
return Math.min(Math.max(raw, range.min), range.max);
|
|
1933
1939
|
}
|
|
1934
|
-
var MAX_REFINE_PROMPT_LENGTH, AI_COLORWAY_IDS, AI_CLAY_TONES, AI_SHOT_RANGES, AI_SCALE_RANGE, AI_CAPTION_FONT_IDS, AI_CAPTION_RANGES, AI_GRADIENT_STOPS;
|
|
1940
|
+
var MAX_REFINE_PROMPT_LENGTH, AI_COLORWAY_IDS, AI_CLAY_TONES, AI_SHOT_RANGES, AI_SCALE_RANGE, AI_CAPTION_FONT_IDS, AI_CAPTION_RANGES, AI_GRADIENT_STOPS, BROADER_DEVICE_SCOPE, NUMBERED_SIBLING_SCOPE;
|
|
1935
1941
|
var init_refineIntent = __esm({
|
|
1936
1942
|
"../mockup-engine/appstore/refineIntent.ts"() {
|
|
1937
1943
|
"use strict";
|
|
@@ -1949,6 +1955,8 @@ var init_refineIntent = __esm({
|
|
|
1949
1955
|
AI_CAPTION_FONT_IDS = CAPTION_FONT_IDS;
|
|
1950
1956
|
AI_CAPTION_RANGES = CAPTION_STYLE_BOUNDS;
|
|
1951
1957
|
AI_GRADIENT_STOPS = { min: 1, max: 6 };
|
|
1958
|
+
BROADER_DEVICE_SCOPE = /\b(?:all|every|each|both|entire|whole|across|throughout|other|others|rest|remaining|project|strip|frames|screenshots|screens|devices|phones|captions)\b/i;
|
|
1959
|
+
NUMBERED_SIBLING_SCOPE = /\b(?:frame|device|phone)\s+\d+\b/i;
|
|
1952
1960
|
}
|
|
1953
1961
|
});
|
|
1954
1962
|
|
|
@@ -2200,7 +2208,7 @@ var init_screenVariants = __esm({
|
|
|
2200
2208
|
});
|
|
2201
2209
|
|
|
2202
2210
|
// ../mockup-engine/appstore/catalog/layouts.ts
|
|
2203
|
-
var LAYOUT_TEMPLATE_CATALOG, LAYOUT_TEMPLATES, TEMPLATE_IDS, LAYOUT_TEMPLATE_IDS, LAYOUT_POSE_FIELDS, LAYOUT_CAMERA_FIELDS, LAYOUT_CAPTION_FIELDS;
|
|
2211
|
+
var LAYOUT_TEMPLATE_CATALOG, LAYOUT_TEMPLATES, TEMPLATE_IDS, LAYOUT_TEMPLATE_IDS, LAYOUT_CAPTION_ABSENT_DEFAULTS, LAYOUT_POSE_FIELDS, LAYOUT_CAMERA_FIELDS, LAYOUT_CAPTION_FIELDS;
|
|
2204
2212
|
var init_layouts = __esm({
|
|
2205
2213
|
"../mockup-engine/appstore/catalog/layouts.ts"() {
|
|
2206
2214
|
"use strict";
|
|
@@ -2210,28 +2218,78 @@ var init_layouts = __esm({
|
|
|
2210
2218
|
label: "Standard",
|
|
2211
2219
|
blurb: "Headline on top, whole device below it, nothing cropped. The safe default.",
|
|
2212
2220
|
character: "safe",
|
|
2221
|
+
bleedEdge: "none",
|
|
2213
2222
|
shotLook: { phoneHeight: "82", hOffset: "0", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2214
|
-
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2 }
|
|
2223
|
+
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2, align: "center", bandCenter: 0.5 }
|
|
2215
2224
|
},
|
|
2216
2225
|
{
|
|
2217
2226
|
id: "bleed",
|
|
2218
2227
|
label: "Bleed",
|
|
2219
2228
|
blurb: "Room for a longer headline, device running off the bottom edge. What the market looks like.",
|
|
2220
2229
|
character: "conventional",
|
|
2230
|
+
bleedEdge: "bottom",
|
|
2221
2231
|
shotLook: { phoneHeight: "82", hOffset: "0", vOffset: "38", angle: "front", cameraPos: null, roll: "0" },
|
|
2222
|
-
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4 }
|
|
2232
|
+
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4, align: "center", bandCenter: 0.5 }
|
|
2223
2233
|
},
|
|
2224
2234
|
{
|
|
2225
2235
|
id: "top-bleed",
|
|
2226
2236
|
label: "Top bleed",
|
|
2227
2237
|
blurb: "Device running off the TOP edge, headline underneath it. Inverts the usual reading order.",
|
|
2228
2238
|
character: "expressive",
|
|
2239
|
+
bleedEdge: "top",
|
|
2229
2240
|
shotLook: { phoneHeight: "82", hOffset: "0", vOffset: "-38", angle: "front", cameraPos: null, roll: "0" },
|
|
2230
2241
|
// The only entry that moves the band. Its caption sits BELOW the device, which the engine
|
|
2231
2242
|
// could not express at all until #218 gave placeCaptions a bottom-anchored band — before that
|
|
2232
2243
|
// the only way down was pinning an anchor, which silently costs reflow, stacking and the
|
|
2233
2244
|
// legibility advisory. `band: 'bottom'` keeps the anchor ABSENT, so it stays genuinely auto.
|
|
2234
|
-
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4, band: "bottom" }
|
|
2245
|
+
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4, band: "bottom", align: "center", bandCenter: 0.5 }
|
|
2246
|
+
},
|
|
2247
|
+
// ── The side bleeds. Facts 5–7 in the essay above own every number here. ────────────────────
|
|
2248
|
+
//
|
|
2249
|
+
// What they buy that the vertical three cannot: the headline gets a COLUMN instead of a band the
|
|
2250
|
+
// device sits under. Narrowing `maxWidth` to 0.61 and moving `bandCenter` off centre is what
|
|
2251
|
+
// makes that column, and it is why the catalog owns `align`/`bandCenter` at all — see the note
|
|
2252
|
+
// on LayoutCaptionField below for what that costs every other template.
|
|
2253
|
+
{
|
|
2254
|
+
id: "lean-right",
|
|
2255
|
+
label: "Lean right",
|
|
2256
|
+
blurb: "Device pushed off the RIGHT edge, headline in a column down the left. Reads as one continuous swipe.",
|
|
2257
|
+
character: "expressive",
|
|
2258
|
+
bleedEdge: "right",
|
|
2259
|
+
shotLook: { phoneHeight: "82", hOffset: "46", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2260
|
+
caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 2, align: "left", bandCenter: 0.375 }
|
|
2261
|
+
},
|
|
2262
|
+
{
|
|
2263
|
+
id: "lean-left",
|
|
2264
|
+
label: "Lean left",
|
|
2265
|
+
blurb: "Device pushed off the LEFT edge, headline in a column down the right. The mirror of Lean right.",
|
|
2266
|
+
character: "expressive",
|
|
2267
|
+
bleedEdge: "left",
|
|
2268
|
+
shotLook: { phoneHeight: "82", hOffset: "-46", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2269
|
+
caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 2, align: "right", bandCenter: 0.625 }
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
id: "swipe",
|
|
2273
|
+
label: "Swipe",
|
|
2274
|
+
blurb: "A third of the device carried into the next panel, headline full width above it. Only honest applied to every frame.",
|
|
2275
|
+
character: "expressive",
|
|
2276
|
+
bleedEdge: "right",
|
|
2277
|
+
// Standard's vertical, and that is not a free choice: a 4-line reservation over vOffset 18 puts
|
|
2278
|
+
// the device's top edge through the fourth line, and Bleed's vOffset 38 would crop the bottom
|
|
2279
|
+
// too — which makes this template claim `bottom-right` and collide with Corner bleed, the only
|
|
2280
|
+
// other entry on that edge. The horizontal push is the whole idea here; the vertical stays out
|
|
2281
|
+
// of its way. Both halves are guarded (clearance, and the declared-edge check).
|
|
2282
|
+
shotLook: { phoneHeight: "82", hOffset: "78", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2283
|
+
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2, align: "center", bandCenter: 0.5 }
|
|
2284
|
+
},
|
|
2285
|
+
{
|
|
2286
|
+
id: "corner-bleed",
|
|
2287
|
+
label: "Corner bleed",
|
|
2288
|
+
blurb: "Off the bottom AND the right at once, headline in a column down the left. The most opinionated panel here.",
|
|
2289
|
+
character: "expressive",
|
|
2290
|
+
bleedEdge: "bottom-right",
|
|
2291
|
+
shotLook: { phoneHeight: "82", hOffset: "46", vOffset: "38", angle: "front", cameraPos: null, roll: "0" },
|
|
2292
|
+
caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 4, align: "left", bandCenter: 0.375 }
|
|
2235
2293
|
}
|
|
2236
2294
|
];
|
|
2237
2295
|
LAYOUT_TEMPLATES = LAYOUT_TEMPLATE_CATALOG;
|
|
@@ -2240,6 +2298,10 @@ var init_layouts = __esm({
|
|
|
2240
2298
|
TEMPLATE_IDS[0],
|
|
2241
2299
|
...TEMPLATE_IDS.slice(1)
|
|
2242
2300
|
];
|
|
2301
|
+
LAYOUT_CAPTION_ABSENT_DEFAULTS = {
|
|
2302
|
+
align: "center",
|
|
2303
|
+
bandCenter: 0.5
|
|
2304
|
+
};
|
|
2243
2305
|
LAYOUT_POSE_FIELDS = ["phoneHeight", "hOffset", "vOffset"];
|
|
2244
2306
|
LAYOUT_CAMERA_FIELDS = ["angle", "cameraPos", "roll"];
|
|
2245
2307
|
LAYOUT_CAPTION_FIELDS = Array.from(
|
|
@@ -2251,8 +2313,8 @@ var init_layouts = __esm({
|
|
|
2251
2313
|
// ../mockup-engine/appstore/layoutTemplates.ts
|
|
2252
2314
|
function layoutTemplateAgentSummary(template) {
|
|
2253
2315
|
const reserveLines = template.caption.reserveLines;
|
|
2254
|
-
const capacity = typeof reserveLines === "number" ? ` Reserves ${reserveLines} headline lines (~${reserveLines * 18} characters).` : "";
|
|
2255
|
-
return `${template.blurb}${capacity} Character: [${template.character}].`;
|
|
2316
|
+
const capacity = typeof reserveLines === "number" ? ` Reserves ${reserveLines} headline lines (~${Math.round(reserveLines * 18 * (template.caption.maxWidth ?? 0.86) / 0.86)} characters).` : "";
|
|
2317
|
+
return `${template.blurb}${capacity} Character: [${template.character}]. Bleeds: [${template.bleedEdge}].`;
|
|
2256
2318
|
}
|
|
2257
2319
|
function layoutTemplateById(id) {
|
|
2258
2320
|
return LAYOUT_TEMPLATES.find((t) => t.id === id);
|
|
@@ -2272,8 +2334,9 @@ function differencesFrom(template, shotLook2, captionStyle) {
|
|
|
2272
2334
|
}
|
|
2273
2335
|
if (captionStyle) {
|
|
2274
2336
|
for (const field of LAYOUT_CAPTION_FIELDS) {
|
|
2275
|
-
const
|
|
2276
|
-
const
|
|
2337
|
+
const fallback = LAYOUT_CAPTION_ABSENT_DEFAULTS[field];
|
|
2338
|
+
const actual = captionStyle[field] ?? fallback;
|
|
2339
|
+
const wanted = template.caption[field] ?? fallback;
|
|
2277
2340
|
if (actual !== wanted) differs.push({ field, actual, template: wanted });
|
|
2278
2341
|
}
|
|
2279
2342
|
}
|
|
@@ -2318,7 +2381,7 @@ function layoutTemplateToLook(template) {
|
|
|
2318
2381
|
}
|
|
2319
2382
|
return { shotLook: { ...template.shotLook }, caption };
|
|
2320
2383
|
}
|
|
2321
|
-
var LAYOUT_CHARACTER_ANSWERS, LAYOUT_CHARACTER_IDS, LAYOUT_SELECTION_CAVEATS, valuesEqual;
|
|
2384
|
+
var LAYOUT_CHARACTER_ANSWERS, LAYOUT_CHARACTER_IDS, LAYOUT_BLEED_EDGE_WORDS, LAYOUT_BLEED_EDGE_IDS, LAYOUT_SELECTION_CAVEATS, valuesEqual;
|
|
2322
2385
|
var init_layoutTemplates = __esm({
|
|
2323
2386
|
"../mockup-engine/appstore/layoutTemplates.ts"() {
|
|
2324
2387
|
"use strict";
|
|
@@ -2330,9 +2393,20 @@ var init_layoutTemplates = __esm({
|
|
|
2330
2393
|
expressive: "the request explicitly asks to stand out \u2014 unique, creative, bold, striking, different, surprising"
|
|
2331
2394
|
};
|
|
2332
2395
|
LAYOUT_CHARACTER_IDS = Object.keys(LAYOUT_CHARACTER_ANSWERS);
|
|
2396
|
+
LAYOUT_BLEED_EDGE_WORDS = {
|
|
2397
|
+
none: "nothing about cropping or edges was asked for",
|
|
2398
|
+
top: "the phone should run off the TOP, or the headline should sit underneath it",
|
|
2399
|
+
bottom: "the phone should run off the BOTTOM \u2014 the ordinary store crop",
|
|
2400
|
+
left: "the phone should sit to the LEFT, or run off the left edge",
|
|
2401
|
+
right: "the phone should sit to the RIGHT, run off the right edge, or carry into the next screenshot",
|
|
2402
|
+
"bottom-right": "the phone should run off TWO edges at once, or the request asks for the boldest possible crop"
|
|
2403
|
+
};
|
|
2404
|
+
LAYOUT_BLEED_EDGE_IDS = Object.keys(LAYOUT_BLEED_EDGE_WORDS);
|
|
2333
2405
|
LAYOUT_SELECTION_CAVEATS = [
|
|
2334
2406
|
'A request that names a composition outright ("device running off the bottom edge", "headline under the phone") beats the character.',
|
|
2335
|
-
"
|
|
2407
|
+
"The EDGE beats the character too, and is checked first: a request that says which way the phone should go has already chosen, whatever quality words sit around it.",
|
|
2408
|
+
"When several templates share the right character AND the right edge, take the one whose reserved headline lines fit the LONGEST caption in the set. Under-reserving truncates; over-reserving leaves a visible empty band.",
|
|
2409
|
+
"A side bleed is chosen for the WHOLE set or not at all. Every panel leaning the same way reads as one swipe; mirrored neighbours (lean-right beside lean-left) overlap and read as a mistake."
|
|
2336
2410
|
];
|
|
2337
2411
|
valuesEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2338
2412
|
}
|
|
@@ -3259,7 +3333,7 @@ function presetCaptionStyle(preset) {
|
|
|
3259
3333
|
return { ...preset.caption };
|
|
3260
3334
|
}
|
|
3261
3335
|
function presetShotLook(preset) {
|
|
3262
|
-
return { ...preset.shotLook };
|
|
3336
|
+
return { ...preset.shotLook, ...preset.shotLook.angle !== void 0 ? { cameraPos: null } : {} };
|
|
3263
3337
|
}
|
|
3264
3338
|
function expandLookChoice(designPreset, layout) {
|
|
3265
3339
|
const preset = designPreset === void 0 ? void 0 : designPresetById(designPreset);
|
|
@@ -6154,6 +6228,17 @@ function matchAuthoredLocale(locales, requested) {
|
|
|
6154
6228
|
});
|
|
6155
6229
|
return matches.length === 1 ? matches[0] : null;
|
|
6156
6230
|
}
|
|
6231
|
+
function localesWithoutCaptions(captionText, locales, baseLocale = DEFAULT_LOCALE) {
|
|
6232
|
+
return locales.filter((locale2) => {
|
|
6233
|
+
if (locale2 === baseLocale) return false;
|
|
6234
|
+
const inner = captionText[locale2];
|
|
6235
|
+
if (!inner) return true;
|
|
6236
|
+
for (const layers of Object.values(inner)) {
|
|
6237
|
+
if (layers && layers.some((l) => l.text.trim().length > 0)) return false;
|
|
6238
|
+
}
|
|
6239
|
+
return true;
|
|
6240
|
+
});
|
|
6241
|
+
}
|
|
6157
6242
|
function normalizeLegacyLocales(captionText, locales) {
|
|
6158
6243
|
const hasLegacy = Object.keys(captionText).some((c) => c in LEGACY_LOCALE_ALIASES) || (locales?.some((c) => c in LEGACY_LOCALE_ALIASES) ?? false);
|
|
6159
6244
|
if (!hasLegacy) return { captionText, locales };
|
|
@@ -7117,8 +7202,8 @@ var init_agentCapabilities = __esm({
|
|
|
7117
7202
|
studioCommands: ["addPanel"],
|
|
7118
7203
|
wire: "structuralOperations: frame.add",
|
|
7119
7204
|
targets: ["project"],
|
|
7120
|
-
requiredInputs: ["afterFrameId (optional)"],
|
|
7121
|
-
limits: ["
|
|
7205
|
+
requiredInputs: ["afterFrameId (optional)", "frameRef (optional, patch-local alias)"],
|
|
7206
|
+
limits: ["Declare frameRef to address the new frame in later ordered operations of the same patch. Without frameRef, wait until the next cycle reports its id."],
|
|
7122
7207
|
evaluationCases: ["structure-add-closing-frame"]
|
|
7123
7208
|
},
|
|
7124
7209
|
{
|
|
@@ -7131,7 +7216,7 @@ var init_agentCapabilities = __esm({
|
|
|
7131
7216
|
targets: ["frame"],
|
|
7132
7217
|
requiredInputs: ["frameId"],
|
|
7133
7218
|
limits: ["The last remaining frame cannot be removed."],
|
|
7134
|
-
evaluationCases: ["structure-remove-empty-frame"]
|
|
7219
|
+
evaluationCases: ["structure-remove-empty-frame", "structure-remove-populated-frame"]
|
|
7135
7220
|
},
|
|
7136
7221
|
{
|
|
7137
7222
|
id: "frame.reorder",
|
|
@@ -7278,7 +7363,7 @@ var init_agentCapabilities = __esm({
|
|
|
7278
7363
|
domain: "caption",
|
|
7279
7364
|
status: "supported",
|
|
7280
7365
|
summary: "Restyle captions: font, size, colour, alignment, width and band placement.",
|
|
7281
|
-
studioCommands: [],
|
|
7366
|
+
studioCommands: ["patchAllCaptionStyles"],
|
|
7282
7367
|
wire: "patch.captionStyle (strip-wide) or patch.frames[i].captionStyle (one frame)",
|
|
7283
7368
|
targets: ["project", "frame"],
|
|
7284
7369
|
requiredInputs: ["the style fields to change"],
|
|
@@ -7322,7 +7407,7 @@ var init_agentCapabilities = __esm({
|
|
|
7322
7407
|
domain: "caption",
|
|
7323
7408
|
status: "supported",
|
|
7324
7409
|
summary: "Move a caption layer to another frame.",
|
|
7325
|
-
studioCommands: ["moveCaptionLayerToFrame"],
|
|
7410
|
+
studioCommands: ["moveCaptionLayer", "moveCaptionLayerToFrame"],
|
|
7326
7411
|
wire: "structuralOperations: caption.move across frames",
|
|
7327
7412
|
targets: ["caption", "frame"],
|
|
7328
7413
|
requiredInputs: ["frameId", "the caption position", "toFrameId"],
|
|
@@ -7337,19 +7422,19 @@ var init_agentCapabilities = __esm({
|
|
|
7337
7422
|
wire: null,
|
|
7338
7423
|
targets: ["project"],
|
|
7339
7424
|
requiredInputs: [],
|
|
7340
|
-
reason: "
|
|
7425
|
+
reason: "Agent refinement cannot translate captions. Open Translate in Studio to create German or other language versions while retaining the base words.",
|
|
7341
7426
|
evaluationCases: ["unsupported-translate-captions"]
|
|
7342
7427
|
},
|
|
7343
7428
|
{
|
|
7344
7429
|
id: "caption.output-override",
|
|
7345
7430
|
domain: "caption",
|
|
7346
7431
|
status: "intentionally_excluded",
|
|
7347
|
-
summary: "Give
|
|
7432
|
+
summary: "Give an output family its own caption wording.",
|
|
7348
7433
|
studioCommands: ["patchOutputCaption", "clearOutputCaption"],
|
|
7349
7434
|
wire: null,
|
|
7350
7435
|
targets: ["caption"],
|
|
7351
7436
|
requiredInputs: [],
|
|
7352
|
-
reason: "
|
|
7437
|
+
reason: "Agent refinement edits shared captions. Studio has manual output-family caption wording; both iPhone sizes share one family and cannot have separate words.",
|
|
7353
7438
|
evaluationCases: ["unsupported-per-output-caption"]
|
|
7354
7439
|
},
|
|
7355
7440
|
// ── Look and backdrop ─────────────────────────────────────────────────────────────────────────
|
|
@@ -7377,18 +7462,18 @@ var init_agentCapabilities = __esm({
|
|
|
7377
7462
|
wire: "patch.frames[i].background",
|
|
7378
7463
|
targets: ["frame"],
|
|
7379
7464
|
requiredInputs: ["the frame", "the backdrop fields"],
|
|
7380
|
-
evaluationCases: ["background-frame-one-amber", "inspection-correct-frame-two-background"]
|
|
7465
|
+
evaluationCases: ["background-frame-one-amber", "background-frame-three-flat-teal", "inspection-correct-frame-two-background"]
|
|
7381
7466
|
},
|
|
7382
7467
|
{
|
|
7383
7468
|
id: "frame.background-reset",
|
|
7384
7469
|
domain: "look",
|
|
7385
7470
|
status: "intentionally_excluded",
|
|
7386
|
-
summary: "Drop
|
|
7387
|
-
studioCommands: ["clearPanelBackground"],
|
|
7471
|
+
summary: "Drop one or all frame backdrops so they inherit the strip again.",
|
|
7472
|
+
studioCommands: ["clearPanelBackground", "clearAllPanelBackgrounds"],
|
|
7388
7473
|
wire: null,
|
|
7389
7474
|
targets: ["frame"],
|
|
7390
7475
|
requiredInputs: [],
|
|
7391
|
-
reason: "
|
|
7476
|
+
reason: "Agent refinement cannot reset backdrop inheritance. Use the frame background reset control in Studio to return it to the strip default.",
|
|
7392
7477
|
evaluationCases: ["unsupported-frame-background-reset"]
|
|
7393
7478
|
},
|
|
7394
7479
|
{
|
|
@@ -7407,13 +7492,13 @@ var init_agentCapabilities = __esm({
|
|
|
7407
7492
|
id: "frame.color",
|
|
7408
7493
|
domain: "look",
|
|
7409
7494
|
status: "intentionally_excluded",
|
|
7410
|
-
summary: "Set a frame\u2019s
|
|
7495
|
+
summary: "Set the legacy per-panel colour that sits under a frame\u2019s backdrop.",
|
|
7411
7496
|
studioCommands: ["setPanelColor"],
|
|
7412
7497
|
wire: null,
|
|
7413
7498
|
targets: ["frame"],
|
|
7414
7499
|
requiredInputs: [],
|
|
7415
|
-
reason: "No Studio surface dispatches it
|
|
7416
|
-
evaluationCases: ["
|
|
7500
|
+
reason: "No Studio surface dispatches it. A flat colour is a one-stop frame backdrop (`frame.background`), so a request for one is carried out there, never refused.",
|
|
7501
|
+
evaluationCases: ["background-frame-three-flat-teal"]
|
|
7417
7502
|
},
|
|
7418
7503
|
{
|
|
7419
7504
|
id: "look.design-preset",
|
|
@@ -7447,7 +7532,7 @@ var init_agentCapabilities = __esm({
|
|
|
7447
7532
|
wire: null,
|
|
7448
7533
|
targets: ["project"],
|
|
7449
7534
|
requiredInputs: [],
|
|
7450
|
-
reason: "
|
|
7535
|
+
reason: "Agent refinement cannot restore a saved Look. Restore the saved version through Look version history in Studio.",
|
|
7451
7536
|
evaluationCases: ["unsupported-restore-saved-look"]
|
|
7452
7537
|
},
|
|
7453
7538
|
// ── Output and locale axes ────────────────────────────────────────────────────────────────────
|
|
@@ -7476,29 +7561,42 @@ var init_agentCapabilities = __esm({
|
|
|
7476
7561
|
evaluationCases: ["project-add-german-locale", "geometry-clear-while-adding-a-language"]
|
|
7477
7562
|
},
|
|
7478
7563
|
// ── Assets ────────────────────────────────────────────────────────────────────────────────────
|
|
7564
|
+
// The whole-strip versions of the two asset capabilities (#880). No wire of their own: a whole-strip
|
|
7565
|
+
// request is carried out as the per-device operations above, one per supplied screenshot, so the
|
|
7566
|
+
// styling, captions and Output membership that hang off each device survive exactly as they do
|
|
7567
|
+
// for a single swap. Asset-gated the same way — with no screenshots on the turn they ask for them.
|
|
7479
7568
|
{
|
|
7480
7569
|
id: "strip.add-screens",
|
|
7481
7570
|
domain: "asset",
|
|
7482
|
-
status: "
|
|
7571
|
+
status: "supported",
|
|
7483
7572
|
summary: "Add new screenshots to the project, each in its own frame.",
|
|
7484
7573
|
studioCommands: ["addShots"],
|
|
7485
|
-
wire:
|
|
7574
|
+
wire: "structuralOperations: frame.add with a frameRef, then device.add into it, once per screenshot from AVAILABLE SCREENSHOTS",
|
|
7486
7575
|
targets: ["project"],
|
|
7487
7576
|
requiredInputs: ["one or more authorized screenshot references"],
|
|
7488
|
-
|
|
7489
|
-
|
|
7577
|
+
withoutAssetReason: "New frames need new pixels. Send the screenshots with the instruction and the Agent will place them.",
|
|
7578
|
+
limits: [
|
|
7579
|
+
"Only screenshots listed in AVAILABLE SCREENSHOTS; the Agent never invents or generates pixels.",
|
|
7580
|
+
"Each new frame starts without a caption and its device on the default look, exactly as adding screenshots in Studio does."
|
|
7581
|
+
],
|
|
7582
|
+
evaluationCases: ["asset-add-screens-supplied", "asset-add-screens-needs-pixels"]
|
|
7490
7583
|
},
|
|
7491
7584
|
{
|
|
7492
7585
|
id: "strip.replace-screens",
|
|
7493
7586
|
domain: "asset",
|
|
7494
|
-
status: "
|
|
7587
|
+
status: "supported",
|
|
7495
7588
|
summary: "Replace every screenshot in the project, keeping the styling.",
|
|
7496
7589
|
studioCommands: ["loadScreens"],
|
|
7497
|
-
wire:
|
|
7590
|
+
wire: "structuralOperations: device.replace on every device in strip order, each naming its own assetId from AVAILABLE SCREENSHOTS",
|
|
7498
7591
|
targets: ["project"],
|
|
7499
7592
|
requiredInputs: ["a complete authorized screenshot set"],
|
|
7500
|
-
|
|
7501
|
-
|
|
7593
|
+
withoutAssetReason: "Replacing the whole strip needs the whole new set of pixels. This instruction carried none.",
|
|
7594
|
+
limits: [
|
|
7595
|
+
"Only screenshots listed in AVAILABLE SCREENSHOTS; the Agent never invents or generates pixels.",
|
|
7596
|
+
"Every device keeps its look, placement, captions, translated screens and Output membership \u2014 only the pixels change.",
|
|
7597
|
+
"When the supplied screenshots do not match the devices one to one, the Agent asks which goes where instead of swapping some of them."
|
|
7598
|
+
],
|
|
7599
|
+
evaluationCases: ["asset-replace-all-screens-supplied", "asset-replace-all-screens-needs-pixels", "asset-replace-all-screens-count-mismatch"]
|
|
7502
7600
|
},
|
|
7503
7601
|
{
|
|
7504
7602
|
id: "locale.load-screens",
|
|
@@ -7566,8 +7664,8 @@ function validateAgentOutcome(outcome) {
|
|
|
7566
7664
|
if (outcome.status === "applied_with_remaining" && !outcome.remaining) {
|
|
7567
7665
|
problems.push("applied_with_remaining must name what is still outstanding.");
|
|
7568
7666
|
}
|
|
7569
|
-
if (outcome.question !== void 0 && outcome.status !== "needs_clarification") {
|
|
7570
|
-
problems.push(`Only needs_clarification
|
|
7667
|
+
if (outcome.question !== void 0 && outcome.status !== "needs_clarification" && outcome.status !== "needs_asset") {
|
|
7668
|
+
problems.push(`Only needs_clarification and needs_asset carry a question, not ${outcome.status}.`);
|
|
7571
7669
|
}
|
|
7572
7670
|
if (outcome.status === "needs_clarification" && !outcome.question) {
|
|
7573
7671
|
problems.push("needs_clarification must ask exactly one focused question.");
|
|
@@ -7575,6 +7673,16 @@ function validateAgentOutcome(outcome) {
|
|
|
7575
7673
|
if (outcome.capabilityId !== void 0 && !capabilityById(outcome.capabilityId)) {
|
|
7576
7674
|
problems.push(`Outcome names capability ${JSON.stringify(outcome.capabilityId)}, which the catalog does not list.`);
|
|
7577
7675
|
}
|
|
7676
|
+
if (outcome.capabilityIds !== void 0) {
|
|
7677
|
+
if (!Array.isArray(outcome.capabilityIds) || outcome.capabilityIds.length > MAX_OUTCOME_CAPABILITY_IDS) {
|
|
7678
|
+
problems.push(`capabilityIds must be a list of at most ${MAX_OUTCOME_CAPABILITY_IDS} catalog ids.`);
|
|
7679
|
+
} else {
|
|
7680
|
+
for (const id of outcome.capabilityIds) {
|
|
7681
|
+
if (typeof id !== "string" || !capabilityById(id)) problems.push(`Outcome lists capability ${JSON.stringify(id)}, which the catalog does not list.`);
|
|
7682
|
+
}
|
|
7683
|
+
if (new Set(outcome.capabilityIds).size !== outcome.capabilityIds.length) problems.push("capabilityIds repeats an id.");
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7578
7686
|
problems.push(...boundProblems("headline", outcome.headline, AGENT_OUTCOME_BOUNDS.headline, true));
|
|
7579
7687
|
problems.push(...boundProblems("detail", outcome.detail, AGENT_OUTCOME_BOUNDS.detail, false));
|
|
7580
7688
|
problems.push(...boundProblems("remaining", outcome.remaining, AGENT_OUTCOME_BOUNDS.remaining, false));
|
|
@@ -7591,6 +7699,77 @@ function boundProblems(field, value, maximum, required) {
|
|
|
7591
7699
|
if (value.length > maximum) return [`${field} is ${value.length} characters, over its ${maximum} bound.`];
|
|
7592
7700
|
return [];
|
|
7593
7701
|
}
|
|
7702
|
+
function capabilitiesForPatch(patch) {
|
|
7703
|
+
const ids = [];
|
|
7704
|
+
if (!patch) return ids;
|
|
7705
|
+
const add = (id) => {
|
|
7706
|
+
if (ids.indexOf(id) < 0) ids.push(id);
|
|
7707
|
+
};
|
|
7708
|
+
if (patch.captions) add("caption.text");
|
|
7709
|
+
if (patch.screenOrder) add("frame.reorder");
|
|
7710
|
+
if (patch.designPreset) add("look.design-preset");
|
|
7711
|
+
if (patch.layout) add("look.layout-template");
|
|
7712
|
+
if (patch.outputs) add("output.membership");
|
|
7713
|
+
if (patch.locales) add("locale.membership");
|
|
7714
|
+
if (patch.shots?.some((shot) => Object.keys(shot).length > 0)) add("device.look");
|
|
7715
|
+
if (patch.devices?.some((device) => Object.keys(device.patch).length > 0)) add("device.look");
|
|
7716
|
+
if (patch.captionStyle && Object.keys(patch.captionStyle).length > 0) add("caption.style");
|
|
7717
|
+
if (patch.background && Object.keys(patch.background).length > 0) add("strip.background");
|
|
7718
|
+
patch.frames?.forEach((frame) => {
|
|
7719
|
+
if (frame.captionStyle && Object.keys(frame.captionStyle).length > 0) add("caption.style");
|
|
7720
|
+
if (frame.background && Object.keys(frame.background).length > 0) add("frame.background");
|
|
7721
|
+
});
|
|
7722
|
+
patch.captionOperations?.forEach((operation) => {
|
|
7723
|
+
if (operation.operation === "add") add("caption.add");
|
|
7724
|
+
else if (operation.operation === "remove") add("caption.remove");
|
|
7725
|
+
else add(operation.frameId === operation.toFrameId ? "caption.reorder" : "caption.move-to-frame");
|
|
7726
|
+
});
|
|
7727
|
+
patch.frameOperations?.forEach((operation) => {
|
|
7728
|
+
if (operation.operation === "add") add("frame.add");
|
|
7729
|
+
else if (operation.operation === "remove") add("frame.remove");
|
|
7730
|
+
else if (operation.operation === "move") add("frame.reorder");
|
|
7731
|
+
else add("frame.split");
|
|
7732
|
+
});
|
|
7733
|
+
patch.structuralOperations?.forEach((operation) => add(capabilityForStructuralOperation(operation)));
|
|
7734
|
+
return ids;
|
|
7735
|
+
}
|
|
7736
|
+
function capabilityForStructuralOperation(operation) {
|
|
7737
|
+
switch (operation.op) {
|
|
7738
|
+
case "frame.add":
|
|
7739
|
+
return "frame.add";
|
|
7740
|
+
case "frame.remove":
|
|
7741
|
+
return "frame.remove";
|
|
7742
|
+
case "frame.move":
|
|
7743
|
+
return "frame.reorder";
|
|
7744
|
+
case "frame.split":
|
|
7745
|
+
return "frame.split";
|
|
7746
|
+
case "frame.combine":
|
|
7747
|
+
return "frame.combine";
|
|
7748
|
+
case "frame.template-reset":
|
|
7749
|
+
return "look.layout-template";
|
|
7750
|
+
case "caption.add":
|
|
7751
|
+
return "caption.add";
|
|
7752
|
+
case "caption.remove":
|
|
7753
|
+
return "caption.remove";
|
|
7754
|
+
// One `caption.move` covers both Studio controls: staying inside a frame is a reorder, crossing
|
|
7755
|
+
// to another is a move. The catalog names them separately because the user experiences them
|
|
7756
|
+
// separately.
|
|
7757
|
+
case "caption.move":
|
|
7758
|
+
return operation.frameId === operation.toFrameId ? "caption.reorder" : "caption.move-to-frame";
|
|
7759
|
+
case "device.move":
|
|
7760
|
+
return "device.move-to-frame";
|
|
7761
|
+
case "device.reorder":
|
|
7762
|
+
return "device.reorder";
|
|
7763
|
+
case "device.remove":
|
|
7764
|
+
return "device.remove";
|
|
7765
|
+
case "device.add":
|
|
7766
|
+
return "device.add";
|
|
7767
|
+
// The catalog names this one for what the user experiences — the pixels change, the device does
|
|
7768
|
+
// not — so the id deliberately does not match the wire's `device.replace`.
|
|
7769
|
+
case "device.replace":
|
|
7770
|
+
return "device.replace-pixels";
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7594
7773
|
function applicationOutcome(facts) {
|
|
7595
7774
|
if (facts.conflicted) {
|
|
7596
7775
|
return {
|
|
@@ -7620,7 +7799,13 @@ function applicationOutcome(facts) {
|
|
|
7620
7799
|
}
|
|
7621
7800
|
const gap = (facts.gaps ?? []).find((entry) => entry.kind === "asset") ?? (facts.gaps ?? []).find((entry) => entry.kind === "excluded") ?? (facts.gaps ?? []).find((entry) => entry.kind === "clarification");
|
|
7622
7801
|
if (!gap) {
|
|
7623
|
-
|
|
7802
|
+
const layout = facts.unresolvedLayout?.trim();
|
|
7803
|
+
return layout ? {
|
|
7804
|
+
status: "applied_with_remaining",
|
|
7805
|
+
headline: headlineForChanges(changed),
|
|
7806
|
+
changed,
|
|
7807
|
+
remaining: boundOutcomeText(layout, AGENT_OUTCOME_BOUNDS.remaining)
|
|
7808
|
+
} : { status: "applied", headline: headlineForChanges(changed), changed };
|
|
7624
7809
|
}
|
|
7625
7810
|
return {
|
|
7626
7811
|
status: "applied_with_remaining",
|
|
@@ -7654,7 +7839,14 @@ function cancelledOutcome() {
|
|
|
7654
7839
|
changed: []
|
|
7655
7840
|
};
|
|
7656
7841
|
}
|
|
7657
|
-
|
|
7842
|
+
function withTouchedCapabilities(outcome, touched) {
|
|
7843
|
+
const ids = [];
|
|
7844
|
+
for (const id of touched) if (capabilityById(id) && ids.indexOf(id) < 0) ids.push(id);
|
|
7845
|
+
if (outcome.capabilityId && capabilityById(outcome.capabilityId) && ids.indexOf(outcome.capabilityId) < 0) ids.push(outcome.capabilityId);
|
|
7846
|
+
if (ids.length === 0) return outcome;
|
|
7847
|
+
return { ...outcome, capabilityIds: ids.slice(0, MAX_OUTCOME_CAPABILITY_IDS) };
|
|
7848
|
+
}
|
|
7849
|
+
var AGENT_OUTCOME_STATUSES, AGENT_MUTATING_STATUSES, AGENT_OUTCOME_REASONS, AGENT_OUTCOME_BOUNDS, MAX_OUTCOME_CAPABILITY_IDS;
|
|
7658
7850
|
var init_agentOutcome = __esm({
|
|
7659
7851
|
"../mockup-engine/appstore/agentOutcome.ts"() {
|
|
7660
7852
|
"use strict";
|
|
@@ -7693,9 +7885,39 @@ var init_agentOutcome = __esm({
|
|
|
7693
7885
|
remaining: 300,
|
|
7694
7886
|
/** The label on the single next step. */
|
|
7695
7887
|
nextStep: 120,
|
|
7696
|
-
/**
|
|
7697
|
-
question:
|
|
7888
|
+
/** Transport ceiling for a complete assistant question or screenshot request; never clip it. */
|
|
7889
|
+
question: 32e3
|
|
7698
7890
|
};
|
|
7891
|
+
MAX_OUTCOME_CAPABILITY_IDS = 16;
|
|
7892
|
+
}
|
|
7893
|
+
});
|
|
7894
|
+
|
|
7895
|
+
// ../mockup-engine/appstore/agentUnmetActions.ts
|
|
7896
|
+
function normalizeIds(ids) {
|
|
7897
|
+
return Array.from(new Set(ids)).sort().slice(0, MAX_UNMET_ACTIONS_PER_TURN);
|
|
7898
|
+
}
|
|
7899
|
+
function classifiedDemand(ids) {
|
|
7900
|
+
return { classification: "classified", actionIds: normalizeIds(ids) };
|
|
7901
|
+
}
|
|
7902
|
+
var AGENT_UNMET_ACTION_UNKNOWN, MAX_UNMET_ACTIONS_PER_TURN, AGENT_UNMET_ACTIONS, CAPABILITY_IDS, AGENT_UNMET_ACTION_VOCABULARY, NOT_MEASURED_DEMAND;
|
|
7903
|
+
var init_agentUnmetActions = __esm({
|
|
7904
|
+
"../mockup-engine/appstore/agentUnmetActions.ts"() {
|
|
7905
|
+
"use strict";
|
|
7906
|
+
init_agentCapabilities();
|
|
7907
|
+
AGENT_UNMET_ACTION_UNKNOWN = "unknown";
|
|
7908
|
+
MAX_UNMET_ACTIONS_PER_TURN = 8;
|
|
7909
|
+
AGENT_UNMET_ACTIONS = [
|
|
7910
|
+
{ id: "image.generate", summary: "Generate or paint new imagery rather than arranging supplied screenshots." },
|
|
7911
|
+
{ id: "animation.create", summary: "Animate a screen, produce a video, or export motion instead of stills." },
|
|
7912
|
+
{ id: "device.unsupported-model", summary: "Use a device model ShotOps does not offer." }
|
|
7913
|
+
];
|
|
7914
|
+
CAPABILITY_IDS = AGENT_CAPABILITIES.map((capability) => capability.id);
|
|
7915
|
+
AGENT_UNMET_ACTION_VOCABULARY = [
|
|
7916
|
+
...CAPABILITY_IDS,
|
|
7917
|
+
...AGENT_UNMET_ACTIONS.map((action) => action.id),
|
|
7918
|
+
AGENT_UNMET_ACTION_UNKNOWN
|
|
7919
|
+
];
|
|
7920
|
+
NOT_MEASURED_DEMAND = { classification: "not_measured", actionIds: [] };
|
|
7699
7921
|
}
|
|
7700
7922
|
});
|
|
7701
7923
|
|
|
@@ -8164,7 +8386,7 @@ function applyFrameOperation(state, operation) {
|
|
|
8164
8386
|
if (operation.operation === "add") {
|
|
8165
8387
|
if (operation.afterFrameId !== void 0 && panelIndex(state, operation.afterFrameId) < 0) return null;
|
|
8166
8388
|
const frameId = nextAgentPanelId(state);
|
|
8167
|
-
const added = addPanel(state, frameId);
|
|
8389
|
+
const added = inheritFrameTemplate(addPanel(state, frameId), frameId, operation.afterFrameId ?? state.panels[state.panels.length - 1]?.id);
|
|
8168
8390
|
return operation.afterFrameId === void 0 ? added : movePanelAfter(added, frameId, operation.afterFrameId);
|
|
8169
8391
|
}
|
|
8170
8392
|
if (operation.operation === "remove") {
|
|
@@ -8187,7 +8409,17 @@ function applyFrameOperation(state, operation) {
|
|
|
8187
8409
|
if (!moved) return null;
|
|
8188
8410
|
next = moved;
|
|
8189
8411
|
}
|
|
8190
|
-
return next;
|
|
8412
|
+
return inheritFrameTemplate(next, newFrameId, operation.frameId);
|
|
8413
|
+
}
|
|
8414
|
+
function inheritFrameTemplate(state, panelId, fromPanelId) {
|
|
8415
|
+
const source = fromPanelId === void 0 ? void 0 : state.frameTemplates?.[fromPanelId];
|
|
8416
|
+
if (!source) return state;
|
|
8417
|
+
const baseline = captureFrameTemplateBaseline(
|
|
8418
|
+
{ shots: state.shots, captionText: state.captionText, captionSlotIds: captionSlotIds(state), baseLocale: state.baseLocale },
|
|
8419
|
+
panelId,
|
|
8420
|
+
source.templateId
|
|
8421
|
+
);
|
|
8422
|
+
return { ...state, frameTemplates: { ...state.frameTemplates ?? {}, [panelId]: baseline } };
|
|
8191
8423
|
}
|
|
8192
8424
|
function addCaptionLayer(state, panelId, text3, style2) {
|
|
8193
8425
|
const source = state.captionText[state.baseLocale] ?? {};
|
|
@@ -8493,19 +8725,67 @@ function applyStructuralOperation(state, operation, assets = [], bindings) {
|
|
|
8493
8725
|
}
|
|
8494
8726
|
}
|
|
8495
8727
|
}
|
|
8728
|
+
function applyMergedCaptionMove(state, merge, index, toIndex) {
|
|
8729
|
+
const slot2 = merge.pending[index];
|
|
8730
|
+
if (slot2 === void 0 || panelIndex(state, merge.into) < 0) return null;
|
|
8731
|
+
const ids = captionSlotIds(state)[merge.into] ?? [];
|
|
8732
|
+
const settled = ids.length - merge.pending.length;
|
|
8733
|
+
if (settled < 0 || ids.slice(settled).some((id, offset) => id !== merge.pending[offset])) return null;
|
|
8734
|
+
const to = toIndex ?? settled;
|
|
8735
|
+
if (to < 0 || to > settled) return null;
|
|
8736
|
+
const moved = applyCaptionOperation(state, {
|
|
8737
|
+
operation: "move",
|
|
8738
|
+
frameId: merge.into,
|
|
8739
|
+
index: ids.indexOf(slot2),
|
|
8740
|
+
toFrameId: merge.into,
|
|
8741
|
+
toIndex: to
|
|
8742
|
+
});
|
|
8743
|
+
if (moved) merge.pending.splice(index, 1);
|
|
8744
|
+
return moved;
|
|
8745
|
+
}
|
|
8496
8746
|
function applyStructuralOperations(state, patch, assets = []) {
|
|
8497
8747
|
let next = state;
|
|
8498
8748
|
const assetBindings = [];
|
|
8499
|
-
|
|
8500
|
-
|
|
8749
|
+
const frameRefs = /* @__PURE__ */ new Map();
|
|
8750
|
+
const retiredRefs = /* @__PURE__ */ new Set();
|
|
8751
|
+
const merged = /* @__PURE__ */ new Map();
|
|
8752
|
+
for (const raw of normalizeStructuralOperations(patch)) {
|
|
8753
|
+
const ref = raw.op === "frame.add" ? raw.frameRef : void 0;
|
|
8754
|
+
if (ref !== void 0 && (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(ref) || frameRefs.has(ref) || state.panels.some((panel) => panel.id === ref) || state.shots.some((shot) => shot.id === ref))) return null;
|
|
8755
|
+
if (structuralOperationTargets(raw).frameIds.some((id) => retiredRefs.has(id))) return null;
|
|
8756
|
+
const resolve14 = (id) => frameRefs.get(id) ?? id;
|
|
8757
|
+
const operation = {
|
|
8758
|
+
...raw,
|
|
8759
|
+
..."frameId" in raw ? { frameId: resolve14(raw.frameId) } : {},
|
|
8760
|
+
..."toFrameId" in raw ? { toFrameId: resolve14(raw.toFrameId) } : {},
|
|
8761
|
+
..."fromFrameId" in raw ? { fromFrameId: resolve14(raw.fromFrameId) } : {},
|
|
8762
|
+
..."afterFrameId" in raw && raw.afterFrameId !== void 0 ? { afterFrameId: resolve14(raw.afterFrameId) } : {}
|
|
8763
|
+
};
|
|
8764
|
+
const merge = operation.op === "caption.move" && panelIndex(next, operation.frameId) < 0 ? merged.get(operation.frameId) : void 0;
|
|
8765
|
+
const applied = operation.op === "caption.move" && merge?.into === operation.toFrameId ? applyMergedCaptionMove(next, merge, operation.index, operation.toIndex) : applyStructuralOperation(next, operation, assets, assetBindings);
|
|
8501
8766
|
if (!applied) return null;
|
|
8767
|
+
if (operation.op === "frame.combine") {
|
|
8768
|
+
const carried = captionSlotIds(next)[operation.fromFrameId]?.length ?? 0;
|
|
8769
|
+
const survivor = captionSlotIds(applied)[operation.frameId] ?? [];
|
|
8770
|
+
merged.set(operation.fromFrameId, { into: operation.frameId, pending: carried > 0 ? survivor.slice(-carried) : [] });
|
|
8771
|
+
}
|
|
8772
|
+
if (ref !== void 0) {
|
|
8773
|
+
const created = applied.panels.find((panel) => !next.panels.some((previous) => previous.id === panel.id));
|
|
8774
|
+
if (!created) return null;
|
|
8775
|
+
frameRefs.set(ref, created.id);
|
|
8776
|
+
}
|
|
8777
|
+
frameRefs.forEach((id, alias) => {
|
|
8778
|
+
if (!applied.panels.some((panel) => panel.id === id)) retiredRefs.add(alias);
|
|
8779
|
+
});
|
|
8502
8780
|
next = applied;
|
|
8503
8781
|
}
|
|
8504
|
-
|
|
8782
|
+
const survivingRefs = Array.from(frameRefs.entries()).filter(([alias]) => !retiredRefs.has(alias));
|
|
8783
|
+
return { state: next, assetBindings, ...survivingRefs.length > 0 ? { frameRefs: Object.fromEntries(survivingRefs) } : {} };
|
|
8505
8784
|
}
|
|
8506
8785
|
var init_refinementStructure = __esm({
|
|
8507
8786
|
"../mockup-engine/appstore/refinementStructure.ts"() {
|
|
8508
8787
|
"use strict";
|
|
8788
|
+
init_refineIntent();
|
|
8509
8789
|
init_defaults();
|
|
8510
8790
|
init_recordTransforms();
|
|
8511
8791
|
init_shot();
|
|
@@ -8514,6 +8794,57 @@ var init_refinementStructure = __esm({
|
|
|
8514
8794
|
}
|
|
8515
8795
|
});
|
|
8516
8796
|
|
|
8797
|
+
// ../mockup-engine/appstore/templatePoseGuard.ts
|
|
8798
|
+
function withoutPose(entry) {
|
|
8799
|
+
if (!TEMPLATE_OWNED_SHOT_FIELDS.some((field) => entry[field] !== void 0)) return null;
|
|
8800
|
+
const kept = { ...entry };
|
|
8801
|
+
for (const field of TEMPLATE_OWNED_SHOT_FIELDS) delete kept[field];
|
|
8802
|
+
return kept;
|
|
8803
|
+
}
|
|
8804
|
+
function dropTemplatePoses(patch, templated, panelIds, panelOfDevice) {
|
|
8805
|
+
if (templated.size === 0 || !patch.shots && !patch.devices) return { patch, dropped: false };
|
|
8806
|
+
let dropped = false;
|
|
8807
|
+
let next = patch;
|
|
8808
|
+
if (patch.shots) {
|
|
8809
|
+
const shots = patch.shots.map((entry, index) => {
|
|
8810
|
+
const panelId = panelIds[index];
|
|
8811
|
+
const kept = panelId !== void 0 && templated.has(panelId) ? withoutPose(entry) : null;
|
|
8812
|
+
if (!kept) return entry;
|
|
8813
|
+
dropped = true;
|
|
8814
|
+
return kept;
|
|
8815
|
+
});
|
|
8816
|
+
if (dropped) next = { ...next, shots };
|
|
8817
|
+
}
|
|
8818
|
+
if (patch.devices) {
|
|
8819
|
+
let devicesDropped = false;
|
|
8820
|
+
const devices = [];
|
|
8821
|
+
for (const entry of patch.devices) {
|
|
8822
|
+
const panelId = panelOfDevice(entry.deviceId);
|
|
8823
|
+
const kept = panelId !== void 0 && templated.has(panelId) ? withoutPose(entry.patch) : null;
|
|
8824
|
+
if (!kept) {
|
|
8825
|
+
devices.push(entry);
|
|
8826
|
+
continue;
|
|
8827
|
+
}
|
|
8828
|
+
devicesDropped = true;
|
|
8829
|
+
if (Object.keys(kept).length > 0) devices.push({ ...entry, patch: kept });
|
|
8830
|
+
}
|
|
8831
|
+
if (devicesDropped) {
|
|
8832
|
+
dropped = true;
|
|
8833
|
+
const { devices: _replaced, ...rest } = next;
|
|
8834
|
+
void _replaced;
|
|
8835
|
+
next = devices.length > 0 ? { ...rest, devices } : rest;
|
|
8836
|
+
}
|
|
8837
|
+
}
|
|
8838
|
+
return { patch: next, dropped };
|
|
8839
|
+
}
|
|
8840
|
+
var TEMPLATE_OWNED_SHOT_FIELDS;
|
|
8841
|
+
var init_templatePoseGuard = __esm({
|
|
8842
|
+
"../mockup-engine/appstore/templatePoseGuard.ts"() {
|
|
8843
|
+
"use strict";
|
|
8844
|
+
TEMPLATE_OWNED_SHOT_FIELDS = ["orbit", "roll", "angle", "phoneHeight", "hOffset", "vOffset"];
|
|
8845
|
+
}
|
|
8846
|
+
});
|
|
8847
|
+
|
|
8517
8848
|
// ../mockup-engine/appstore/refinementEffect.ts
|
|
8518
8849
|
function describeRefinementEffect(before, after) {
|
|
8519
8850
|
const changes = [];
|
|
@@ -8525,6 +8856,17 @@ function describeRefinementEffect(before, after) {
|
|
|
8525
8856
|
if (framesRemoved > 0) changes.push(`Removed ${framesRemoved} frame${framesRemoved === 1 ? "" : "s"}`);
|
|
8526
8857
|
const beforeCaptions = before.captionText[before.baseLocale] ?? {};
|
|
8527
8858
|
const afterCaptions = after.captionText[after.baseLocale] ?? {};
|
|
8859
|
+
const beforeSlots = normalizeCaptionSlotIds(before.captionSlotIds, before.captionText, before.panels.map((panel) => panel.id));
|
|
8860
|
+
const afterSlots = normalizeCaptionSlotIds(after.captionSlotIds, after.captionText, after.panels.map((panel) => panel.id));
|
|
8861
|
+
const reorderOf = (panelId) => {
|
|
8862
|
+
const was = beforeSlots[panelId] ?? [];
|
|
8863
|
+
const now = afterSlots[panelId] ?? [];
|
|
8864
|
+
if (was.length !== now.length || was.join("\0") === now.join("\0")) return null;
|
|
8865
|
+
const source = now.map((id) => was.indexOf(id));
|
|
8866
|
+
return source.every((index, position) => index >= 0 && source.indexOf(index) === position) ? source : null;
|
|
8867
|
+
};
|
|
8868
|
+
const sourceIndex = (panelId, index) => reorderOf(panelId)?.[index] ?? index;
|
|
8869
|
+
let captionFramesReordered = 0;
|
|
8528
8870
|
let rewritten = 0;
|
|
8529
8871
|
let captionSlotsAdded = 0;
|
|
8530
8872
|
let captionSlotsRemoved = 0;
|
|
@@ -8535,8 +8877,9 @@ function describeRefinementEffect(before, after) {
|
|
|
8535
8877
|
captionSlotsAdded += Math.max(0, newLayers.length - oldLayers.length);
|
|
8536
8878
|
captionSlotsRemoved += Math.max(0, oldLayers.length - newLayers.length);
|
|
8537
8879
|
if (oldLayers.length !== newLayers.length) continue;
|
|
8880
|
+
if (reorderOf(panelId)) captionFramesReordered += 1;
|
|
8538
8881
|
for (let index = 0; index < oldLayers.length; index++) {
|
|
8539
|
-
if (oldLayers[index]
|
|
8882
|
+
if (oldLayers[sourceIndex(panelId, index)]?.text !== newLayers[index].text) rewritten += 1;
|
|
8540
8883
|
}
|
|
8541
8884
|
}
|
|
8542
8885
|
const captionWordsIn = (source) => Object.entries(source).flatMap(([panelId, layers]) => layers.map((layer) => `${panelId}\0${layer.text}`));
|
|
@@ -8553,6 +8896,9 @@ function describeRefinementEffect(before, after) {
|
|
|
8553
8896
|
captionSlotsAdded -= movedCaptions;
|
|
8554
8897
|
captionSlotsRemoved -= movedCaptions;
|
|
8555
8898
|
if (rewritten > 0) changes.push(`Rewrote ${rewritten} caption${rewritten === 1 ? "" : "s"}`);
|
|
8899
|
+
if (captionFramesReordered > 0) {
|
|
8900
|
+
changes.push(`Reordered the captions in ${captionFramesReordered} frame${captionFramesReordered === 1 ? "" : "s"}`);
|
|
8901
|
+
}
|
|
8556
8902
|
if (movedCaptions > 0) {
|
|
8557
8903
|
changes.push(`Moved ${movedCaptions} caption${movedCaptions === 1 ? "" : "s"} to another frame`);
|
|
8558
8904
|
}
|
|
@@ -8585,7 +8931,10 @@ function describeRefinementEffect(before, after) {
|
|
|
8585
8931
|
const oldLayers = oldByPanel[panelId] ?? [];
|
|
8586
8932
|
const newLayers = newByPanel[panelId] ?? [];
|
|
8587
8933
|
if (oldLayers.length !== newLayers.length) return false;
|
|
8588
|
-
return
|
|
8934
|
+
return newLayers.some((layer, index) => {
|
|
8935
|
+
const prior = oldLayers[sourceIndex(panelId, index)];
|
|
8936
|
+
return !prior || styleOf(prior) !== styleOf(layer);
|
|
8937
|
+
});
|
|
8589
8938
|
});
|
|
8590
8939
|
});
|
|
8591
8940
|
const styleByText = (source) => {
|
|
@@ -8652,6 +9001,7 @@ var init_refinementEffect = __esm({
|
|
|
8652
9001
|
"../mockup-engine/appstore/refinementEffect.ts"() {
|
|
8653
9002
|
"use strict";
|
|
8654
9003
|
init_gradient();
|
|
9004
|
+
init_frameTemplates();
|
|
8655
9005
|
init_refinementValues();
|
|
8656
9006
|
}
|
|
8657
9007
|
});
|
|
@@ -8755,7 +9105,18 @@ function applyProjectRefinementWithAssets(state, patch, panelIds, assets = []) {
|
|
|
8755
9105
|
next = { ...next, shots, captionText, ...overlay.background };
|
|
8756
9106
|
}
|
|
8757
9107
|
}
|
|
8758
|
-
if (patch.layout)
|
|
9108
|
+
if (patch.layout) {
|
|
9109
|
+
next = applyLayout(next, patch.layout, sourcePanelIds);
|
|
9110
|
+
if (layoutTemplateById(patch.layout)) {
|
|
9111
|
+
const shots = next.shots;
|
|
9112
|
+
patch = dropTemplatePoses(
|
|
9113
|
+
patch,
|
|
9114
|
+
new Set(sourcePanelIds),
|
|
9115
|
+
requestedPanelIds,
|
|
9116
|
+
(deviceId) => shots.find((shot) => shot.id === deviceId)?.panelId
|
|
9117
|
+
).patch;
|
|
9118
|
+
}
|
|
9119
|
+
}
|
|
8759
9120
|
if (patch.shots?.length === positionalCount) {
|
|
8760
9121
|
const entries = new Map(requestedPanelIds.map((panelId, index) => [panelId, patch.shots[index]]));
|
|
8761
9122
|
const shared = new Set(
|
|
@@ -8877,7 +9238,7 @@ function applyProjectRefinementWithAssets(state, patch, panelIds, assets = []) {
|
|
|
8877
9238
|
next = { ...next, panels, shots: shotsInPanelOrder(next.shots, panels) };
|
|
8878
9239
|
}
|
|
8879
9240
|
}
|
|
8880
|
-
return { state: next, assetBindings: structured.assetBindings };
|
|
9241
|
+
return { state: next, assetBindings: structured.assetBindings, ...structured.frameRefs ? { frameRefs: structured.frameRefs } : {} };
|
|
8881
9242
|
}
|
|
8882
9243
|
var init_refinement = __esm({
|
|
8883
9244
|
"../mockup-engine/appstore/refinement.ts"() {
|
|
@@ -8892,6 +9253,7 @@ var init_refinement = __esm({
|
|
|
8892
9253
|
init_refinementStructure();
|
|
8893
9254
|
init_refinementValues();
|
|
8894
9255
|
init_gradient();
|
|
9256
|
+
init_templatePoseGuard();
|
|
8895
9257
|
init_refinementEffect();
|
|
8896
9258
|
init_refinementStructure();
|
|
8897
9259
|
}
|
|
@@ -8964,6 +9326,80 @@ var init_appStoreLocales = __esm({
|
|
|
8964
9326
|
}
|
|
8965
9327
|
});
|
|
8966
9328
|
|
|
9329
|
+
// ../mockup-engine/appstore/agentTurnRepetition.ts
|
|
9330
|
+
function appliedOperationOf(operation) {
|
|
9331
|
+
if (!NON_REPEATABLE_OPS.has(operation.op)) return null;
|
|
9332
|
+
if (operation.op === "caption.remove") return { op: operation.op, target: `${operation.frameId}#${operation.index}` };
|
|
9333
|
+
if (operation.op === "device.remove") return { op: operation.op, target: operation.deviceId };
|
|
9334
|
+
const target = "frameId" in operation ? operation.frameId : "toFrameId" in operation ? operation.toFrameId : "afterFrameId" in operation && operation.afterFrameId ? `after:${operation.afterFrameId}` : "strip";
|
|
9335
|
+
return { op: operation.op, target };
|
|
9336
|
+
}
|
|
9337
|
+
function normalizeTarget(target, refs) {
|
|
9338
|
+
if (target.startsWith("after:")) {
|
|
9339
|
+
const id = target.slice(6);
|
|
9340
|
+
return `after:${refs.get(id) ?? id}`;
|
|
9341
|
+
}
|
|
9342
|
+
const ordinal = target.indexOf("#");
|
|
9343
|
+
if (ordinal >= 0) {
|
|
9344
|
+
const id = target.slice(0, ordinal);
|
|
9345
|
+
return `${refs.get(id) ?? id}${target.slice(ordinal)}`;
|
|
9346
|
+
}
|
|
9347
|
+
return refs.get(target) ?? target;
|
|
9348
|
+
}
|
|
9349
|
+
function contextualOperations(operations, resolved = {}) {
|
|
9350
|
+
const refs = /* @__PURE__ */ new Map();
|
|
9351
|
+
return operations.map((operation) => {
|
|
9352
|
+
const applied = appliedOperationOf(operation);
|
|
9353
|
+
const normalized = applied ? { ...applied, target: normalizeTarget(applied.target, refs) } : null;
|
|
9354
|
+
if (operation.op === "frame.add" && operation.frameRef && normalized) {
|
|
9355
|
+
refs.set(operation.frameRef, Object.prototype.hasOwnProperty.call(resolved, operation.frameRef) ? resolved[operation.frameRef] : `created-frame:${normalized.target}`);
|
|
9356
|
+
}
|
|
9357
|
+
return normalized;
|
|
9358
|
+
});
|
|
9359
|
+
}
|
|
9360
|
+
function appliedOperationsIn(patch, frameRefs) {
|
|
9361
|
+
const operations = patch.structuralOperations ?? [];
|
|
9362
|
+
const symbolic = contextualOperations(operations).flatMap((applied) => applied ? [applied] : []);
|
|
9363
|
+
const durable = frameRefs ? contextualOperations(operations, frameRefs).flatMap((applied) => applied ? [applied] : []) : [];
|
|
9364
|
+
return symbolic.concat(durable.filter((applied) => !symbolic.some((entry) => entry.op === applied.op && entry.target === applied.target)));
|
|
9365
|
+
}
|
|
9366
|
+
function dropRepeatedOperations(patch, already) {
|
|
9367
|
+
const operations = patch.structuralOperations;
|
|
9368
|
+
if (!operations || operations.length === 0 || already.length === 0) return { patch, dropped: [] };
|
|
9369
|
+
const seen = new Set(already.map((applied2) => `${applied2.op} ${applied2.target}`));
|
|
9370
|
+
const dropped = [];
|
|
9371
|
+
const applied = contextualOperations(operations);
|
|
9372
|
+
const unavailableRefs = /* @__PURE__ */ new Set();
|
|
9373
|
+
const kept = operations.filter((operation, index) => {
|
|
9374
|
+
const entry = applied[index];
|
|
9375
|
+
const dependent = structuralOperationTargets(operation).frameIds.some((id) => unavailableRefs.has(id));
|
|
9376
|
+
if (!dependent && (!entry || !seen.has(`${entry.op} ${entry.target}`))) return true;
|
|
9377
|
+
if (operation.op === "frame.add" && operation.frameRef) unavailableRefs.add(operation.frameRef);
|
|
9378
|
+
if (entry) dropped.push(entry);
|
|
9379
|
+
return false;
|
|
9380
|
+
});
|
|
9381
|
+
if (dropped.length === 0) return { patch, dropped };
|
|
9382
|
+
const { structuralOperations: _dropped, ...rest } = patch;
|
|
9383
|
+
void _dropped;
|
|
9384
|
+
return { patch: kept.length > 0 ? { ...rest, structuralOperations: kept } : rest, dropped };
|
|
9385
|
+
}
|
|
9386
|
+
var NON_REPEATABLE_OPS;
|
|
9387
|
+
var init_agentTurnRepetition = __esm({
|
|
9388
|
+
"../mockup-engine/appstore/agentTurnRepetition.ts"() {
|
|
9389
|
+
"use strict";
|
|
9390
|
+
init_refineIntent();
|
|
9391
|
+
NON_REPEATABLE_OPS = /* @__PURE__ */ new Set([
|
|
9392
|
+
"frame.add",
|
|
9393
|
+
"frame.split",
|
|
9394
|
+
"caption.add",
|
|
9395
|
+
"device.add",
|
|
9396
|
+
"frame.remove",
|
|
9397
|
+
"caption.remove",
|
|
9398
|
+
"device.remove"
|
|
9399
|
+
]);
|
|
9400
|
+
}
|
|
9401
|
+
});
|
|
9402
|
+
|
|
8967
9403
|
// ../mockup-engine/appstore/agentTurnCoordinator.ts
|
|
8968
9404
|
function phaseFor(cycle) {
|
|
8969
9405
|
return cycle === 1 ? "applying" : cycle === 2 ? "inspecting" : "adjusting";
|
|
@@ -9008,25 +9444,38 @@ async function runAgentTurn(input, ports) {
|
|
|
9008
9444
|
if (!turnId) throw new Error("Agent turn id must not be empty");
|
|
9009
9445
|
const emit = (event) => ports.emit({ ...event, turnId });
|
|
9010
9446
|
const now = input.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
9447
|
+
const layoutRepairable = canRepairLayoutForFocus(input.focus, instruction);
|
|
9011
9448
|
const loaded = await ports.loadProject();
|
|
9012
9449
|
const before = loaded.state;
|
|
9450
|
+
const sourceCaptions = before.panels.map((panel) => ({
|
|
9451
|
+
frameId: panel.id,
|
|
9452
|
+
captions: (before.captionText[before.baseLocale]?.[panel.id] ?? []).map((layer) => layer.text)
|
|
9453
|
+
}));
|
|
9013
9454
|
let working = before;
|
|
9014
9455
|
let rendered;
|
|
9456
|
+
let renderedState = null;
|
|
9457
|
+
let measuredLayout = null;
|
|
9015
9458
|
const passes = [];
|
|
9016
9459
|
const broaderChanges = /* @__PURE__ */ new Set();
|
|
9017
9460
|
let checkpointId = 0;
|
|
9018
9461
|
let best = null;
|
|
9019
9462
|
let selected = null;
|
|
9020
9463
|
let workingAssetBindings = [];
|
|
9464
|
+
const unmetActions = /* @__PURE__ */ new Set();
|
|
9465
|
+
let demandClassified = false;
|
|
9021
9466
|
let interpretation = "";
|
|
9022
9467
|
let inspection = "";
|
|
9023
9468
|
let remainingMismatch = null;
|
|
9469
|
+
const appliedOperations = [];
|
|
9470
|
+
const touchedCapabilities = /* @__PURE__ */ new Set();
|
|
9471
|
+
const templatedPanels = /* @__PURE__ */ new Set();
|
|
9024
9472
|
let terminalOutcome = null;
|
|
9025
9473
|
let invalid2 = false;
|
|
9026
9474
|
let cancelled = false;
|
|
9027
9475
|
await emit({ type: "turn_started" });
|
|
9028
9476
|
try {
|
|
9029
9477
|
rendered = await ports.renderComposed(working, 0);
|
|
9478
|
+
renderedState = working;
|
|
9030
9479
|
} catch {
|
|
9031
9480
|
terminalOutcome = failureOutcome(
|
|
9032
9481
|
"service_unavailable",
|
|
@@ -9049,15 +9498,27 @@ async function runAgentTurn(input, ports) {
|
|
|
9049
9498
|
focus: input.focus,
|
|
9050
9499
|
state: working,
|
|
9051
9500
|
rendered,
|
|
9052
|
-
...cycle > 1 ? {
|
|
9501
|
+
...cycle > 1 ? {
|
|
9502
|
+
previous: {
|
|
9503
|
+
actualDiff: selected?.changed ?? [],
|
|
9504
|
+
inspection,
|
|
9505
|
+
cycle: cycle - 1,
|
|
9506
|
+
appliedOperations: [...appliedOperations],
|
|
9507
|
+
sourceCaptions: sourceCaptions.map((frame) => ({ ...frame, captions: [...frame.captions] }))
|
|
9508
|
+
}
|
|
9509
|
+
} : {}
|
|
9053
9510
|
});
|
|
9054
|
-
} catch {
|
|
9055
|
-
terminalOutcome = failureOutcome(
|
|
9511
|
+
} catch (error) {
|
|
9512
|
+
terminalOutcome = error instanceof AgentTurnAuthorizationError ? error.outcome : failureOutcome(
|
|
9056
9513
|
"service_unavailable",
|
|
9057
9514
|
"The Agent could not complete this turn. Nothing changed."
|
|
9058
9515
|
);
|
|
9059
9516
|
break;
|
|
9060
9517
|
}
|
|
9518
|
+
if (authorization.demand?.classification === "classified") {
|
|
9519
|
+
demandClassified = true;
|
|
9520
|
+
for (const actionId of authorization.demand.actionIds) unmetActions.add(actionId);
|
|
9521
|
+
}
|
|
9061
9522
|
interpretation ||= authorization.interpretation?.trim() || instruction;
|
|
9062
9523
|
inspection = authorization.inspection?.trim() || "Inspected the current composed result.";
|
|
9063
9524
|
const pass = {
|
|
@@ -9085,7 +9546,22 @@ async function runAgentTurn(input, ports) {
|
|
|
9085
9546
|
}
|
|
9086
9547
|
remainingMismatch = authorization.remainingMismatch?.trim() || authorization.followUp?.trim() || null;
|
|
9087
9548
|
for (const change of authorization.broaderChanges ?? []) broaderChanges.add(change);
|
|
9549
|
+
if (authorization.patch && cycle > 1) {
|
|
9550
|
+
const guarded = dropRepeatedOperations(authorization.patch, appliedOperations);
|
|
9551
|
+
if (guarded.dropped.length > 0) authorization = { ...authorization, patch: guarded.patch };
|
|
9552
|
+
}
|
|
9553
|
+
if (authorization.patch && templatedPanels.size > 0) {
|
|
9554
|
+
const shots = working.shots;
|
|
9555
|
+
const guarded = dropTemplatePoses(
|
|
9556
|
+
authorization.patch,
|
|
9557
|
+
templatedPanels,
|
|
9558
|
+
authorization.panelIds,
|
|
9559
|
+
(deviceId) => shots.find((shot) => shot.id === deviceId)?.panelId
|
|
9560
|
+
);
|
|
9561
|
+
if (guarded.dropped) authorization = { ...authorization, patch: guarded.patch };
|
|
9562
|
+
}
|
|
9088
9563
|
if (authorization.patch) {
|
|
9564
|
+
for (const id of capabilitiesForPatch(authorization.patch)) touchedCapabilities.add(id);
|
|
9089
9565
|
const application = applyProjectRefinementWithAssets(
|
|
9090
9566
|
working,
|
|
9091
9567
|
authorization.patch,
|
|
@@ -9097,6 +9573,12 @@ async function runAgentTurn(input, ports) {
|
|
|
9097
9573
|
break;
|
|
9098
9574
|
}
|
|
9099
9575
|
working = application.state;
|
|
9576
|
+
if (authorization.patch.layout && layoutTemplateById(authorization.patch.layout)) {
|
|
9577
|
+
for (const panelId of authorization.panelIds) {
|
|
9578
|
+
if (working.panels.some((panel) => panel.id === panelId)) templatedPanels.add(panelId);
|
|
9579
|
+
}
|
|
9580
|
+
}
|
|
9581
|
+
appliedOperations.push(...appliedOperationsIn(authorization.patch, application.frameRefs));
|
|
9100
9582
|
workingAssetBindings = [...workingAssetBindings, ...application.assetBindings];
|
|
9101
9583
|
checkpointId += 1;
|
|
9102
9584
|
const checkpoint = {
|
|
@@ -9123,15 +9605,35 @@ async function runAgentTurn(input, ports) {
|
|
|
9123
9605
|
});
|
|
9124
9606
|
}
|
|
9125
9607
|
const requiresRenderedInspection = cycle === 1 && authorization.patch != null;
|
|
9126
|
-
|
|
9608
|
+
let renderedThisCycle = false;
|
|
9609
|
+
let unresolvedLayout = null;
|
|
9610
|
+
if (layoutRepairable && authorization.complete === true && !requiresRenderedInspection && ports.unresolvedLayout) {
|
|
9611
|
+
let measurable = true;
|
|
9612
|
+
if (authorization.patch != null) {
|
|
9613
|
+
try {
|
|
9614
|
+
rendered = await ports.renderComposed(working, cycle);
|
|
9615
|
+
renderedState = working;
|
|
9616
|
+
renderedThisCycle = true;
|
|
9617
|
+
} catch {
|
|
9618
|
+
measurable = false;
|
|
9619
|
+
}
|
|
9620
|
+
}
|
|
9621
|
+
if (measurable) {
|
|
9622
|
+
unresolvedLayout = ports.unresolvedLayout(rendered)?.trim() || null;
|
|
9623
|
+
if (renderedState) measuredLayout = { state: renderedState, collision: unresolvedLayout };
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9626
|
+
if (authorization.complete === true && !requiresRenderedInspection && !(unresolvedLayout && cycle < AGENT_TURN_PASS_CAP)) {
|
|
9127
9627
|
if (selected?.changed.length) best = selected;
|
|
9128
|
-
remainingMismatch =
|
|
9628
|
+
remainingMismatch = unresolvedLayout;
|
|
9129
9629
|
break;
|
|
9130
9630
|
}
|
|
9131
|
-
if (
|
|
9132
|
-
if (
|
|
9631
|
+
if (unresolvedLayout) remainingMismatch = unresolvedLayout;
|
|
9632
|
+
else if (authorization.followUp && !authorization.patch) break;
|
|
9633
|
+
if (cycle < AGENT_TURN_PASS_CAP && !renderedThisCycle) {
|
|
9133
9634
|
try {
|
|
9134
9635
|
rendered = await ports.renderComposed(working, cycle);
|
|
9636
|
+
renderedState = working;
|
|
9135
9637
|
} catch {
|
|
9136
9638
|
terminalOutcome = failureOutcome(
|
|
9137
9639
|
"service_unavailable",
|
|
@@ -9145,11 +9647,17 @@ async function runAgentTurn(input, ports) {
|
|
|
9145
9647
|
selected = best;
|
|
9146
9648
|
const selectedState = selected?.state ?? before;
|
|
9147
9649
|
await emit({ type: "draft_selected", checkpointId: selected?.id ?? 0, state: selectedState });
|
|
9148
|
-
let outcome =
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9650
|
+
let outcome = withTouchedCapabilities(
|
|
9651
|
+
terminalOutcome ?? applicationOutcome({
|
|
9652
|
+
changed: selected?.changed ?? [],
|
|
9653
|
+
invalid: invalid2,
|
|
9654
|
+
gaps: selected?.gaps ?? [],
|
|
9655
|
+
// Only a measurement of the very state being committed. A collision seen on an earlier draft,
|
|
9656
|
+
// or on a strip the last pass then changed without it being rendered, says nothing about this one.
|
|
9657
|
+
unresolvedLayout: selected && measuredLayout?.state === selected.state ? measuredLayout.collision : null
|
|
9658
|
+
}),
|
|
9659
|
+
Array.from(touchedCapabilities)
|
|
9660
|
+
);
|
|
9153
9661
|
let committed = false;
|
|
9154
9662
|
let commitData;
|
|
9155
9663
|
if (outcome.status === "applied" || outcome.status === "applied_with_remaining") {
|
|
@@ -9166,7 +9674,10 @@ async function runAgentTurn(input, ports) {
|
|
|
9166
9674
|
commitData = commit.data;
|
|
9167
9675
|
await emit({ type: "commit_succeeded", data: commit.data, outcome });
|
|
9168
9676
|
} else {
|
|
9169
|
-
outcome =
|
|
9677
|
+
outcome = withTouchedCapabilities(
|
|
9678
|
+
commit.reason === "stale_version" ? applicationOutcome({ changed: [], conflicted: true }) : failureOutcome("service_unavailable", "The Agent could not save this change. Nothing changed."),
|
|
9679
|
+
Array.from(touchedCapabilities)
|
|
9680
|
+
);
|
|
9170
9681
|
await emit({ type: "draft_rolled_back", outcome });
|
|
9171
9682
|
}
|
|
9172
9683
|
} else {
|
|
@@ -9200,23 +9711,37 @@ async function runAgentTurn(input, ports) {
|
|
|
9200
9711
|
state: committed ? selectedState : before,
|
|
9201
9712
|
expectedVersion: loaded.expectedVersion,
|
|
9202
9713
|
committed,
|
|
9203
|
-
...commitData === void 0 ? {} : { commitData }
|
|
9714
|
+
...commitData === void 0 ? {} : { commitData },
|
|
9715
|
+
demand: demandClassified ? classifiedDemand(unmetActions) : NOT_MEASURED_DEMAND
|
|
9204
9716
|
};
|
|
9205
9717
|
assertOutcome(outcome);
|
|
9206
9718
|
await emit({ type: "turn_completed", result });
|
|
9207
9719
|
void input.requestedCycle;
|
|
9208
9720
|
return result;
|
|
9209
9721
|
}
|
|
9210
|
-
var AGENT_TURN_PASS_CAP, EMPTY_USAGE, NO_CHANGE_SUMMARY, NEEDS_INPUT_SUMMARY, FAILED_PASS_SUMMARY;
|
|
9722
|
+
var AGENT_TURN_PASS_CAP, AgentTurnAuthorizationError, EMPTY_USAGE, NO_CHANGE_SUMMARY, NEEDS_INPUT_SUMMARY, FAILED_PASS_SUMMARY;
|
|
9211
9723
|
var init_agentTurnCoordinator = __esm({
|
|
9212
9724
|
"../mockup-engine/appstore/agentTurnCoordinator.ts"() {
|
|
9213
9725
|
"use strict";
|
|
9214
9726
|
init_agentOutcome();
|
|
9727
|
+
init_refineIntent();
|
|
9215
9728
|
init_refinement();
|
|
9729
|
+
init_agentUnmetActions();
|
|
9216
9730
|
init_recordTransforms();
|
|
9217
9731
|
init_appStoreLocales();
|
|
9218
9732
|
init_agentAssetCustody();
|
|
9733
|
+
init_agentTurnRepetition();
|
|
9734
|
+
init_templatePoseGuard();
|
|
9735
|
+
init_layoutTemplates();
|
|
9219
9736
|
AGENT_TURN_PASS_CAP = 3;
|
|
9737
|
+
AgentTurnAuthorizationError = class extends Error {
|
|
9738
|
+
constructor(outcome) {
|
|
9739
|
+
super(outcome.headline);
|
|
9740
|
+
this.outcome = outcome;
|
|
9741
|
+
this.name = "AgentTurnAuthorizationError";
|
|
9742
|
+
}
|
|
9743
|
+
outcome;
|
|
9744
|
+
};
|
|
9220
9745
|
EMPTY_USAGE = {
|
|
9221
9746
|
inputTokens: 0,
|
|
9222
9747
|
cacheReadTokens: 0,
|
|
@@ -9263,7 +9788,9 @@ var init_agentTurnConformance = __esm({
|
|
|
9263
9788
|
panelId: "p1",
|
|
9264
9789
|
frameNodeId: "frame-1",
|
|
9265
9790
|
frameName: "home.png",
|
|
9266
|
-
|
|
9791
|
+
// Real pixels, because a surface derives its vision list from THIS state: a shot with no
|
|
9792
|
+
// bytes is a blank frame, which every door drops from the turn (#829).
|
|
9793
|
+
bytes: new Uint8Array([1, 2, 3]),
|
|
9267
9794
|
look: { ...DEFAULT_LOOK },
|
|
9268
9795
|
thumb: null,
|
|
9269
9796
|
thumbStale: false
|
|
@@ -9331,6 +9858,8 @@ var init_agentTurnConformance = __esm({
|
|
|
9331
9858
|
outcome: {
|
|
9332
9859
|
status: "applied",
|
|
9333
9860
|
headline: "Restyled 1 device",
|
|
9861
|
+
// #840 — every capability the drafts touched, derived from the patch, travels on the outcome.
|
|
9862
|
+
capabilityIds: ["device.look"],
|
|
9334
9863
|
changed: ["Restyled 1 device"]
|
|
9335
9864
|
},
|
|
9336
9865
|
stateDiff: {
|
|
@@ -9399,6 +9928,7 @@ var init_agentTurnConformance = __esm({
|
|
|
9399
9928
|
outcome: {
|
|
9400
9929
|
status: "applied",
|
|
9401
9930
|
headline: "Replaced the screenshot on 1 device",
|
|
9931
|
+
capabilityIds: ["device.replace-pixels"],
|
|
9402
9932
|
changed: ["Replaced the screenshot on 1 device"]
|
|
9403
9933
|
},
|
|
9404
9934
|
stateDiff: {
|
|
@@ -9461,6 +9991,7 @@ var init_appstore = __esm({
|
|
|
9461
9991
|
init_agentAssetCustody();
|
|
9462
9992
|
init_agentCapabilities();
|
|
9463
9993
|
init_agentOutcome();
|
|
9994
|
+
init_agentUnmetActions();
|
|
9464
9995
|
init_agentTurnCoordinator();
|
|
9465
9996
|
init_agentTurnConformance();
|
|
9466
9997
|
init_refinement();
|
|
@@ -10990,6 +11521,84 @@ var init_renderCredits = __esm({
|
|
|
10990
11521
|
}
|
|
10991
11522
|
});
|
|
10992
11523
|
|
|
11524
|
+
// src/render/browserInstall.ts
|
|
11525
|
+
import { spawn as spawnProcess } from "node:child_process";
|
|
11526
|
+
import { createRequire } from "node:module";
|
|
11527
|
+
import { dirname, join as join2 } from "node:path";
|
|
11528
|
+
function isMissingBrowserError(message) {
|
|
11529
|
+
return /Executable doesn't exist at /.test(message);
|
|
11530
|
+
}
|
|
11531
|
+
function classifyInstallFailure(output2) {
|
|
11532
|
+
const lines = output2.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !/^at /.test(line));
|
|
11533
|
+
const detail = (lines[lines.length - 1] ?? "the installer exited without output").slice(0, 300);
|
|
11534
|
+
return { offline: NETWORK_FAILURE.test(output2), detail };
|
|
11535
|
+
}
|
|
11536
|
+
function rendererSetupRefusal(err) {
|
|
11537
|
+
if (!(err instanceof RendererSetupError)) return null;
|
|
11538
|
+
if (err.offline) return errorResult(RENDERER_DOWNLOAD_OFFLINE_MESSAGE, "upstream_unavailable");
|
|
11539
|
+
return errorResult(`The one-time download of the renderer (Chromium) failed: ${err.detail}`, "internal_error");
|
|
11540
|
+
}
|
|
11541
|
+
async function refuseOnRendererSetupFailure(run) {
|
|
11542
|
+
try {
|
|
11543
|
+
return await run();
|
|
11544
|
+
} catch (err) {
|
|
11545
|
+
const refusal = rendererSetupRefusal(err);
|
|
11546
|
+
if (refusal) return refusal;
|
|
11547
|
+
throw err;
|
|
11548
|
+
}
|
|
11549
|
+
}
|
|
11550
|
+
function playwrightCliPath() {
|
|
11551
|
+
const require2 = createRequire(import.meta.url);
|
|
11552
|
+
return join2(dirname(require2.resolve("playwright/package.json")), "cli.js");
|
|
11553
|
+
}
|
|
11554
|
+
function installChromium(options = {}) {
|
|
11555
|
+
const spawn2 = options.spawn ?? spawnProcess;
|
|
11556
|
+
const log = options.log ?? ((chunk) => void process.stderr.write(chunk));
|
|
11557
|
+
const execPath = options.execPath ?? process.execPath;
|
|
11558
|
+
const cliPath = options.cliPath ?? playwrightCliPath();
|
|
11559
|
+
return new Promise((resolve14, reject) => {
|
|
11560
|
+
let output2 = "";
|
|
11561
|
+
const collect = (data) => {
|
|
11562
|
+
const text3 = String(data);
|
|
11563
|
+
log(text3);
|
|
11564
|
+
output2 = (output2 + text3).slice(-OUTPUT_TAIL_BYTES);
|
|
11565
|
+
};
|
|
11566
|
+
const child = spawn2(execPath, [cliPath, "install", "chromium"], {
|
|
11567
|
+
env: options.env ?? process.env,
|
|
11568
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11569
|
+
windowsHide: true
|
|
11570
|
+
});
|
|
11571
|
+
child.stdout?.on("data", collect);
|
|
11572
|
+
child.stderr?.on("data", collect);
|
|
11573
|
+
child.on("error", (err) => reject(new RendererSetupError(classifyInstallFailure(err.message))));
|
|
11574
|
+
child.on("close", (code) => {
|
|
11575
|
+
if (code === 0) resolve14();
|
|
11576
|
+
else reject(new RendererSetupError(classifyInstallFailure(output2)));
|
|
11577
|
+
});
|
|
11578
|
+
});
|
|
11579
|
+
}
|
|
11580
|
+
var RENDERER_DOWNLOADED_NOTE, RENDERER_DOWNLOAD_OFFLINE_MESSAGE, NETWORK_FAILURE, RendererSetupError, OUTPUT_TAIL_BYTES;
|
|
11581
|
+
var init_browserInstall = __esm({
|
|
11582
|
+
"src/render/browserInstall.ts"() {
|
|
11583
|
+
"use strict";
|
|
11584
|
+
init_toolResults();
|
|
11585
|
+
RENDERER_DOWNLOADED_NOTE = "This was the first render on this machine, so ShotOps downloaded its renderer (Chromium) once before rendering; later renders skip that step.";
|
|
11586
|
+
RENDERER_DOWNLOAD_OFFLINE_MESSAGE = "The first render on this machine needs a one-time download of the renderer (Chromium), and the download server could not be reached, so connect to the internet and try again.";
|
|
11587
|
+
NETWORK_FAILURE = /\b(?:ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|EHOSTUNREACH|ENETDOWN)\b|getaddrinfo|socket hang up|server closed connection|timed out after \d+ms/;
|
|
11588
|
+
RendererSetupError = class extends Error {
|
|
11589
|
+
offline;
|
|
11590
|
+
detail;
|
|
11591
|
+
constructor(failure2) {
|
|
11592
|
+
super(failure2.offline ? RENDERER_DOWNLOAD_OFFLINE_MESSAGE : `renderer download failed: ${failure2.detail}`);
|
|
11593
|
+
this.name = "RendererSetupError";
|
|
11594
|
+
this.offline = failure2.offline;
|
|
11595
|
+
this.detail = failure2.detail;
|
|
11596
|
+
}
|
|
11597
|
+
};
|
|
11598
|
+
OUTPUT_TAIL_BYTES = 16e3;
|
|
11599
|
+
}
|
|
11600
|
+
});
|
|
11601
|
+
|
|
10993
11602
|
// ../api/_lib/billingStore.ts
|
|
10994
11603
|
var SUBSCRIPTION_COLUMNS;
|
|
10995
11604
|
var init_billingStore = __esm({
|
|
@@ -11136,6 +11745,33 @@ var init_production = __esm({
|
|
|
11136
11745
|
}
|
|
11137
11746
|
});
|
|
11138
11747
|
|
|
11748
|
+
// ../product/accountRights.ts
|
|
11749
|
+
var ACCOUNT_TRAFFIC_CLASSES, RECORDED_TRAFFIC_CLASSES;
|
|
11750
|
+
var init_accountRights = __esm({
|
|
11751
|
+
"../product/accountRights.ts"() {
|
|
11752
|
+
"use strict";
|
|
11753
|
+
ACCOUNT_TRAFFIC_CLASSES = ["customer", "owner", "benchmark"];
|
|
11754
|
+
RECORDED_TRAFFIC_CLASSES = [...ACCOUNT_TRAFFIC_CLASSES, "unclassified"];
|
|
11755
|
+
}
|
|
11756
|
+
});
|
|
11757
|
+
|
|
11758
|
+
// ../api/_lib/authorization/accountAccess.ts
|
|
11759
|
+
var init_accountAccess = __esm({
|
|
11760
|
+
"../api/_lib/authorization/accountAccess.ts"() {
|
|
11761
|
+
"use strict";
|
|
11762
|
+
init_accountRights();
|
|
11763
|
+
init_supabaseRest();
|
|
11764
|
+
}
|
|
11765
|
+
});
|
|
11766
|
+
|
|
11767
|
+
// ../api/_lib/authorization/operatorAudit.ts
|
|
11768
|
+
var init_operatorAudit = __esm({
|
|
11769
|
+
"../api/_lib/authorization/operatorAudit.ts"() {
|
|
11770
|
+
"use strict";
|
|
11771
|
+
init_supabaseRest();
|
|
11772
|
+
}
|
|
11773
|
+
});
|
|
11774
|
+
|
|
11139
11775
|
// ../api/_lib/log.ts
|
|
11140
11776
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11141
11777
|
function currentRequestId() {
|
|
@@ -11168,6 +11804,7 @@ var JWKS_TTL_MS;
|
|
|
11168
11804
|
var init_supabaseAuth = __esm({
|
|
11169
11805
|
"../api/_lib/supabaseAuth.ts"() {
|
|
11170
11806
|
"use strict";
|
|
11807
|
+
init_accountAccess();
|
|
11171
11808
|
JWKS_TTL_MS = 10 * 60 * 1e3;
|
|
11172
11809
|
}
|
|
11173
11810
|
});
|
|
@@ -11214,6 +11851,8 @@ var init_polarWebhook = __esm({
|
|
|
11214
11851
|
var init_billingReconcile = __esm({
|
|
11215
11852
|
"../api/_lib/billingReconcile.ts"() {
|
|
11216
11853
|
"use strict";
|
|
11854
|
+
init_accountAccess();
|
|
11855
|
+
init_operatorAudit();
|
|
11217
11856
|
init_log();
|
|
11218
11857
|
init_supabaseAuth();
|
|
11219
11858
|
init_entitlement();
|
|
@@ -11224,10 +11863,62 @@ var init_billingReconcile = __esm({
|
|
|
11224
11863
|
}
|
|
11225
11864
|
});
|
|
11226
11865
|
|
|
11866
|
+
// ../api/_lib/providerUsage.ts
|
|
11867
|
+
var init_providerUsage = __esm({
|
|
11868
|
+
"../api/_lib/providerUsage.ts"() {
|
|
11869
|
+
"use strict";
|
|
11870
|
+
}
|
|
11871
|
+
});
|
|
11872
|
+
|
|
11873
|
+
// ../api/_lib/providerCost.ts
|
|
11874
|
+
var init_providerCost = __esm({
|
|
11875
|
+
"../api/_lib/providerCost.ts"() {
|
|
11876
|
+
"use strict";
|
|
11877
|
+
}
|
|
11878
|
+
});
|
|
11879
|
+
|
|
11880
|
+
// ../api/_lib/providerSpendStore.ts
|
|
11881
|
+
import { z as z8 } from "zod";
|
|
11882
|
+
var nullableCount, decimalMoney, evidenceSchema;
|
|
11883
|
+
var init_providerSpendStore = __esm({
|
|
11884
|
+
"../api/_lib/providerSpendStore.ts"() {
|
|
11885
|
+
"use strict";
|
|
11886
|
+
init_providerCost();
|
|
11887
|
+
init_providerUsage();
|
|
11888
|
+
init_supabaseRest();
|
|
11889
|
+
nullableCount = z8.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).nullable();
|
|
11890
|
+
decimalMoney = z8.string().regex(/^\d{1,19}$/).refine((n) => BigInt(n) <= 9223372036854775807n);
|
|
11891
|
+
evidenceSchema = z8.object({
|
|
11892
|
+
state: z8.enum(["estimated", "partial", "unknown", "not_incurred"]),
|
|
11893
|
+
amountNanoUsd: decimalMoney.nullable(),
|
|
11894
|
+
missing: z8.array(z8.enum(["rate", "usage", "input", "input_breakdown", "inconsistent_input", "cache_read", "cache_write", "cache_write_ttl", "output", "multiple_provider_attempts", "provider_attempts_unknown", "gateway_bill", "amount_overflow"])),
|
|
11895
|
+
usage: z8.object({ inputTokens: nullableCount, cacheReadTokens: nullableCount, cacheWriteTokens: nullableCount, outputTokens: nullableCount, cacheWriteTtl: z8.enum(["5m", "1h"]).nullable() }).strict().nullable(),
|
|
11896
|
+
rate: z8.object({
|
|
11897
|
+
version: z8.string().regex(/^[a-zA-Z0-9._-]{1,100}$/),
|
|
11898
|
+
effectiveAt: z8.string().datetime(),
|
|
11899
|
+
currency: z8.literal("USD"),
|
|
11900
|
+
model: z8.literal("anthropic/claude-sonnet-4.6"),
|
|
11901
|
+
basis: z8.literal("gateway-list-standard"),
|
|
11902
|
+
region: z8.enum(["global", "us", "eu"]),
|
|
11903
|
+
nanoUsdPerMillion: z8.object({ input: decimalMoney, output: decimalMoney, cacheRead: decimalMoney, cacheWrite5m: decimalMoney, cacheWrite1h: decimalMoney }).strict(),
|
|
11904
|
+
sources: z8.array(z8.enum(["https://platform.claude.com/docs/en/about-claude/pricing", "https://ai-gateway.vercel.sh/v1/models"]))
|
|
11905
|
+
}).strict().nullable(),
|
|
11906
|
+
gateway: z8.object({ generationId: z8.string().regex(/^gen_[a-zA-Z0-9_-]{1,100}$/).nullable(), costUsd: z8.string().regex(/^\d{1,12}(\.\d{1,18})?$/).nullable(), marketCostUsd: z8.string().regex(/^\d{1,12}(\.\d{1,18})?$/).nullable(), attempts: nullableCount, route: z8.string().regex(/^[a-zA-Z0-9_-]{1,40}$/).nullable() }).strict().nullable(),
|
|
11907
|
+
noAttempt: z8.literal(true).optional()
|
|
11908
|
+
}).strict().superRefine((e, context) => {
|
|
11909
|
+
if ((e.state === "estimated" || e.state === "partial") && e.amountNanoUsd === null || e.state === "unknown" && e.amountNanoUsd !== null || e.state === "not_incurred" && (e.amountNanoUsd !== "0" || e.noAttempt !== true)) {
|
|
11910
|
+
context.addIssue({ code: "custom", message: "inconsistent spend state" });
|
|
11911
|
+
}
|
|
11912
|
+
});
|
|
11913
|
+
}
|
|
11914
|
+
});
|
|
11915
|
+
|
|
11227
11916
|
// ../api/_lib/aiGenerationStore.ts
|
|
11228
11917
|
var init_aiGenerationStore = __esm({
|
|
11229
11918
|
"../api/_lib/aiGenerationStore.ts"() {
|
|
11230
11919
|
"use strict";
|
|
11920
|
+
init_providerUsage();
|
|
11921
|
+
init_providerSpendStore();
|
|
11231
11922
|
init_supabaseRest();
|
|
11232
11923
|
}
|
|
11233
11924
|
});
|
|
@@ -11716,7 +12407,7 @@ var init_package = __esm({
|
|
|
11716
12407
|
"package.json"() {
|
|
11717
12408
|
package_default = {
|
|
11718
12409
|
name: "shotops-mcp",
|
|
11719
|
-
version: "0.9.
|
|
12410
|
+
version: "0.9.7",
|
|
11720
12411
|
private: false,
|
|
11721
12412
|
type: "module",
|
|
11722
12413
|
description: "The bundle-emitting MCP server over the @engine/@sync spine. Exposes ShotOps tools to ChatGPT, Claude Code, Cursor, and CI over Streamable HTTP with OAuth or personal API tokens. Hosted and MCP tool paths never accept, store, or forward a store-signing credential; the explicit local release CLI validates the user's existing key directly with Apple and records only its path. Renders via headless Playwright Chromium, the engine's native non-browser habitat (same path as mockup-mcp). Also ships as a free LOCAL stdio server (`npx shotops-mcp`) \u2014 same render, on your own machine, no account needed.",
|
|
@@ -11753,7 +12444,8 @@ var init_package = __esm({
|
|
|
11753
12444
|
prepack: "node ../scripts/third-party-notices.mjs --check",
|
|
11754
12445
|
prepublishOnly: "npm run build",
|
|
11755
12446
|
typecheck: "tsc --noEmit",
|
|
11756
|
-
"build:widget": "node build.widget.mjs"
|
|
12447
|
+
"build:widget": "node build.widget.mjs",
|
|
12448
|
+
"build:mcpb": "tsx build.mcpb.ts"
|
|
11757
12449
|
},
|
|
11758
12450
|
dependencies: {
|
|
11759
12451
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -11767,7 +12459,9 @@ var init_package = __esm({
|
|
|
11767
12459
|
zod: "^3.25.76"
|
|
11768
12460
|
},
|
|
11769
12461
|
devDependencies: {
|
|
12462
|
+
"@anthropic-ai/mcpb": "2.1.2",
|
|
11770
12463
|
"@types/express": "^5.0.6",
|
|
12464
|
+
"@vercel/blob": "2.8.0",
|
|
11771
12465
|
ai: "^7.0.48",
|
|
11772
12466
|
esbuild: "^0.28.1",
|
|
11773
12467
|
nitro: "3.0.260610-beta",
|
|
@@ -11917,6 +12611,15 @@ function projectCopyLocales(projectFile, locales, family = BASE_SCREEN_FAMILY) {
|
|
|
11917
12611
|
}
|
|
11918
12612
|
return out;
|
|
11919
12613
|
}
|
|
12614
|
+
function projectCopyGaps(projectFile, locales, family = BASE_SCREEN_FAMILY) {
|
|
12615
|
+
const file = projectFile;
|
|
12616
|
+
const baseLocale = typeof file?.baseLocale === "string" && file.baseLocale ? file.baseLocale : "en-US";
|
|
12617
|
+
const captionText = normalizeCaptionText(file?.captionText, file?.captionStyles, baseLocale);
|
|
12618
|
+
const outputCaptions = normalizeOutputCaptionText(file?.outputCaptions);
|
|
12619
|
+
const base = resolveOutputCaptionText(captionText, outputCaptions, family, baseLocale, baseLocale);
|
|
12620
|
+
if (!Object.values(base).some((layers) => layers.some((layer) => layer.text.trim().length > 0))) return [];
|
|
12621
|
+
return localesWithoutCaptions(captionText, [...locales], baseLocale).filter((locale2) => !Object.values(outputCaptions[family]?.[locale2] ?? {}).some((words) => words.some((word) => word.trim().length > 0))).map((locale2) => ({ code: "copy_locale_missing", subject: locale2 }));
|
|
12622
|
+
}
|
|
11920
12623
|
function directCopyLocales(style2, locales) {
|
|
11921
12624
|
const captions = style2?.captions;
|
|
11922
12625
|
const words = [];
|
|
@@ -12132,7 +12835,7 @@ var init_productionReadiness2 = __esm({
|
|
|
12132
12835
|
// src/tools/toolsRender.ts
|
|
12133
12836
|
function handleRenderStrip(deps, args, signal) {
|
|
12134
12837
|
const scope = openChargeScope(deps);
|
|
12135
|
-
return runCharged(scope, () => renderStrip(deps, args, scope, signal));
|
|
12838
|
+
return refuseOnRendererSetupFailure(() => runCharged(scope, () => renderStrip(deps, args, scope, signal)));
|
|
12136
12839
|
}
|
|
12137
12840
|
async function renderStrip(deps, args, scope, signal) {
|
|
12138
12841
|
const operationId = newOperationId("render_strip");
|
|
@@ -12250,7 +12953,7 @@ async function renderStrip(deps, args, scope, signal) {
|
|
|
12250
12953
|
}
|
|
12251
12954
|
const totalBytes = result.panels.reduce((sum, p) => sum + estimateDecodedBytes(p), 0);
|
|
12252
12955
|
const { mode: outputMode, note: outputNote } = resolveOutputMode(args.output, totalBytes, deps.forceOutput);
|
|
12253
|
-
const noteWithOutput = [note, outputNote, result.layoutNote, resolutionGapNote(snapshot)].filter(Boolean).join(" ") || void 0;
|
|
12956
|
+
const noteWithOutput = [note, outputNote, result.layoutNote, result.setupNote, resolutionGapNote(snapshot)].filter(Boolean).join(" ") || void 0;
|
|
12254
12957
|
const measuredReady = requireReadiness({
|
|
12255
12958
|
snapshot,
|
|
12256
12959
|
surface: "render",
|
|
@@ -12747,7 +13450,7 @@ async function handleRenderProject(deps, args, signal) {
|
|
|
12747
13450
|
const scope = openChargeScope(deps);
|
|
12748
13451
|
const attempt = {};
|
|
12749
13452
|
try {
|
|
12750
|
-
return await runCharged(scope, () => renderProject(deps, args, scope, attempt, signal));
|
|
13453
|
+
return await refuseOnRendererSetupFailure(() => runCharged(scope, () => renderProject(deps, args, scope, attempt, signal)));
|
|
12751
13454
|
} finally {
|
|
12752
13455
|
finishProductionRender(attempt.access);
|
|
12753
13456
|
}
|
|
@@ -12975,6 +13678,7 @@ async function renderProject(deps, args, scope, attempt, signal) {
|
|
|
12975
13678
|
if (outputNote) notes.push(outputNote);
|
|
12976
13679
|
if (droppedNote) notes.push(droppedNote);
|
|
12977
13680
|
if (result.layoutNote) notes.push(result.layoutNote);
|
|
13681
|
+
if (result.setupNote) notes.push(result.setupNote);
|
|
12978
13682
|
if (multi) {
|
|
12979
13683
|
notes.push(
|
|
12980
13684
|
`Rendered ${outputBlocks.length} output devices (${outputBlocks.map((o) => o.label).join(", ")}) \u2014 see \`outputs\`. The top-level panels are the active device (${active?.label}).`
|
|
@@ -13070,6 +13774,7 @@ var init_toolsRender = __esm({
|
|
|
13070
13774
|
init_studioUrl();
|
|
13071
13775
|
init_renderCredits();
|
|
13072
13776
|
init_panelDelivery();
|
|
13777
|
+
init_browserInstall();
|
|
13073
13778
|
init_toolContract();
|
|
13074
13779
|
init_schemas();
|
|
13075
13780
|
init_productionOperationStart();
|
|
@@ -13388,12 +14093,12 @@ var init_recordFromStrip = __esm({
|
|
|
13388
14093
|
// src/render/staticServer.ts
|
|
13389
14094
|
import { createServer } from "node:http";
|
|
13390
14095
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
13391
|
-
import { extname, join as
|
|
14096
|
+
import { extname, join as join4, relative, isAbsolute } from "node:path";
|
|
13392
14097
|
async function startStaticServer(rootDir) {
|
|
13393
14098
|
const server = createServer((req, res) => {
|
|
13394
14099
|
const reqPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
|
13395
14100
|
const rel = reqPath === "/" ? "index.html" : reqPath.replace(/^\/+/, "");
|
|
13396
|
-
const filePath =
|
|
14101
|
+
const filePath = join4(rootDir, rel);
|
|
13397
14102
|
const within = relative(rootDir, filePath);
|
|
13398
14103
|
if (within.startsWith("..") || isAbsolute(within)) {
|
|
13399
14104
|
res.statusCode = 403;
|
|
@@ -13457,6 +14162,7 @@ var init_renderer = __esm({
|
|
|
13457
14162
|
init_recordFromStrip();
|
|
13458
14163
|
init_log();
|
|
13459
14164
|
init_staticServer();
|
|
14165
|
+
init_browserInstall();
|
|
13460
14166
|
StripRenderer = class {
|
|
13461
14167
|
serveMode;
|
|
13462
14168
|
vite = null;
|
|
@@ -13472,9 +14178,14 @@ var init_renderer = __esm({
|
|
|
13472
14178
|
// The harness server's URL, cached so a Chromium re-boot (#336) re-launches the browser
|
|
13473
14179
|
// against the SAME server instead of starting a second Vite/static server per boot.
|
|
13474
14180
|
harnessUrl = null;
|
|
14181
|
+
installBrowser;
|
|
14182
|
+
// #889 — the note owed to the first SUCCESSFUL render after a browser install. Kept until one
|
|
14183
|
+
// succeeds, so a render that fails for an unrelated reason does not swallow it.
|
|
14184
|
+
pendingSetupNote;
|
|
13475
14185
|
// Default 'vite' so `new StripRenderer()` (server.ts) keeps the hosted path byte-for-byte.
|
|
13476
14186
|
constructor(options = {}) {
|
|
13477
14187
|
this.serveMode = options.serve ?? "vite";
|
|
14188
|
+
this.installBrowser = options.installBrowser;
|
|
13478
14189
|
}
|
|
13479
14190
|
boot() {
|
|
13480
14191
|
if (!this.booting) {
|
|
@@ -13487,8 +14198,7 @@ var init_renderer = __esm({
|
|
|
13487
14198
|
}
|
|
13488
14199
|
async doBoot() {
|
|
13489
14200
|
if (!this.harnessUrl) this.harnessUrl = await this.startHarnessServer();
|
|
13490
|
-
|
|
13491
|
-
this.browser = await chromium.launch({ headless: true });
|
|
14201
|
+
this.browser = await this.launchBrowser();
|
|
13492
14202
|
const page2 = await this.browser.newPage();
|
|
13493
14203
|
page2.on("console", (m) => {
|
|
13494
14204
|
if (/CONTEXT_LOST|Context Lost/i.test(m.text())) this.sawContextLost = true;
|
|
@@ -13497,6 +14207,24 @@ var init_renderer = __esm({
|
|
|
13497
14207
|
await page2.waitForFunction(() => window.__RENDER_READY === true, { timeout: 9e4 });
|
|
13498
14208
|
this.page = page2;
|
|
13499
14209
|
}
|
|
14210
|
+
// #889 — the install decision. Only a launch that failed because the browser binary is missing,
|
|
14211
|
+
// and only on a renderer that was given an installer, downloads anything; the install's own
|
|
14212
|
+
// failure (offline, most often) propagates as a RendererSetupError, which the render tools'
|
|
14213
|
+
// `refuseOnRendererSetupFailure` turns into a coded refusal. `boot()` is memoized, so concurrent
|
|
14214
|
+
// first renders share one download.
|
|
14215
|
+
async launchBrowser() {
|
|
14216
|
+
const { chromium } = await import("playwright");
|
|
14217
|
+
try {
|
|
14218
|
+
return await chromium.launch({ headless: true });
|
|
14219
|
+
} catch (err) {
|
|
14220
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14221
|
+
if (!this.installBrowser || !isMissingBrowserError(message)) throw err;
|
|
14222
|
+
console.error("[shotops-mcp-local] Chromium is not installed \u2014 downloading the renderer once (playwright install chromium)\u2026");
|
|
14223
|
+
await this.installBrowser();
|
|
14224
|
+
this.pendingSetupNote = RENDERER_DOWNLOADED_NOTE;
|
|
14225
|
+
return await chromium.launch({ headless: true });
|
|
14226
|
+
}
|
|
14227
|
+
}
|
|
13500
14228
|
// The one line that forks between hosted and local. Everything downstream (browser launch,
|
|
13501
14229
|
// the __RENDER_READY handshake, window.renderStrip, boot-retry, CONTEXT_LOST) is identical
|
|
13502
14230
|
// — the page is the same harness, only its bytes come from a different server.
|
|
@@ -13573,6 +14301,11 @@ var init_renderer = __esm({
|
|
|
13573
14301
|
panels: result.count ?? void 0
|
|
13574
14302
|
});
|
|
13575
14303
|
}
|
|
14304
|
+
if (result.ok && this.pendingSetupNote) {
|
|
14305
|
+
const setupNote = this.pendingSetupNote;
|
|
14306
|
+
this.pendingSetupNote = void 0;
|
|
14307
|
+
return { ...result, setupNote };
|
|
14308
|
+
}
|
|
13576
14309
|
return result;
|
|
13577
14310
|
} catch (err) {
|
|
13578
14311
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -13686,6 +14419,9 @@ function parseControlPlaneResponse(op, value) {
|
|
|
13686
14419
|
case "read_project_screens":
|
|
13687
14420
|
if (!Array.isArray(body.screens)) throw new Error("control plane response is missing screens");
|
|
13688
14421
|
break;
|
|
14422
|
+
case "read_attachment_screens":
|
|
14423
|
+
if (!Array.isArray(body.screens)) throw new Error("control plane response is missing screens");
|
|
14424
|
+
break;
|
|
13689
14425
|
case "refine_project":
|
|
13690
14426
|
if (typeof body.ok !== "boolean") throw new Error("control plane response is missing refine_project result");
|
|
13691
14427
|
if (body.ok) {
|
|
@@ -13923,17 +14659,28 @@ function connectHostedBridge(token2) {
|
|
|
13923
14659
|
...repairFindings && repairFindings.length > 0 ? { repairFindings } : {}
|
|
13924
14660
|
};
|
|
13925
14661
|
},
|
|
14662
|
+
async readAttachmentScreens(projectId, refs) {
|
|
14663
|
+
const res = await callOp(token2, "read_attachment_screens", { project: projectId, refs: [...refs] });
|
|
14664
|
+
return res.screens;
|
|
14665
|
+
},
|
|
13926
14666
|
async refineProject(input) {
|
|
13927
14667
|
return callOp(token2, "refine_project", input);
|
|
13928
14668
|
},
|
|
13929
|
-
async refinePass(projectId, pass, turnKey) {
|
|
13930
|
-
const
|
|
14669
|
+
async refinePass(projectId, pass, turnKey, benchmark, attachments) {
|
|
14670
|
+
const args = {
|
|
14671
|
+
project: projectId,
|
|
14672
|
+
turnKey,
|
|
14673
|
+
pass,
|
|
14674
|
+
...benchmark === void 0 ? {} : { benchmark },
|
|
14675
|
+
...attachments === void 0 ? {} : { attachments }
|
|
14676
|
+
};
|
|
14677
|
+
const body = JSON.stringify({ op: "refine_pass", ...args });
|
|
13931
14678
|
if (body.length > MAX_PASS_BODY_BYTES) {
|
|
13932
14679
|
throw new Error(
|
|
13933
14680
|
"This project has too many screenshots for one Agent pass. Refine a smaller selection, or run the Agent from ShotOps in the browser."
|
|
13934
14681
|
);
|
|
13935
14682
|
}
|
|
13936
|
-
return callOp(token2, "refine_pass",
|
|
14683
|
+
return callOp(token2, "refine_pass", args);
|
|
13937
14684
|
},
|
|
13938
14685
|
async settleRefineTurn(projectId, settlement) {
|
|
13939
14686
|
const res = await callOp(token2, "refine_finalize", {
|
|
@@ -14004,6 +14751,23 @@ var init_hostedBridge = __esm({
|
|
|
14004
14751
|
}
|
|
14005
14752
|
});
|
|
14006
14753
|
|
|
14754
|
+
// ../mockup-engine/layoutFindings.ts
|
|
14755
|
+
function describeUnresolvedLayout(findings) {
|
|
14756
|
+
if (!findings || findings.length === 0) return null;
|
|
14757
|
+
const frames = Array.from(new Set(findings.flatMap((finding) => finding.frames))).sort((left, right) => left - right).map((index) => `Frame ${index + 1}`);
|
|
14758
|
+
const where = frames.length === 0 ? "" : frames.length === 1 ? ` in ${frames[0]}` : ` in ${frames.slice(0, -1).join(", ")} and ${frames[frames.length - 1]}`;
|
|
14759
|
+
if (findings.length === 1) {
|
|
14760
|
+
const what = findings[0].kind === "caption-device" ? "A caption still overlaps a device" : findings[0].kind === "caption-caption" ? "Two captions still overlap" : "Two devices still collide";
|
|
14761
|
+
return `${what}${where}.`;
|
|
14762
|
+
}
|
|
14763
|
+
return `${findings.length} layout overlaps still remain${where}.`;
|
|
14764
|
+
}
|
|
14765
|
+
var init_layoutFindings = __esm({
|
|
14766
|
+
"../mockup-engine/layoutFindings.ts"() {
|
|
14767
|
+
"use strict";
|
|
14768
|
+
}
|
|
14769
|
+
});
|
|
14770
|
+
|
|
14007
14771
|
// ../api/_lib/invalidRequest.ts
|
|
14008
14772
|
function safeFailureDiagnostic(error) {
|
|
14009
14773
|
const errorTypes = [];
|
|
@@ -14038,57 +14802,57 @@ var init_invalidRequest = __esm({
|
|
|
14038
14802
|
});
|
|
14039
14803
|
|
|
14040
14804
|
// ../api/_lib/aiShotProjection.ts
|
|
14041
|
-
import { z as
|
|
14805
|
+
import { z as z9 } from "zod";
|
|
14042
14806
|
var deviceHexSchema, API_AI_SHOT_PROJECTION, API_AI_SHOT_INCLUDED_ENTRIES, API_AI_SHOT_PATCH_SHAPE, apiAiShotPatchSchema, API_AI_SHOT_PROMPT_INVENTORY;
|
|
14043
14807
|
var init_aiShotProjection = __esm({
|
|
14044
14808
|
"../api/_lib/aiShotProjection.ts"() {
|
|
14045
14809
|
"use strict";
|
|
14046
14810
|
init_refineIntent();
|
|
14047
|
-
deviceHexSchema =
|
|
14811
|
+
deviceHexSchema = z9.string().trim().regex(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
|
|
14048
14812
|
API_AI_SHOT_PROJECTION = {
|
|
14049
14813
|
orbit: {
|
|
14050
14814
|
exposure: "included",
|
|
14051
|
-
schema:
|
|
14815
|
+
schema: z9.number().min(AI_SHOT_RANGES.orbit.min).max(AI_SHOT_RANGES.orbit.max).optional(),
|
|
14052
14816
|
prompt: `orbit: camera azimuth in degrees, ${AI_SHOT_RANGES.orbit.min} to ${AI_SHOT_RANGES.orbit.max}. 0 is straight on, negative swings the device left, positive right. Subtle is \xB15\u201320.`
|
|
14053
14817
|
},
|
|
14054
14818
|
roll: {
|
|
14055
14819
|
exposure: "included",
|
|
14056
|
-
schema:
|
|
14820
|
+
schema: z9.number().min(AI_SHOT_RANGES.roll.min).max(AI_SHOT_RANGES.roll.max).optional(),
|
|
14057
14821
|
prompt: `roll: clock-hand tilt, ${AI_SHOT_RANGES.roll.min} to ${AI_SHOT_RANGES.roll.max} degrees. 0 is upright. Subtle is \xB12\u20138.`
|
|
14058
14822
|
},
|
|
14059
14823
|
angle: {
|
|
14060
14824
|
exposure: "included",
|
|
14061
|
-
schema:
|
|
14825
|
+
schema: z9.enum(["front", "left", "right"]).optional(),
|
|
14062
14826
|
prompt: "angle: front | left | right \u2014 a camera PRESET. Setting it discards any orbit value on that frame, so send orbit OR angle, never both."
|
|
14063
14827
|
},
|
|
14064
14828
|
phoneHeight: {
|
|
14065
14829
|
exposure: "included",
|
|
14066
|
-
schema:
|
|
14830
|
+
schema: z9.number().min(AI_SHOT_RANGES.phoneHeight.min).max(AI_SHOT_RANGES.phoneHeight.max).optional(),
|
|
14067
14831
|
prompt: `phoneHeight: ${AI_SHOT_RANGES.phoneHeight.min}\u2013${AI_SHOT_RANGES.phoneHeight.max} (% of panel height; over 100 bleeds off an edge)`
|
|
14068
14832
|
},
|
|
14069
14833
|
hOffset: {
|
|
14070
14834
|
exposure: "included",
|
|
14071
|
-
schema:
|
|
14835
|
+
schema: z9.number().min(AI_SHOT_RANGES.hOffset.min).max(AI_SHOT_RANGES.hOffset.max).optional(),
|
|
14072
14836
|
prompt: `hOffset: ${AI_SHOT_RANGES.hOffset.min}\u2013${AI_SHOT_RANGES.hOffset.max} horizontal position, 0 is centred`
|
|
14073
14837
|
},
|
|
14074
14838
|
vOffset: {
|
|
14075
14839
|
exposure: "included",
|
|
14076
|
-
schema:
|
|
14840
|
+
schema: z9.number().min(AI_SHOT_RANGES.vOffset.min).max(AI_SHOT_RANGES.vOffset.max).optional(),
|
|
14077
14841
|
prompt: `vOffset: ${AI_SHOT_RANGES.vOffset.min}\u2013${AI_SHOT_RANGES.vOffset.max} vertical position, 0 is centred`
|
|
14078
14842
|
},
|
|
14079
14843
|
material: {
|
|
14080
14844
|
exposure: "included",
|
|
14081
|
-
schema:
|
|
14845
|
+
schema: z9.enum(["real", "clay"]).optional(),
|
|
14082
14846
|
prompt: `material: real | clay. colorway (real only): ${AI_COLORWAY_IDS.join(" | ")}. clayTone (clay only): ${AI_CLAY_TONES.join(" | ")}.`
|
|
14083
14847
|
},
|
|
14084
14848
|
colorway: {
|
|
14085
14849
|
exposure: "included",
|
|
14086
|
-
schema:
|
|
14850
|
+
schema: z9.enum(AI_COLORWAY_IDS).optional(),
|
|
14087
14851
|
prompt: `material: real | clay. colorway (real only): ${AI_COLORWAY_IDS.join(" | ")}. clayTone (clay only): ${AI_CLAY_TONES.join(" | ")}.`
|
|
14088
14852
|
},
|
|
14089
14853
|
clayTone: {
|
|
14090
14854
|
exposure: "included",
|
|
14091
|
-
schema:
|
|
14855
|
+
schema: z9.enum(AI_CLAY_TONES).optional(),
|
|
14092
14856
|
prompt: `material: real | clay. colorway (real only): ${AI_COLORWAY_IDS.join(" | ")}. clayTone (clay only): ${AI_CLAY_TONES.join(" | ")}.`
|
|
14093
14857
|
},
|
|
14094
14858
|
customColor: {
|
|
@@ -14103,37 +14867,37 @@ var init_aiShotProjection = __esm({
|
|
|
14103
14867
|
},
|
|
14104
14868
|
finish: {
|
|
14105
14869
|
exposure: "included",
|
|
14106
|
-
schema:
|
|
14870
|
+
schema: z9.number().min(AI_SHOT_RANGES.finish.min).max(AI_SHOT_RANGES.finish.max).optional(),
|
|
14107
14871
|
prompt: `finish: ${AI_SHOT_RANGES.finish.min}\u2013${AI_SHOT_RANGES.finish.max} (0 matte, 1 glossy). clearcoat: ${AI_SHOT_RANGES.clearcoat.min}\u2013${AI_SHOT_RANGES.clearcoat.max} lacquer sheen on top.`
|
|
14108
14872
|
},
|
|
14109
14873
|
clearcoat: {
|
|
14110
14874
|
exposure: "included",
|
|
14111
|
-
schema:
|
|
14875
|
+
schema: z9.number().min(AI_SHOT_RANGES.clearcoat.min).max(AI_SHOT_RANGES.clearcoat.max).optional(),
|
|
14112
14876
|
prompt: `finish: ${AI_SHOT_RANGES.finish.min}\u2013${AI_SHOT_RANGES.finish.max} (0 matte, 1 glossy). clearcoat: ${AI_SHOT_RANGES.clearcoat.min}\u2013${AI_SHOT_RANGES.clearcoat.max} lacquer sheen on top.`
|
|
14113
14877
|
},
|
|
14114
14878
|
flatScreen: {
|
|
14115
14879
|
exposure: "included",
|
|
14116
|
-
schema:
|
|
14880
|
+
schema: z9.boolean().optional(),
|
|
14117
14881
|
prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
|
|
14118
14882
|
},
|
|
14119
14883
|
glare: {
|
|
14120
14884
|
exposure: "included",
|
|
14121
|
-
schema:
|
|
14885
|
+
schema: z9.boolean().optional(),
|
|
14122
14886
|
prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
|
|
14123
14887
|
},
|
|
14124
14888
|
lighting: {
|
|
14125
14889
|
exposure: "included",
|
|
14126
|
-
schema:
|
|
14890
|
+
schema: z9.boolean().optional(),
|
|
14127
14891
|
prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
|
|
14128
14892
|
},
|
|
14129
14893
|
reflections: {
|
|
14130
14894
|
exposure: "included",
|
|
14131
|
-
schema:
|
|
14895
|
+
schema: z9.boolean().optional(),
|
|
14132
14896
|
prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
|
|
14133
14897
|
},
|
|
14134
14898
|
clipToFrame: {
|
|
14135
14899
|
exposure: "included",
|
|
14136
|
-
schema:
|
|
14900
|
+
schema: z9.boolean().optional(),
|
|
14137
14901
|
prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
|
|
14138
14902
|
}
|
|
14139
14903
|
};
|
|
@@ -14141,7 +14905,7 @@ var init_aiShotProjection = __esm({
|
|
|
14141
14905
|
API_AI_SHOT_PATCH_SHAPE = Object.fromEntries(
|
|
14142
14906
|
API_AI_SHOT_INCLUDED_ENTRIES.map(([field, projection]) => [field, projection.schema])
|
|
14143
14907
|
);
|
|
14144
|
-
apiAiShotPatchSchema =
|
|
14908
|
+
apiAiShotPatchSchema = z9.object(API_AI_SHOT_PATCH_SHAPE);
|
|
14145
14909
|
API_AI_SHOT_PROMPT_INVENTORY = [
|
|
14146
14910
|
...new Set(API_AI_SHOT_INCLUDED_ENTRIES.map(([, projection]) => projection.prompt))
|
|
14147
14911
|
];
|
|
@@ -14163,7 +14927,9 @@ var init_visionGeneration = __esm({
|
|
|
14163
14927
|
LAYOUT_SELECTION_RULE = [
|
|
14164
14928
|
"LAYOUT SELECTION",
|
|
14165
14929
|
"- Choose the layout from the user's WORDS, not from the screenshots. A composition is a styling decision; pixels do not vote on it.",
|
|
14166
|
-
"- Every layout above ends with a [character].
|
|
14930
|
+
"- Every layout above ends with a [character] and a [bleeds] edge. Read the EDGE first: if the request says which way the device should go, that settles it.",
|
|
14931
|
+
...LAYOUT_BLEED_EDGE_IDS.map((edge) => ` - bleeds [${edge}] when ${LAYOUT_BLEED_EDGE_WORDS[edge]}.`),
|
|
14932
|
+
"- Otherwise pick the one whose character matches what was asked:",
|
|
14167
14933
|
...LAYOUT_CHARACTER_IDS.map((character) => ` - [${character}] when ${LAYOUT_CHARACTER_ANSWERS[character]}.`),
|
|
14168
14934
|
...LAYOUT_SELECTION_CAVEATS.map((line) => `- ${line}`)
|
|
14169
14935
|
].join("\n");
|
|
@@ -14171,8 +14937,8 @@ var init_visionGeneration = __esm({
|
|
|
14171
14937
|
});
|
|
14172
14938
|
|
|
14173
14939
|
// ../api/_lib/refineProjectContracts.ts
|
|
14174
|
-
import { z as
|
|
14175
|
-
var localeIds, MAX_DURABLE_MODEL_PROSE, hexSchema, currentColorSchema, shotPatchSchema, aiNumberSchema, captionStylePatchSchema, MAX_WIRE_GRADIENT_STOPS, gradientStopSchema, backgroundPatchSchema, stripBackgroundPatchSchema, framePatchSchema, currentCaptionStyleSchema, currentGradientStopSchema, currentStops, currentBackgroundSchema, currentStripBackgroundSchema, currentShotSchema, currentFramePatchSchema, localeCodeSchema, focusSchema, currentFrameSchema, captionOperationSchema, frameOperationSchema, frameIdSchema, assetIdSchema, structuralOperationSchema, attachmentSchema, visionScreenshotSchema, requestSchema, legacyFinalizeSchema, durableUsageSchema, durablePassSchema, durableStateDiffSchema, agentOutcomeSchema, durableFinalizeSchema, revertSchema, finalizeSchema, createSessionSchema, historySchema, REFINE_FIELD_LABELS;
|
|
14940
|
+
import { z as z10 } from "zod";
|
|
14941
|
+
var MAX_STRIP_FRAMES, localeIds, MAX_DURABLE_MODEL_PROSE, hexSchema, currentColorSchema, shotPatchSchema, aiNumberSchema, captionStylePatchSchema, MAX_WIRE_GRADIENT_STOPS, gradientStopSchema, backgroundPatchSchema, stripBackgroundPatchSchema, framePatchSchema, currentCaptionStyleSchema, currentGradientStopSchema, currentStops, currentBackgroundSchema, currentStripBackgroundSchema, currentShotSchema, currentFramePatchSchema, localeCodeSchema, focusSchema, currentFrameSchema, captionOperationSchema, frameOperationSchema, frameIdSchema, assetIdSchema, structuralOperationSchema, attachmentSchema, refinementAttachmentsSchema, visionScreenshotSchema, requestSchema, legacyFinalizeSchema, durableUsageSchema, durablePassSchema, durableStateDiffSchema, agentOutcomeSchema, durableFinalizeSchema, revertSchema, finalizeSchema, createSessionSchema, historySchema, REFINE_FIELD_LABELS;
|
|
14176
14942
|
var init_refineProjectContracts = __esm({
|
|
14177
14943
|
"../api/_lib/refineProjectContracts.ts"() {
|
|
14178
14944
|
"use strict";
|
|
@@ -14182,231 +14948,241 @@ var init_refineProjectContracts = __esm({
|
|
|
14182
14948
|
init_startIntent();
|
|
14183
14949
|
init_visionGeneration();
|
|
14184
14950
|
init_aiShotProjection();
|
|
14951
|
+
MAX_STRIP_FRAMES = 20;
|
|
14185
14952
|
localeIds = APP_STORE_LOCALES;
|
|
14186
14953
|
MAX_DURABLE_MODEL_PROSE = 32e3;
|
|
14187
|
-
hexSchema =
|
|
14188
|
-
currentColorSchema =
|
|
14954
|
+
hexSchema = z10.string().trim().regex(/^#?([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/);
|
|
14955
|
+
currentColorSchema = z10.string().trim().max(64);
|
|
14189
14956
|
shotPatchSchema = apiAiShotPatchSchema;
|
|
14190
|
-
aiNumberSchema =
|
|
14191
|
-
captionStylePatchSchema =
|
|
14192
|
-
fontId:
|
|
14957
|
+
aiNumberSchema = z10.union([z10.number().finite(), z10.object({ scale: z10.number().finite() })]);
|
|
14958
|
+
captionStylePatchSchema = z10.object({
|
|
14959
|
+
fontId: z10.enum(AI_CAPTION_FONT_IDS).optional(),
|
|
14193
14960
|
sizePt: aiNumberSchema.optional(),
|
|
14194
14961
|
color: hexSchema.optional(),
|
|
14195
|
-
align:
|
|
14962
|
+
align: z10.enum(["left", "center", "right"]).optional(),
|
|
14196
14963
|
maxWidth: aiNumberSchema.optional(),
|
|
14197
14964
|
lineHeight: aiNumberSchema.optional(),
|
|
14198
|
-
band:
|
|
14965
|
+
band: z10.enum(["top", "bottom"]).optional(),
|
|
14199
14966
|
bandInset: aiNumberSchema.optional(),
|
|
14200
14967
|
bandCenter: aiNumberSchema.optional(),
|
|
14201
14968
|
reserveLines: aiNumberSchema.optional()
|
|
14202
14969
|
});
|
|
14203
14970
|
MAX_WIRE_GRADIENT_STOPS = 32;
|
|
14204
|
-
gradientStopSchema =
|
|
14205
|
-
backgroundPatchSchema =
|
|
14206
|
-
stops:
|
|
14207
|
-
angle:
|
|
14971
|
+
gradientStopSchema = z10.object({ color: hexSchema, at: z10.number().finite() });
|
|
14972
|
+
backgroundPatchSchema = z10.object({
|
|
14973
|
+
stops: z10.array(gradientStopSchema).max(MAX_WIRE_GRADIENT_STOPS).optional(),
|
|
14974
|
+
angle: z10.number().finite().optional()
|
|
14208
14975
|
});
|
|
14209
14976
|
stripBackgroundPatchSchema = backgroundPatchSchema.extend({
|
|
14210
|
-
shadow:
|
|
14211
|
-
floorReflection:
|
|
14977
|
+
shadow: z10.boolean().optional(),
|
|
14978
|
+
floorReflection: z10.boolean().optional()
|
|
14212
14979
|
});
|
|
14213
|
-
framePatchSchema =
|
|
14980
|
+
framePatchSchema = z10.object({
|
|
14214
14981
|
captionStyle: captionStylePatchSchema.optional(),
|
|
14215
14982
|
background: backgroundPatchSchema.optional()
|
|
14216
14983
|
});
|
|
14217
14984
|
currentCaptionStyleSchema = captionStylePatchSchema.extend({
|
|
14218
14985
|
color: currentColorSchema.optional()
|
|
14219
14986
|
});
|
|
14220
|
-
currentGradientStopSchema =
|
|
14221
|
-
currentStops =
|
|
14987
|
+
currentGradientStopSchema = z10.object({ color: currentColorSchema, at: z10.number().finite() });
|
|
14988
|
+
currentStops = z10.array(currentGradientStopSchema).max(MAX_WIRE_GRADIENT_STOPS).optional();
|
|
14222
14989
|
currentBackgroundSchema = backgroundPatchSchema.extend({ stops: currentStops });
|
|
14223
14990
|
currentStripBackgroundSchema = stripBackgroundPatchSchema.extend({ stops: currentStops });
|
|
14224
14991
|
currentShotSchema = shotPatchSchema.extend({
|
|
14225
14992
|
customColor: currentColorSchema.optional(),
|
|
14226
14993
|
clayCustom: currentColorSchema.optional()
|
|
14227
14994
|
});
|
|
14228
|
-
currentFramePatchSchema =
|
|
14995
|
+
currentFramePatchSchema = z10.object({
|
|
14229
14996
|
captionStyle: currentCaptionStyleSchema.optional(),
|
|
14230
14997
|
background: currentBackgroundSchema.optional()
|
|
14231
14998
|
});
|
|
14232
|
-
localeCodeSchema =
|
|
14233
|
-
focusSchema =
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
kind:
|
|
14237
|
-
frameId:
|
|
14238
|
-
label:
|
|
14999
|
+
localeCodeSchema = z10.string().trim().regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/);
|
|
15000
|
+
focusSchema = z10.discriminatedUnion("kind", [
|
|
15001
|
+
z10.object({ kind: z10.literal("project"), label: z10.string().trim().min(1).max(180) }),
|
|
15002
|
+
z10.object({
|
|
15003
|
+
kind: z10.literal("frame"),
|
|
15004
|
+
frameId: z10.string().trim().min(1).max(180),
|
|
15005
|
+
label: z10.string().trim().min(1).max(180)
|
|
14239
15006
|
}),
|
|
14240
|
-
|
|
14241
|
-
kind:
|
|
14242
|
-
frameId:
|
|
14243
|
-
deviceId:
|
|
14244
|
-
label:
|
|
15007
|
+
z10.object({
|
|
15008
|
+
kind: z10.literal("device"),
|
|
15009
|
+
frameId: z10.string().trim().min(1).max(180),
|
|
15010
|
+
deviceId: z10.string().trim().min(1).max(180),
|
|
15011
|
+
label: z10.string().trim().min(1).max(180)
|
|
14245
15012
|
}),
|
|
14246
|
-
|
|
14247
|
-
kind:
|
|
14248
|
-
frameId:
|
|
14249
|
-
captionIndex:
|
|
14250
|
-
label:
|
|
15013
|
+
z10.object({
|
|
15014
|
+
kind: z10.literal("caption"),
|
|
15015
|
+
frameId: z10.string().trim().min(1).max(180),
|
|
15016
|
+
captionIndex: z10.number().int().min(0).max(32),
|
|
15017
|
+
label: z10.string().trim().min(1).max(240)
|
|
14251
15018
|
})
|
|
14252
15019
|
]);
|
|
14253
|
-
currentFrameSchema =
|
|
14254
|
-
id:
|
|
14255
|
-
label:
|
|
14256
|
-
sourceName:
|
|
14257
|
-
devices:
|
|
14258
|
-
id:
|
|
14259
|
-
label:
|
|
15020
|
+
currentFrameSchema = z10.object({
|
|
15021
|
+
id: z10.string().trim().min(1).max(180),
|
|
15022
|
+
label: z10.string().trim().min(1).max(180),
|
|
15023
|
+
sourceName: z10.string().trim().min(1).max(180).nullable(),
|
|
15024
|
+
devices: z10.array(z10.object({
|
|
15025
|
+
id: z10.string().trim().min(1).max(180),
|
|
15026
|
+
label: z10.string().trim().min(1).max(180),
|
|
14260
15027
|
look: currentShotSchema
|
|
14261
15028
|
})).max(20),
|
|
14262
|
-
captions:
|
|
14263
|
-
index:
|
|
14264
|
-
label:
|
|
14265
|
-
text:
|
|
15029
|
+
captions: z10.array(z10.object({
|
|
15030
|
+
index: z10.number().int().min(0).max(32),
|
|
15031
|
+
label: z10.string().trim().min(1).max(180),
|
|
15032
|
+
text: z10.string().max(240),
|
|
14266
15033
|
style: currentCaptionStyleSchema
|
|
14267
15034
|
})).max(32),
|
|
14268
|
-
background: currentBackgroundSchema.nullable()
|
|
15035
|
+
background: currentBackgroundSchema.nullable(),
|
|
15036
|
+
// The per-screenshot slot this frame's pixels occupy, or null for a frame with no screenshot yet.
|
|
15037
|
+
// Optional so a client that predates it still parses; the prompt then numbers by position.
|
|
15038
|
+
screenshotIndex: z10.number().int().min(0).max(31).nullable().optional()
|
|
14269
15039
|
});
|
|
14270
|
-
captionOperationSchema =
|
|
14271
|
-
|
|
14272
|
-
operation:
|
|
14273
|
-
frameId:
|
|
14274
|
-
afterIndex:
|
|
14275
|
-
text:
|
|
15040
|
+
captionOperationSchema = z10.discriminatedUnion("operation", [
|
|
15041
|
+
z10.object({
|
|
15042
|
+
operation: z10.literal("add"),
|
|
15043
|
+
frameId: z10.string().trim().min(1).max(180),
|
|
15044
|
+
afterIndex: z10.number().int().min(-1).max(32).optional(),
|
|
15045
|
+
text: z10.string().trim().max(240),
|
|
14276
15046
|
style: captionStylePatchSchema.optional()
|
|
14277
15047
|
}),
|
|
14278
|
-
|
|
14279
|
-
operation:
|
|
14280
|
-
frameId:
|
|
14281
|
-
index:
|
|
15048
|
+
z10.object({
|
|
15049
|
+
operation: z10.literal("remove"),
|
|
15050
|
+
frameId: z10.string().trim().min(1).max(180),
|
|
15051
|
+
index: z10.number().int().min(0).max(32)
|
|
14282
15052
|
}),
|
|
14283
|
-
|
|
14284
|
-
operation:
|
|
14285
|
-
frameId:
|
|
14286
|
-
index:
|
|
14287
|
-
toFrameId:
|
|
14288
|
-
toIndex:
|
|
15053
|
+
z10.object({
|
|
15054
|
+
operation: z10.literal("move"),
|
|
15055
|
+
frameId: z10.string().trim().min(1).max(180),
|
|
15056
|
+
index: z10.number().int().min(0).max(32),
|
|
15057
|
+
toFrameId: z10.string().trim().min(1).max(180),
|
|
15058
|
+
toIndex: z10.number().int().min(0).max(32).optional()
|
|
14289
15059
|
})
|
|
14290
15060
|
]);
|
|
14291
|
-
frameOperationSchema =
|
|
14292
|
-
|
|
14293
|
-
operation:
|
|
14294
|
-
afterFrameId:
|
|
15061
|
+
frameOperationSchema = z10.discriminatedUnion("operation", [
|
|
15062
|
+
z10.object({
|
|
15063
|
+
operation: z10.literal("add"),
|
|
15064
|
+
afterFrameId: z10.string().trim().min(1).max(180).optional()
|
|
14295
15065
|
}),
|
|
14296
|
-
|
|
14297
|
-
operation:
|
|
14298
|
-
frameId:
|
|
15066
|
+
z10.object({
|
|
15067
|
+
operation: z10.literal("remove"),
|
|
15068
|
+
frameId: z10.string().trim().min(1).max(180)
|
|
14299
15069
|
}),
|
|
14300
|
-
|
|
14301
|
-
operation:
|
|
14302
|
-
frameId:
|
|
14303
|
-
afterFrameId:
|
|
15070
|
+
z10.object({
|
|
15071
|
+
operation: z10.literal("move"),
|
|
15072
|
+
frameId: z10.string().trim().min(1).max(180),
|
|
15073
|
+
afterFrameId: z10.string().trim().min(1).max(180).optional()
|
|
14304
15074
|
}),
|
|
14305
|
-
|
|
14306
|
-
operation:
|
|
14307
|
-
frameId:
|
|
14308
|
-
deviceIds:
|
|
15075
|
+
z10.object({
|
|
15076
|
+
operation: z10.literal("split"),
|
|
15077
|
+
frameId: z10.string().trim().min(1).max(180),
|
|
15078
|
+
deviceIds: z10.array(z10.string().trim().min(1).max(180)).min(1).max(20)
|
|
14309
15079
|
})
|
|
14310
15080
|
]);
|
|
14311
|
-
frameIdSchema =
|
|
14312
|
-
assetIdSchema =
|
|
14313
|
-
structuralOperationSchema =
|
|
14314
|
-
|
|
14315
|
-
|
|
14316
|
-
|
|
14317
|
-
|
|
14318
|
-
|
|
15081
|
+
frameIdSchema = z10.string().trim().min(1).max(180);
|
|
15082
|
+
assetIdSchema = z10.string().trim().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/);
|
|
15083
|
+
structuralOperationSchema = z10.discriminatedUnion("op", [
|
|
15084
|
+
z10.object({
|
|
15085
|
+
op: z10.literal("frame.add"),
|
|
15086
|
+
afterFrameId: frameIdSchema.optional(),
|
|
15087
|
+
frameRef: z10.string().min(1).max(64).regex(/^[A-Za-z][A-Za-z0-9_-]*$/).describe("Patch-local frame alias, unique and different from every existing frame/device id. Later ordered operations may use it as a frame target. Never persisted.").optional()
|
|
15088
|
+
}),
|
|
15089
|
+
z10.object({ op: z10.literal("frame.remove"), frameId: frameIdSchema }),
|
|
15090
|
+
z10.object({ op: z10.literal("frame.move"), frameId: frameIdSchema, afterFrameId: frameIdSchema.optional() }),
|
|
15091
|
+
z10.object({
|
|
15092
|
+
op: z10.literal("frame.split"),
|
|
14319
15093
|
frameId: frameIdSchema,
|
|
14320
|
-
deviceIds:
|
|
15094
|
+
deviceIds: z10.array(frameIdSchema).min(1).max(20)
|
|
14321
15095
|
}),
|
|
14322
|
-
|
|
14323
|
-
|
|
14324
|
-
|
|
14325
|
-
op:
|
|
15096
|
+
z10.object({ op: z10.literal("frame.combine"), frameId: frameIdSchema, fromFrameId: frameIdSchema }),
|
|
15097
|
+
z10.object({ op: z10.literal("frame.template-reset"), frameId: frameIdSchema }),
|
|
15098
|
+
z10.object({
|
|
15099
|
+
op: z10.literal("caption.add"),
|
|
14326
15100
|
frameId: frameIdSchema,
|
|
14327
|
-
afterIndex:
|
|
14328
|
-
text:
|
|
15101
|
+
afterIndex: z10.number().int().min(-1).max(32).optional(),
|
|
15102
|
+
text: z10.string().trim().max(240),
|
|
14329
15103
|
style: captionStylePatchSchema.optional()
|
|
14330
15104
|
}),
|
|
14331
|
-
|
|
14332
|
-
|
|
14333
|
-
op:
|
|
15105
|
+
z10.object({ op: z10.literal("caption.remove"), frameId: frameIdSchema, index: z10.number().int().min(0).max(32) }),
|
|
15106
|
+
z10.object({
|
|
15107
|
+
op: z10.literal("caption.move"),
|
|
14334
15108
|
frameId: frameIdSchema,
|
|
14335
|
-
index:
|
|
15109
|
+
index: z10.number().int().min(0).max(32),
|
|
14336
15110
|
toFrameId: frameIdSchema,
|
|
14337
|
-
toIndex:
|
|
15111
|
+
toIndex: z10.number().int().min(0).max(32).optional()
|
|
14338
15112
|
}),
|
|
14339
|
-
|
|
14340
|
-
op:
|
|
15113
|
+
z10.object({
|
|
15114
|
+
op: z10.literal("device.move"),
|
|
14341
15115
|
deviceId: frameIdSchema,
|
|
14342
15116
|
toFrameId: frameIdSchema,
|
|
14343
|
-
toIndex:
|
|
15117
|
+
toIndex: z10.number().int().min(0).max(20).optional()
|
|
14344
15118
|
}),
|
|
14345
|
-
|
|
14346
|
-
|
|
15119
|
+
z10.object({ op: z10.literal("device.reorder"), deviceId: frameIdSchema, toIndex: z10.number().int().min(0).max(20) }),
|
|
15120
|
+
z10.object({ op: z10.literal("device.remove"), deviceId: frameIdSchema }),
|
|
14347
15121
|
// The two asset operations (#573). `assetId` is an OPAQUE per-turn handle, so the wire bounds its
|
|
14348
15122
|
// shape and nothing more: a path, a URL or an account id is never a legal value here because the
|
|
14349
15123
|
// handle table is the only thing that resolves one, and it holds only handles this turn minted.
|
|
14350
|
-
|
|
14351
|
-
op:
|
|
15124
|
+
z10.object({
|
|
15125
|
+
op: z10.literal("device.add"),
|
|
14352
15126
|
toFrameId: frameIdSchema,
|
|
14353
15127
|
assetId: assetIdSchema,
|
|
14354
|
-
toIndex:
|
|
15128
|
+
toIndex: z10.number().int().min(0).max(20).optional()
|
|
14355
15129
|
}),
|
|
14356
|
-
|
|
15130
|
+
z10.object({ op: z10.literal("device.replace"), deviceId: frameIdSchema, assetId: assetIdSchema })
|
|
14357
15131
|
]);
|
|
14358
|
-
attachmentSchema =
|
|
14359
|
-
ref:
|
|
14360
|
-
label:
|
|
14361
|
-
width:
|
|
14362
|
-
height:
|
|
15132
|
+
attachmentSchema = z10.object({
|
|
15133
|
+
ref: z10.string().trim().min(1).max(512),
|
|
15134
|
+
label: z10.string().trim().min(1).max(180),
|
|
15135
|
+
width: z10.number().int().min(1).max(2e4).optional(),
|
|
15136
|
+
height: z10.number().int().min(1).max(2e4).optional()
|
|
14363
15137
|
});
|
|
14364
|
-
|
|
14365
|
-
|
|
14366
|
-
|
|
14367
|
-
|
|
15138
|
+
refinementAttachmentsSchema = z10.array(attachmentSchema).max(MAX_START_SCREENSHOTS);
|
|
15139
|
+
visionScreenshotSchema = z10.object({
|
|
15140
|
+
name: z10.string().trim().min(1).max(180),
|
|
15141
|
+
mediaType: z10.enum(["image/png", "image/jpeg", "image/webp"]),
|
|
15142
|
+
data: z10.string().min(16).max(MAX_VISION_IMAGE_BASE64).regex(/^[A-Za-z0-9+/]+=*$/)
|
|
14368
15143
|
});
|
|
14369
|
-
requestSchema =
|
|
14370
|
-
projectId:
|
|
14371
|
-
sessionId:
|
|
14372
|
-
turnId:
|
|
14373
|
-
cycle:
|
|
14374
|
-
instruction:
|
|
15144
|
+
requestSchema = z10.object({
|
|
15145
|
+
projectId: z10.string().uuid(),
|
|
15146
|
+
sessionId: z10.string().uuid().optional(),
|
|
15147
|
+
turnId: z10.string().uuid().optional(),
|
|
15148
|
+
cycle: z10.number().int().min(1).max(3).optional(),
|
|
15149
|
+
instruction: z10.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH),
|
|
14375
15150
|
focus: focusSchema.optional(),
|
|
14376
15151
|
baseLocale: localeCodeSchema,
|
|
14377
|
-
current:
|
|
14378
|
-
captions:
|
|
14379
|
-
outputs:
|
|
15152
|
+
current: z10.object({
|
|
15153
|
+
captions: z10.array(z10.string().max(240)).min(1).max(MAX_STRIP_FRAMES),
|
|
15154
|
+
outputs: z10.array(z10.enum(START_OUTPUT_IDS)).min(1).max(START_OUTPUT_IDS.length),
|
|
14380
15155
|
// Bounded by the CATALOG, not by six. Studio's Add-language dialog offers every catalog entry
|
|
14381
15156
|
// with no ceiling and the reducer just appends, so a seventh language used to 400 every Agent
|
|
14382
15157
|
// turn on that project forever. A count ceiling below what the product lets a user build is a
|
|
14383
15158
|
// lockout; this one is only here to bound the prompt.
|
|
14384
|
-
locales:
|
|
14385
|
-
designPreset:
|
|
14386
|
-
layout:
|
|
14387
|
-
frames:
|
|
15159
|
+
locales: z10.array(localeCodeSchema).min(1).max(localeIds.length),
|
|
15160
|
+
designPreset: z10.enum(START_DESIGN_PRESET_IDS).nullable(),
|
|
15161
|
+
layout: z10.enum(START_LAYOUT_IDS).nullable(),
|
|
15162
|
+
frames: z10.array(currentShotSchema).min(1).max(MAX_STRIP_FRAMES),
|
|
14388
15163
|
background: currentStripBackgroundSchema.optional(),
|
|
14389
|
-
frameStyles:
|
|
14390
|
-
// Structural editing can add blank frames without adding source screenshots
|
|
14391
|
-
//
|
|
14392
|
-
//
|
|
14393
|
-
|
|
15164
|
+
frameStyles: z10.array(currentFramePatchSchema).min(1).max(MAX_STRIP_FRAMES).optional(),
|
|
15165
|
+
// Structural editing can add blank frames without adding source screenshots, so this and every
|
|
15166
|
+
// other per-frame array on `current` share `MAX_STRIP_FRAMES` rather than the vision-upload
|
|
15167
|
+
// ceiling — a real project frame belongs back in correction cycles instead of rejecting the
|
|
15168
|
+
// first successful `add`/`split` operation on a full strip (#838).
|
|
15169
|
+
structure: z10.array(currentFrameSchema).min(1).max(MAX_STRIP_FRAMES).optional()
|
|
14394
15170
|
}),
|
|
14395
|
-
screenshots:
|
|
15171
|
+
screenshots: z10.array(visionScreenshotSchema).min(1).max(MAX_STRIP_FRAMES),
|
|
14396
15172
|
// Private refs from Studio. Opaque asset handles are derived server-side, so a client cannot
|
|
14397
15173
|
// plant its own handle table or smuggle a ref into model-visible data.
|
|
14398
|
-
attachments:
|
|
15174
|
+
attachments: refinementAttachmentsSchema.optional(),
|
|
14399
15175
|
// Opaque per-turn handles, on the wire because the MCP doors mint the table where the refs are
|
|
14400
15176
|
// (the adapter) and buy the pass somewhere else — hosted in-process, local over the control
|
|
14401
15177
|
// plane. Carrying a descriptor is not carrying authority: `{ id, label }` names nothing outside
|
|
14402
15178
|
// the turn that minted it, binds nothing, and the Studio door overwrites this field with its own
|
|
14403
15179
|
// table's descriptors regardless of what a client sent. The gate that matters is the
|
|
14404
15180
|
// authoritative write, which re-checks every ref a record introduces.
|
|
14405
|
-
assets:
|
|
14406
|
-
id:
|
|
14407
|
-
label:
|
|
14408
|
-
width:
|
|
14409
|
-
height:
|
|
15181
|
+
assets: z10.array(z10.object({
|
|
15182
|
+
id: z10.string().trim().min(1).max(64),
|
|
15183
|
+
label: z10.string().trim().min(1).max(180),
|
|
15184
|
+
width: z10.number().int().min(1).max(2e4).optional(),
|
|
15185
|
+
height: z10.number().int().min(1).max(2e4).optional()
|
|
14410
15186
|
})).max(MAX_START_SCREENSHOTS).optional(),
|
|
14411
15187
|
composedPreview: visionScreenshotSchema.optional(),
|
|
14412
15188
|
// MEASURED, never authored (#550): the client derives these from the engine's own collision
|
|
@@ -14414,17 +15190,29 @@ var init_refineProjectContracts = __esm({
|
|
|
14414
15190
|
// prompt renders into prose, never a value that reaches the reducer, and a strip with an unusual
|
|
14415
15191
|
// number of overlaps must not 400 a turn that was not about layout. The client's own ceiling is
|
|
14416
15192
|
// `MAX_REPORTED_FINDINGS` (mockup-engine/layoutFindings.ts), well inside this.
|
|
14417
|
-
layoutFindings:
|
|
14418
|
-
kind:
|
|
14419
|
-
frames:
|
|
14420
|
-
depthPct:
|
|
14421
|
-
anchored:
|
|
14422
|
-
clearAtScale:
|
|
15193
|
+
layoutFindings: z10.array(z10.object({
|
|
15194
|
+
kind: z10.enum(["caption-caption", "caption-device", "device-device"]),
|
|
15195
|
+
frames: z10.array(z10.number().int().min(0).max(31)).min(1).max(2),
|
|
15196
|
+
depthPct: z10.number().int().min(0).max(100),
|
|
15197
|
+
anchored: z10.boolean(),
|
|
15198
|
+
clearAtScale: z10.number().min(0.05).max(1).optional(),
|
|
15199
|
+
clearAtMaxWidth: z10.number().min(0.1).max(1).optional()
|
|
14423
15200
|
})).max(24).optional(),
|
|
14424
|
-
previous:
|
|
14425
|
-
actualDiff:
|
|
14426
|
-
inspection:
|
|
14427
|
-
cycle:
|
|
15201
|
+
previous: z10.object({
|
|
15202
|
+
actualDiff: z10.array(z10.string().trim().min(1).max(300)).max(24),
|
|
15203
|
+
inspection: z10.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE),
|
|
15204
|
+
cycle: z10.number().int().min(1).max(2),
|
|
15205
|
+
// The creating and removing structural operations earlier cycles of this turn already
|
|
15206
|
+
// performed, so the prompt can list them and the model stops re-doing what it did. Optional: an
|
|
15207
|
+
// older client sends none, and the coordinator drops a repeat regardless.
|
|
15208
|
+
appliedOperations: z10.array(z10.object({
|
|
15209
|
+
op: z10.string().trim().min(1).max(40),
|
|
15210
|
+
target: z10.string().trim().min(1).max(180)
|
|
15211
|
+
})).max(32).optional(),
|
|
15212
|
+
sourceCaptions: z10.array(z10.object({
|
|
15213
|
+
frameId: frameIdSchema,
|
|
15214
|
+
captions: z10.array(z10.string().max(240)).max(32)
|
|
15215
|
+
})).max(MAX_STRIP_FRAMES).optional()
|
|
14428
15216
|
}).optional()
|
|
14429
15217
|
}).superRefine((value, context) => {
|
|
14430
15218
|
if (value.sessionId == null !== (value.turnId == null)) {
|
|
@@ -14454,13 +15242,13 @@ var init_refineProjectContracts = __esm({
|
|
|
14454
15242
|
});
|
|
14455
15243
|
}
|
|
14456
15244
|
});
|
|
14457
|
-
legacyFinalizeSchema =
|
|
14458
|
-
generationId:
|
|
14459
|
-
outcome:
|
|
14460
|
-
projectId:
|
|
14461
|
-
instruction:
|
|
14462
|
-
effectSummary:
|
|
14463
|
-
turnStatus:
|
|
15245
|
+
legacyFinalizeSchema = z10.object({
|
|
15246
|
+
generationId: z10.string().uuid(),
|
|
15247
|
+
outcome: z10.enum(["completed", "cancelled", "failed"]),
|
|
15248
|
+
projectId: z10.string().uuid(),
|
|
15249
|
+
instruction: z10.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH).optional(),
|
|
15250
|
+
effectSummary: z10.string().trim().min(1).max(2e3).optional(),
|
|
15251
|
+
turnStatus: z10.enum(["applied", "no_change", "failed", "cancelled"]).optional()
|
|
14464
15252
|
}).superRefine((value, context) => {
|
|
14465
15253
|
const durableFields = [value.instruction, value.effectSummary, value.turnStatus];
|
|
14466
15254
|
const present = durableFields.filter((field) => field !== void 0).length;
|
|
@@ -14472,52 +15260,57 @@ var init_refineProjectContracts = __esm({
|
|
|
14472
15260
|
});
|
|
14473
15261
|
}
|
|
14474
15262
|
});
|
|
14475
|
-
durableUsageSchema =
|
|
14476
|
-
inputTokens:
|
|
14477
|
-
cacheReadTokens:
|
|
14478
|
-
imageTokens:
|
|
14479
|
-
outputTokens:
|
|
14480
|
-
totalTokens:
|
|
15263
|
+
durableUsageSchema = z10.object({
|
|
15264
|
+
inputTokens: z10.number().int().min(0),
|
|
15265
|
+
cacheReadTokens: z10.number().int().min(0).default(0),
|
|
15266
|
+
imageTokens: z10.number().int().min(0).nullable(),
|
|
15267
|
+
outputTokens: z10.number().int().min(0),
|
|
15268
|
+
totalTokens: z10.number().int().min(0)
|
|
14481
15269
|
});
|
|
14482
|
-
durablePassSchema =
|
|
14483
|
-
phase:
|
|
14484
|
-
generationId:
|
|
14485
|
-
summary:
|
|
15270
|
+
durablePassSchema = z10.object({
|
|
15271
|
+
phase: z10.enum(["applying", "inspecting", "adjusting"]),
|
|
15272
|
+
generationId: z10.string().uuid(),
|
|
15273
|
+
summary: z10.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE),
|
|
14486
15274
|
usage: durableUsageSchema,
|
|
14487
|
-
createdAt:
|
|
15275
|
+
createdAt: z10.string().datetime()
|
|
14488
15276
|
});
|
|
14489
|
-
durableStateDiffSchema =
|
|
14490
|
-
summary:
|
|
14491
|
-
broaderChanges:
|
|
14492
|
-
remainingMismatch:
|
|
15277
|
+
durableStateDiffSchema = z10.object({
|
|
15278
|
+
summary: z10.string().trim().min(1).max(2e3),
|
|
15279
|
+
broaderChanges: z10.array(z10.string().trim().min(1).max(500)).max(24),
|
|
15280
|
+
remainingMismatch: z10.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE).nullable()
|
|
14493
15281
|
});
|
|
14494
|
-
agentOutcomeSchema =
|
|
15282
|
+
agentOutcomeSchema = z10.custom((value) => {
|
|
14495
15283
|
try {
|
|
14496
15284
|
return !!value && typeof value === "object" && validateAgentOutcome(value).length === 0;
|
|
14497
15285
|
} catch {
|
|
14498
15286
|
return false;
|
|
14499
15287
|
}
|
|
14500
15288
|
}, { message: "The Agent outcome is invalid." });
|
|
14501
|
-
durableFinalizeSchema =
|
|
14502
|
-
projectId:
|
|
14503
|
-
sessionId:
|
|
14504
|
-
turnId:
|
|
15289
|
+
durableFinalizeSchema = z10.object({
|
|
15290
|
+
projectId: z10.string().uuid(),
|
|
15291
|
+
sessionId: z10.string().uuid(),
|
|
15292
|
+
turnId: z10.string().uuid(),
|
|
14505
15293
|
// #580 — the retry / clarification chain, and the ONLY receipt fact this door takes from the
|
|
14506
15294
|
// browser. Every other figure on the receipt is re-read server-side from the ledger or derived
|
|
14507
15295
|
// from the validated outcome; a parent id is a correlation the client alone knows.
|
|
14508
|
-
parentTurnId:
|
|
14509
|
-
generationIds:
|
|
14510
|
-
outcome:
|
|
14511
|
-
committed:
|
|
14512
|
-
instruction:
|
|
15296
|
+
parentTurnId: z10.string().uuid().optional(),
|
|
15297
|
+
generationIds: z10.array(z10.string().uuid()).min(1).max(3),
|
|
15298
|
+
outcome: z10.enum(["completed", "cancelled", "failed"]),
|
|
15299
|
+
committed: z10.boolean(),
|
|
15300
|
+
instruction: z10.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH),
|
|
14513
15301
|
focus: focusSchema,
|
|
14514
|
-
interpretation:
|
|
14515
|
-
passes:
|
|
15302
|
+
interpretation: z10.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE),
|
|
15303
|
+
passes: z10.array(durablePassSchema).min(1).max(3),
|
|
14516
15304
|
finalStateDiff: durableStateDiffSchema,
|
|
14517
15305
|
agentOutcome: agentOutcomeSchema,
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
15306
|
+
// #781 — the coordinator's unmet-demand classification for the whole turn. Optional and
|
|
15307
|
+
// deliberately loose here: it is analytics, so a malformed value must never fail a finalize that
|
|
15308
|
+
// settles real money. `parseAgentTurnDemand` closes it against the vocabulary downstream, and
|
|
15309
|
+
// anything it rejects becomes `not_measured` rather than a 400.
|
|
15310
|
+
agentDemand: z10.unknown().optional(),
|
|
15311
|
+
turnStatus: z10.enum(["applied", "no_change", "capped", "needs_input", "failed", "cancelled"]),
|
|
15312
|
+
startedAt: z10.string().datetime(),
|
|
15313
|
+
completedAt: z10.string().datetime()
|
|
14521
15314
|
}).superRefine((value, context) => {
|
|
14522
15315
|
const passGenerationIds = value.passes.map((pass) => pass.generationId);
|
|
14523
15316
|
if (new Set(value.generationIds).size !== value.generationIds.length) {
|
|
@@ -14550,23 +15343,23 @@ var init_refineProjectContracts = __esm({
|
|
|
14550
15343
|
});
|
|
14551
15344
|
}
|
|
14552
15345
|
});
|
|
14553
|
-
revertSchema =
|
|
14554
|
-
intent:
|
|
14555
|
-
projectId:
|
|
15346
|
+
revertSchema = z10.object({
|
|
15347
|
+
intent: z10.literal("revert"),
|
|
15348
|
+
projectId: z10.string().uuid(),
|
|
14556
15349
|
/** The committed turn being undone. Becomes the new receipt's `parent_turn_id`. */
|
|
14557
|
-
turnId:
|
|
15350
|
+
turnId: z10.string().uuid(),
|
|
14558
15351
|
/** The revert's own turn id, minted by the browser exactly as a turn id is. */
|
|
14559
|
-
revertTurnId:
|
|
15352
|
+
revertTurnId: z10.string().uuid()
|
|
14560
15353
|
});
|
|
14561
|
-
finalizeSchema =
|
|
14562
|
-
createSessionSchema =
|
|
14563
|
-
projectId:
|
|
14564
|
-
sessionId:
|
|
14565
|
-
firstInstruction:
|
|
15354
|
+
finalizeSchema = z10.union([revertSchema, durableFinalizeSchema, legacyFinalizeSchema]);
|
|
15355
|
+
createSessionSchema = z10.object({
|
|
15356
|
+
projectId: z10.string().uuid(),
|
|
15357
|
+
sessionId: z10.string().uuid(),
|
|
15358
|
+
firstInstruction: z10.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH).optional()
|
|
14566
15359
|
});
|
|
14567
|
-
historySchema =
|
|
14568
|
-
projectId:
|
|
14569
|
-
sessionId:
|
|
15360
|
+
historySchema = z10.object({
|
|
15361
|
+
projectId: z10.string().uuid(),
|
|
15362
|
+
sessionId: z10.string().uuid().optional()
|
|
14570
15363
|
});
|
|
14571
15364
|
REFINE_FIELD_LABELS = {
|
|
14572
15365
|
projectId: "the project id",
|
|
@@ -14578,21 +15371,21 @@ var init_refineProjectContracts = __esm({
|
|
|
14578
15371
|
baseLocale: "the base language",
|
|
14579
15372
|
// The frame ceiling rides on all four fields it can surface through: Zod reports in declaration
|
|
14580
15373
|
// order, so any one of them can be the fault the user actually reads.
|
|
14581
|
-
"current.captions": `the caption list, one per frame and at most ${
|
|
15374
|
+
"current.captions": `the caption list, one per frame and at most ${MAX_STRIP_FRAMES}`,
|
|
14582
15375
|
"current.outputs": "the output size list",
|
|
14583
15376
|
"current.locales": "the language list on this project",
|
|
14584
15377
|
"current.designPreset": "the design preset",
|
|
14585
15378
|
"current.layout": "the layout template",
|
|
14586
|
-
"current.frames": `the per-frame device styling, at most ${
|
|
15379
|
+
"current.frames": `the per-frame device styling, at most ${MAX_STRIP_FRAMES} frames`,
|
|
14587
15380
|
"current.frames.customColor": "a custom device colour",
|
|
14588
15381
|
"current.frames.clayCustom": "a custom clay colour",
|
|
14589
15382
|
"current.background": "the strip background",
|
|
14590
|
-
"current.frameStyles": `the per-frame caption styling, at most ${
|
|
15383
|
+
"current.frameStyles": `the per-frame caption styling, at most ${MAX_STRIP_FRAMES} frames`,
|
|
14591
15384
|
"current.frameStyles.captionStyle.color": "a caption colour",
|
|
14592
15385
|
"current.frameStyles.background": "a frame's own background",
|
|
14593
|
-
"current.structure":
|
|
15386
|
+
"current.structure": `the frame structure of this project, at most ${MAX_STRIP_FRAMES} frames`,
|
|
14594
15387
|
current: "the current state of this project",
|
|
14595
|
-
screenshots: `the screenshot list, at most ${
|
|
15388
|
+
screenshots: `the screenshot list, at most ${MAX_STRIP_FRAMES} per Agent turn`,
|
|
14596
15389
|
attachments: `the screenshots offered to this turn, at most ${MAX_START_SCREENSHOTS}`,
|
|
14597
15390
|
assets: `the supplied-screenshot handles for this turn, at most ${MAX_START_SCREENSHOTS}`,
|
|
14598
15391
|
composedPreview: "the composed preview of the current strip",
|
|
@@ -14715,6 +15508,36 @@ async function compactVisionImage(data, name) {
|
|
|
14715
15508
|
}
|
|
14716
15509
|
throw new Error("A project screenshot could not be reduced for analysis.");
|
|
14717
15510
|
}
|
|
15511
|
+
async function withBoundAttachments(stored, state, variant, boundAssets, assetTable, readBytes) {
|
|
15512
|
+
const bound = state.shots.flatMap((shot) => {
|
|
15513
|
+
const assetId = boundAssets.get(shot.id);
|
|
15514
|
+
const ref = assetId ? assetTable.sources[assetId]?.ref : void 0;
|
|
15515
|
+
return ref ? [{ shot, ref }] : [];
|
|
15516
|
+
});
|
|
15517
|
+
if (bound.length === 0) return stored;
|
|
15518
|
+
const bytesByRef = await readBytes();
|
|
15519
|
+
if (!bytesByRef) return stored;
|
|
15520
|
+
const overlaid = /* @__PURE__ */ new Map();
|
|
15521
|
+
for (const { shot, ref } of bound) {
|
|
15522
|
+
const bytes = bytesByRef.get(ref);
|
|
15523
|
+
if (!bytes) continue;
|
|
15524
|
+
overlaid.set(shot.id, {
|
|
15525
|
+
shotId: shot.id,
|
|
15526
|
+
frameName: shot.frameName,
|
|
15527
|
+
variant,
|
|
15528
|
+
bytes: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64"),
|
|
15529
|
+
source: "supplied"
|
|
15530
|
+
});
|
|
15531
|
+
}
|
|
15532
|
+
if (overlaid.size === 0) return stored;
|
|
15533
|
+
return {
|
|
15534
|
+
screenManifest: [
|
|
15535
|
+
...stored.screenManifest.filter((entry) => !overlaid.has(entry.shotId)),
|
|
15536
|
+
...overlaid.values()
|
|
15537
|
+
],
|
|
15538
|
+
missing: stored.missing
|
|
15539
|
+
};
|
|
15540
|
+
}
|
|
14718
15541
|
async function renderedTurn(deps, state, screens) {
|
|
14719
15542
|
const projectFile = serializeProject(state);
|
|
14720
15543
|
const variant = projectRenderVariant(projectFile, state.baseLocale);
|
|
@@ -14728,47 +15551,55 @@ async function renderedTurn(deps, state, screens) {
|
|
|
14728
15551
|
if (!rendered.ok) throw new Error("The project could not be rendered for Agent inspection.");
|
|
14729
15552
|
const bytesByShotId = new Map(screens.screenManifest.map((entry) => [entry.shotId, entry.bytes]));
|
|
14730
15553
|
const baseCaptions = resolveCaptionText(state.captionText, state.baseLocale, state.baseLocale);
|
|
14731
|
-
const
|
|
15554
|
+
const hasPixels = (shot) => bytesByShotId.has(shot.id);
|
|
15555
|
+
const visionScreens = await Promise.all(state.panels.flatMap((panel) => {
|
|
14732
15556
|
const devices = state.shots.filter((shot) => shot.panelId === panel.id);
|
|
14733
|
-
const representative = devices.find(
|
|
15557
|
+
const representative = devices.find(hasPixels);
|
|
14734
15558
|
if (!representative) return [];
|
|
14735
15559
|
const bytes = bytesByShotId.get(representative.id);
|
|
14736
15560
|
const captions = baseCaptions[panel.id] ?? [];
|
|
14737
15561
|
const background2 = aiBackgroundFromPanel(state.panelBackgrounds[panel.id]);
|
|
14738
15562
|
return [compactVisionImage(bytes, representative.frameName).then((vision) => ({
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
background: background2
|
|
14746
|
-
},
|
|
14747
|
-
structure: {
|
|
14748
|
-
id: panel.id,
|
|
14749
|
-
label: `Frame ${frameIndex + 1}`,
|
|
14750
|
-
sourceName: devices[0]?.frameName ?? null,
|
|
14751
|
-
devices: devices.map((shot, deviceIndex) => ({
|
|
14752
|
-
id: shot.id,
|
|
14753
|
-
label: devices.length === 1 ? shot.frameName : `${shot.frameName} \xB7 Device ${deviceIndex + 1}`,
|
|
14754
|
-
look: aiShotPatchFromLook(shot.look)
|
|
14755
|
-
})),
|
|
14756
|
-
captions: captions.map((caption, index) => ({
|
|
14757
|
-
index,
|
|
14758
|
-
label: caption.text.trim() || `Caption ${index + 1}`,
|
|
14759
|
-
text: caption.text,
|
|
14760
|
-
style: aiCaptionStyleFromLayer(caption)
|
|
14761
|
-
})),
|
|
14762
|
-
background: background2
|
|
14763
|
-
}
|
|
15563
|
+
...vision,
|
|
15564
|
+
panelId: panel.id,
|
|
15565
|
+
caption: captions[0]?.text ?? "",
|
|
15566
|
+
frame: aiShotPatchFromLook(representative.look),
|
|
15567
|
+
captionStyle: aiCaptionStyleFromLayer(captions[0]),
|
|
15568
|
+
background: background2
|
|
14764
15569
|
}))];
|
|
14765
15570
|
}));
|
|
14766
|
-
|
|
15571
|
+
let nextScreenshot = 0;
|
|
15572
|
+
const structure = state.panels.map((panel, frameIndex) => {
|
|
15573
|
+
const devices = state.shots.filter((shot) => shot.panelId === panel.id);
|
|
15574
|
+
const captions = baseCaptions[panel.id] ?? [];
|
|
15575
|
+
const background2 = aiBackgroundFromPanel(state.panelBackgrounds[panel.id]);
|
|
15576
|
+
const screenshotIndex = devices.some(hasPixels) ? nextScreenshot++ : null;
|
|
15577
|
+
return {
|
|
15578
|
+
id: panel.id,
|
|
15579
|
+
label: `Frame ${frameIndex + 1}`,
|
|
15580
|
+
sourceName: devices[0]?.frameName ?? null,
|
|
15581
|
+
devices: devices.map((shot, deviceIndex) => ({
|
|
15582
|
+
id: shot.id,
|
|
15583
|
+
label: devices.length === 1 ? shot.frameName : `${shot.frameName} \xB7 Device ${deviceIndex + 1}`,
|
|
15584
|
+
look: aiShotPatchFromLook(shot.look)
|
|
15585
|
+
})),
|
|
15586
|
+
captions: captions.map((caption, index) => ({
|
|
15587
|
+
index,
|
|
15588
|
+
label: caption.text.trim() || `Caption ${index + 1}`,
|
|
15589
|
+
text: caption.text,
|
|
15590
|
+
style: aiCaptionStyleFromLayer(caption)
|
|
15591
|
+
})),
|
|
15592
|
+
background: background2,
|
|
15593
|
+
screenshotIndex
|
|
15594
|
+
};
|
|
15595
|
+
});
|
|
15596
|
+
if (visionScreens.length === 0) throw new Error("The project has no readable screenshots for Agent inspection.");
|
|
15597
|
+
const layoutFindings = rendered.layout?.findings ?? (rendered.layoutMeasured ? [] : void 0);
|
|
14767
15598
|
return {
|
|
14768
|
-
screens:
|
|
14769
|
-
structure
|
|
15599
|
+
screens: visionScreens,
|
|
15600
|
+
structure,
|
|
14770
15601
|
...rendered.stripBase64 ? { composedPreview: await compactVisionImage(rendered.stripBase64, "shotops-composed-preview.png") } : {},
|
|
14771
|
-
...
|
|
15602
|
+
...layoutFindings !== void 0 ? { layoutFindings } : {}
|
|
14772
15603
|
};
|
|
14773
15604
|
}
|
|
14774
15605
|
function outsideFocus(patch, focus, rendered) {
|
|
@@ -14841,11 +15672,14 @@ function requestForPass(context, input) {
|
|
|
14841
15672
|
// planner pass carries about a supplied screenshot has to be meaningless outside this turn.
|
|
14842
15673
|
...context.assets.length > 0 ? { assets: context.assets.map((asset) => ({ ...asset })) } : {},
|
|
14843
15674
|
...input.rendered.composedPreview ? { composedPreview: input.rendered.composedPreview } : {},
|
|
14844
|
-
|
|
15675
|
+
// #827 — `!== undefined`, not truthiness: a measured-clean render's `layoutFindings` is `[]`,
|
|
15676
|
+
// which must still reach the planner (it is what says "measured, nothing wrong").
|
|
15677
|
+
...input.rendered.layoutFindings !== void 0 ? { layoutFindings: input.rendered.layoutFindings } : {},
|
|
14845
15678
|
...input.previous ? {
|
|
14846
15679
|
previous: {
|
|
14847
15680
|
...input.previous,
|
|
14848
|
-
actualDiff: [...input.previous.actualDiff]
|
|
15681
|
+
actualDiff: [...input.previous.actualDiff],
|
|
15682
|
+
...input.previous.appliedOperations ? { appliedOperations: [...input.previous.appliedOperations] } : {}
|
|
14849
15683
|
}
|
|
14850
15684
|
} : {}
|
|
14851
15685
|
});
|
|
@@ -14866,7 +15700,9 @@ function generatedAuthorization(request, generated, generationId, panelIds, rend
|
|
|
14866
15700
|
...generated.followUp ? { followUp: generated.followUp } : {},
|
|
14867
15701
|
...patch ? { patch, broaderChanges: outsideFocus(patch, request.focus ?? contextFocus(), rendered) } : {},
|
|
14868
15702
|
...generated.outcome ? { terminalOutcome: generated.outcome } : {},
|
|
14869
|
-
...generated.gaps ? { gaps: generated.gaps } : {}
|
|
15703
|
+
...generated.gaps ? { gaps: generated.gaps } : {},
|
|
15704
|
+
// #781 — the same intent that decided the outcome, reported as this pass's unmet demand.
|
|
15705
|
+
...generated.demand ? { demand: generated.demand } : {}
|
|
14870
15706
|
};
|
|
14871
15707
|
}
|
|
14872
15708
|
function contextFocus() {
|
|
@@ -14877,7 +15713,7 @@ function applyAllowance(allowance, pass) {
|
|
|
14877
15713
|
}
|
|
14878
15714
|
async function authorizePass(context, request, panelIds, rendered) {
|
|
14879
15715
|
context.screenshotCount = request.screenshots.length + (request.composedPreview ? 1 : 0);
|
|
14880
|
-
const result = await context.runtime.plannerPass(context.userId, request, context.turnKey);
|
|
15716
|
+
const result = await context.runtime.plannerPass(context.userId, request, context.turnKey, context.assetTable);
|
|
14881
15717
|
applyAllowance(context.allowance, result.allowance);
|
|
14882
15718
|
if (result.ok) {
|
|
14883
15719
|
return generatedAuthorization(request, result.plan, result.generationId, panelIds, rendered);
|
|
@@ -14925,6 +15761,16 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
14925
15761
|
const allowance = { remaining: null, refunded: 0, settled: false };
|
|
14926
15762
|
let storedRecord;
|
|
14927
15763
|
let screenSources = null;
|
|
15764
|
+
let boundAssets = /* @__PURE__ */ new Map();
|
|
15765
|
+
let attachmentBytes = null;
|
|
15766
|
+
const readAttachmentBytes = () => {
|
|
15767
|
+
if (!runtime.readAttachments) return Promise.resolve(null);
|
|
15768
|
+
attachmentBytes ??= runtime.readAttachments(project2.id, attachments.map((attachment) => attachment.ref)).catch((error) => {
|
|
15769
|
+
runtime.log("mcp_agent_turn_attachment_read_failed", { turnKey, ...safeFailureDiagnostic(error) });
|
|
15770
|
+
return null;
|
|
15771
|
+
});
|
|
15772
|
+
return attachmentBytes;
|
|
15773
|
+
};
|
|
14928
15774
|
const context = {
|
|
14929
15775
|
userId: deps.userId,
|
|
14930
15776
|
projectId: project2.id,
|
|
@@ -14934,7 +15780,8 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
14934
15780
|
focus: input.focus,
|
|
14935
15781
|
allowance,
|
|
14936
15782
|
runtime,
|
|
14937
|
-
assets: assetTable.descriptors
|
|
15783
|
+
assets: assetTable.descriptors,
|
|
15784
|
+
assetTable
|
|
14938
15785
|
};
|
|
14939
15786
|
const result = await runAgentTurn({
|
|
14940
15787
|
turnId,
|
|
@@ -14965,12 +15812,14 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
14965
15812
|
renderComposed: async (state) => {
|
|
14966
15813
|
if (!screenSources || "error" in screenSources) throw new Error("The project screenshots are unavailable.");
|
|
14967
15814
|
const variant = projectRenderVariant(serializeProject(state), state.baseLocale);
|
|
14968
|
-
const
|
|
14969
|
-
if (screenSources.expected.length > 0 &&
|
|
15815
|
+
const stored = await screenSources.resolve(variant);
|
|
15816
|
+
if (screenSources.expected.length > 0 && stored.screenManifest.length === 0 && boundAssets.size === 0) {
|
|
14970
15817
|
throw new Error("The project has no readable screenshots for Agent inspection.");
|
|
14971
15818
|
}
|
|
15819
|
+
const resolved = await withBoundAttachments(stored, state, variant, boundAssets, assetTable, readAttachmentBytes);
|
|
14972
15820
|
return renderedTurn(deps, state, resolved);
|
|
14973
15821
|
},
|
|
15822
|
+
unresolvedLayout: (rendered) => describeUnresolvedLayout(rendered.layoutFindings),
|
|
14974
15823
|
authorize: async ({ cycle, state, rendered, previous }) => {
|
|
14975
15824
|
const request = requestForPass(context, { cycle, state, rendered, previous });
|
|
14976
15825
|
const authorization = await authorizePass(
|
|
@@ -15006,6 +15855,9 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
15006
15855
|
return write.error === "conflict" ? { ok: false, reason: "stale_version" } : { ok: false, reason: "write_failed" };
|
|
15007
15856
|
},
|
|
15008
15857
|
emit: async (event) => {
|
|
15858
|
+
if (event.type === "draft_applied") {
|
|
15859
|
+
boundAssets = new Map(event.assetBindings.map((binding) => [binding.deviceId, binding.assetId]));
|
|
15860
|
+
}
|
|
15009
15861
|
if (event.type === "turn_completed") {
|
|
15010
15862
|
const generationIds = event.result.legacy.passes.map((pass) => pass.generationId);
|
|
15011
15863
|
if (generationIds.length > 0) {
|
|
@@ -15023,7 +15875,8 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
15023
15875
|
startedAt,
|
|
15024
15876
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15025
15877
|
screenshotCount: context.screenshotCount,
|
|
15026
|
-
instructionLength: input.instruction.length
|
|
15878
|
+
instructionLength: input.instruction.length,
|
|
15879
|
+
demand: event.result.demand
|
|
15027
15880
|
}
|
|
15028
15881
|
});
|
|
15029
15882
|
allowance.remaining = settlement.balanceCredits;
|
|
@@ -15051,7 +15904,8 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
15051
15904
|
startedAt,
|
|
15052
15905
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15053
15906
|
screenshotCount: context.screenshotCount,
|
|
15054
|
-
instructionLength: input.instruction.length
|
|
15907
|
+
instructionLength: input.instruction.length,
|
|
15908
|
+
demand: event.result.demand
|
|
15055
15909
|
});
|
|
15056
15910
|
} catch (error) {
|
|
15057
15911
|
runtime.log("mcp_agent_turn_receipt_failed", { turnKey, ...safeFailureDiagnostic(error) });
|
|
@@ -15070,6 +15924,7 @@ var init_agentTurnAdapter = __esm({
|
|
|
15070
15924
|
init_agentOutcome();
|
|
15071
15925
|
init_projectRecord();
|
|
15072
15926
|
init_gradient();
|
|
15927
|
+
init_layoutFindings();
|
|
15073
15928
|
init_capabilities();
|
|
15074
15929
|
init_invalidRequest();
|
|
15075
15930
|
init_refineProjectContracts();
|
|
@@ -15102,7 +15957,16 @@ function writeErrorFrom(error) {
|
|
|
15102
15957
|
}
|
|
15103
15958
|
function localAgentTurnRuntime(bridge) {
|
|
15104
15959
|
return {
|
|
15105
|
-
plannerPass: (_userId, request, turnKey) => bridge.refinePass(
|
|
15960
|
+
plannerPass: (_userId, request, turnKey, assetTable) => bridge.refinePass(
|
|
15961
|
+
request.projectId,
|
|
15962
|
+
request,
|
|
15963
|
+
turnKey,
|
|
15964
|
+
void 0,
|
|
15965
|
+
assetTable.descriptors.map(({ id }) => {
|
|
15966
|
+
const { ref, label, width, height } = assetTable.sources[id];
|
|
15967
|
+
return { ref, label, ...width ? { width } : {}, ...height ? { height } : {} };
|
|
15968
|
+
})
|
|
15969
|
+
),
|
|
15106
15970
|
// The receipt facts travel WITH settlement (#580): the server writes the one receipt for this
|
|
15107
15971
|
// turn, keyed by the id the local coordinator ran under, and this machine — which holds no
|
|
15108
15972
|
// Supabase credential — writes nothing durable at all.
|
|
@@ -15122,10 +15986,17 @@ function localAgentTurnRuntime(bridge) {
|
|
|
15122
15986
|
startedAt: input.startedAt,
|
|
15123
15987
|
completedAt: input.completedAt,
|
|
15124
15988
|
screenshotCount: input.screenshotCount ?? 0,
|
|
15125
|
-
instructionLength: input.instructionLength ?? 0
|
|
15989
|
+
instructionLength: input.instructionLength ?? 0,
|
|
15990
|
+
demand: input.demand ?? NOT_MEASURED_DEMAND
|
|
15126
15991
|
}
|
|
15127
15992
|
});
|
|
15128
15993
|
},
|
|
15994
|
+
// #882 — the inspection render's pixels for devices a draft added or re-pointed. The control
|
|
15995
|
+
// plane authorizes and signs; the bytes come down here, where the render runs.
|
|
15996
|
+
readAttachments: async (projectId, refs) => {
|
|
15997
|
+
const screens = await bridge.readAttachmentScreens(projectId, refs);
|
|
15998
|
+
return new Map(await Promise.all(screens.map(async ({ ref, url }) => [ref, new Uint8Array(await downloadPng(url, "Supplied screenshot"))])));
|
|
15999
|
+
},
|
|
15129
16000
|
writeProjectRecord: async (projectId, expectedVersion, record8) => {
|
|
15130
16001
|
try {
|
|
15131
16002
|
const written = await bridge.updateProjectRecord(projectId, record8, expectedVersion);
|
|
@@ -15138,7 +16009,7 @@ function localAgentTurnRuntime(bridge) {
|
|
|
15138
16009
|
log: (event, fields2) => logEvent("error", event, fields2)
|
|
15139
16010
|
};
|
|
15140
16011
|
}
|
|
15141
|
-
async function runLocalRefineTurn(deps, bridge, input) {
|
|
16012
|
+
async function runLocalRefineTurn(deps, bridge, input, runtimeOverrides) {
|
|
15142
16013
|
try {
|
|
15143
16014
|
const turn = await runMcpProjectAgentTurn(
|
|
15144
16015
|
deps,
|
|
@@ -15148,7 +16019,7 @@ async function runLocalRefineTurn(deps, bridge, input) {
|
|
|
15148
16019
|
...input.project ? { project: input.project } : {},
|
|
15149
16020
|
...input.attachments?.length ? { attachments: input.attachments } : {}
|
|
15150
16021
|
},
|
|
15151
|
-
localAgentTurnRuntime(bridge)
|
|
16022
|
+
runtimeOverrides ? { ...localAgentTurnRuntime(bridge), ...runtimeOverrides } : localAgentTurnRuntime(bridge)
|
|
15152
16023
|
);
|
|
15153
16024
|
return refineProjectResult(turn);
|
|
15154
16025
|
} catch {
|
|
@@ -15159,9 +16030,11 @@ var init_localRefineTurn = __esm({
|
|
|
15159
16030
|
"src/project/localRefineTurn.ts"() {
|
|
15160
16031
|
"use strict";
|
|
15161
16032
|
init_log();
|
|
16033
|
+
init_agentUnmetActions();
|
|
15162
16034
|
init_agentTurnAdapter();
|
|
15163
16035
|
init_hostedBridge();
|
|
15164
16036
|
init_refineProjectReceipt();
|
|
16037
|
+
init_uploads();
|
|
15165
16038
|
}
|
|
15166
16039
|
});
|
|
15167
16040
|
|
|
@@ -15307,9 +16180,10 @@ var init_projectClaimClient = __esm({
|
|
|
15307
16180
|
|
|
15308
16181
|
// src/runtime/localDeps.ts
|
|
15309
16182
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
15310
|
-
function localToolDeps(token2) {
|
|
16183
|
+
function localToolDeps(token2, options = {}) {
|
|
15311
16184
|
const base = {
|
|
15312
|
-
|
|
16185
|
+
// #889 — the first render on a machine with no Playwright Chromium downloads it once, here only.
|
|
16186
|
+
renderer: new StripRenderer({ serve: "static", installBrowser: () => installChromium() }),
|
|
15313
16187
|
// Token-backed cloud writes cross the same authority in the control plane. This local handler
|
|
15314
16188
|
// stays permissive to avoid a second billing lookup; tokenless holds fail at persistence.
|
|
15315
16189
|
authorizeCapability: async (request) => ({
|
|
@@ -15372,7 +16246,7 @@ function localToolDeps(token2) {
|
|
|
15372
16246
|
}
|
|
15373
16247
|
};
|
|
15374
16248
|
}
|
|
15375
|
-
const bridge = connectHostedBridge(token2);
|
|
16249
|
+
const bridge = options.decorateBridge ? options.decorateBridge(connectHostedBridge(token2)) : connectHostedBridge(token2);
|
|
15376
16250
|
const bridged = {
|
|
15377
16251
|
...base,
|
|
15378
16252
|
resolveProject: (userId, projectId) => bridge.resolveProject(userId, projectId),
|
|
@@ -15413,7 +16287,7 @@ function localToolDeps(token2) {
|
|
|
15413
16287
|
await bridge.close();
|
|
15414
16288
|
}
|
|
15415
16289
|
};
|
|
15416
|
-
bridged.refineProject = (userId, input) => runLocalRefineTurn({ ...bridged, userId }, bridge, input);
|
|
16290
|
+
bridged.refineProject = (userId, input) => runLocalRefineTurn({ ...bridged, userId }, bridge, input, options.emit ? { emit: options.emit } : void 0);
|
|
15417
16291
|
return bridged;
|
|
15418
16292
|
}
|
|
15419
16293
|
async function pushScreens(bridge, projectId, record8, recordVersion, screenshots2) {
|
|
@@ -15499,6 +16373,7 @@ var init_localDeps = __esm({
|
|
|
15499
16373
|
"use strict";
|
|
15500
16374
|
init_env();
|
|
15501
16375
|
init_renderer();
|
|
16376
|
+
init_browserInstall();
|
|
15502
16377
|
init_hostedBridge();
|
|
15503
16378
|
init_localRefineTurn();
|
|
15504
16379
|
init_projectClaimClient();
|
|
@@ -15524,10 +16399,10 @@ __export(localCredentials_exports, {
|
|
|
15524
16399
|
});
|
|
15525
16400
|
import { chmod, mkdir, readFile as readFile5, rename, unlink, writeFile } from "node:fs/promises";
|
|
15526
16401
|
import { homedir } from "node:os";
|
|
15527
|
-
import { dirname, join as
|
|
16402
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
15528
16403
|
function credentialsPath() {
|
|
15529
|
-
const dir = process.env.SHOTOPS_CONFIG_DIR ||
|
|
15530
|
-
return
|
|
16404
|
+
const dir = process.env.SHOTOPS_CONFIG_DIR || join5(homedir(), ".shotops");
|
|
16405
|
+
return join5(dir, "credentials.json");
|
|
15531
16406
|
}
|
|
15532
16407
|
async function readFileJson() {
|
|
15533
16408
|
let raw;
|
|
@@ -15546,7 +16421,7 @@ async function readFileJson() {
|
|
|
15546
16421
|
}
|
|
15547
16422
|
async function writeFileJson(next) {
|
|
15548
16423
|
const target = credentialsPath();
|
|
15549
|
-
await mkdir(
|
|
16424
|
+
await mkdir(dirname2(target), { recursive: true, mode: 448 });
|
|
15550
16425
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
15551
16426
|
await writeFile(tmp, `${JSON.stringify(next, null, 2)}
|
|
15552
16427
|
`, { mode: 384 });
|
|
@@ -16041,7 +16916,7 @@ var init_releaseCommitments = __esm({
|
|
|
16041
16916
|
import { execFile } from "node:child_process";
|
|
16042
16917
|
import { mkdtemp, open, readFile as readFile6, readdir, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
16043
16918
|
import { tmpdir } from "node:os";
|
|
16044
|
-
import { extname as extname2, join as
|
|
16919
|
+
import { extname as extname2, join as join6, resolve } from "node:path";
|
|
16045
16920
|
import { inflateSync } from "fflate";
|
|
16046
16921
|
async function readAt(path, offset, length) {
|
|
16047
16922
|
const handle = await open(path, "r");
|
|
@@ -16122,8 +16997,8 @@ function plistStringArray(xml, key) {
|
|
|
16122
16997
|
}
|
|
16123
16998
|
async function parsePlist(bytes, source, runner) {
|
|
16124
16999
|
if (bytes.subarray(0, 8).toString() === "bplist00") {
|
|
16125
|
-
const scratch = await mkdtemp(
|
|
16126
|
-
const path =
|
|
17000
|
+
const scratch = await mkdtemp(join6(tmpdir(), "shotops-plist-"));
|
|
17001
|
+
const path = join6(scratch, "Info.plist");
|
|
16127
17002
|
try {
|
|
16128
17003
|
await writeFile2(path, bytes, { mode: 384 });
|
|
16129
17004
|
const json = await runner("plutil", ["-convert", "json", "-o", "-", path]);
|
|
@@ -16181,10 +17056,10 @@ async function findInfoPlists(root, path = root, found = []) {
|
|
|
16181
17056
|
const entries = await readdir(path, { withFileTypes: true });
|
|
16182
17057
|
for (const entry of entries) {
|
|
16183
17058
|
if (entry.isSymbolicLink()) continue;
|
|
16184
|
-
const child =
|
|
17059
|
+
const child = join6(path, entry.name);
|
|
16185
17060
|
if (entry.isDirectory()) {
|
|
16186
17061
|
if (entry.name.endsWith(".app")) {
|
|
16187
|
-
const candidate =
|
|
17062
|
+
const candidate = join6(child, "Contents", "Info.plist");
|
|
16188
17063
|
if ((await stat(candidate).catch(() => null))?.isFile()) found.push(candidate);
|
|
16189
17064
|
}
|
|
16190
17065
|
await findInfoPlists(root, child, found);
|
|
@@ -16199,8 +17074,8 @@ async function inspectPkg(path, runner) {
|
|
|
16199
17074
|
if (!/status:\s*signed|signed by/i.test(signature.toString("utf8")) || /no signature|unsigned/i.test(signature.toString("utf8"))) {
|
|
16200
17075
|
throw new Error("PKG is unsigned or pkgutil could not verify its signature");
|
|
16201
17076
|
}
|
|
16202
|
-
const scratch = await mkdtemp(
|
|
16203
|
-
const expanded =
|
|
17077
|
+
const scratch = await mkdtemp(join6(tmpdir(), "shotops-pkg-"));
|
|
17078
|
+
const expanded = join6(scratch, "expanded");
|
|
16204
17079
|
try {
|
|
16205
17080
|
await runner("pkgutil", ["--expand-full", path, expanded]).catch(() => {
|
|
16206
17081
|
throw new Error("PKG is not a supported flat package (pkgutil --expand-full failed)");
|
|
@@ -16261,7 +17136,7 @@ var init_releaseBuild = __esm({
|
|
|
16261
17136
|
|
|
16262
17137
|
// src/release/desiredReleaseSchema.ts
|
|
16263
17138
|
import { readFile as readFile7 } from "node:fs/promises";
|
|
16264
|
-
import { join as
|
|
17139
|
+
import { join as join7 } from "node:path";
|
|
16265
17140
|
function blobPath(digestValue) {
|
|
16266
17141
|
if (!SHA256.test(digestValue)) throw new Error(`not a SHA-256 digest: ${digestValue}`);
|
|
16267
17142
|
return `blobs/${digestValue.slice(0, 2)}/${digestValue}`;
|
|
@@ -16857,7 +17732,7 @@ function parseCanonicalJson(text3, path) {
|
|
|
16857
17732
|
return value;
|
|
16858
17733
|
}
|
|
16859
17734
|
async function readDesiredReleaseManifest(workspace) {
|
|
16860
|
-
const path =
|
|
17735
|
+
const path = join7(workspace, DESIRED_RELEASE_FILENAME);
|
|
16861
17736
|
let text3;
|
|
16862
17737
|
try {
|
|
16863
17738
|
text3 = await readFile7(path, "utf8");
|
|
@@ -16890,7 +17765,7 @@ function inspectDesiredReleaseValue(value) {
|
|
|
16890
17765
|
};
|
|
16891
17766
|
}
|
|
16892
17767
|
async function inspectDesiredReleaseWorkspace(workspace) {
|
|
16893
|
-
const path =
|
|
17768
|
+
const path = join7(workspace, DESIRED_RELEASE_FILENAME);
|
|
16894
17769
|
let text3;
|
|
16895
17770
|
try {
|
|
16896
17771
|
text3 = await readFile7(path, "utf8");
|
|
@@ -17015,7 +17890,7 @@ var init_desiredReleaseSchema = __esm({
|
|
|
17015
17890
|
|
|
17016
17891
|
// src/release/releaseRepository.ts
|
|
17017
17892
|
import { lstat as lstat2, readFile as readFile8, readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
|
17018
|
-
import { basename as basename2, extname as extname3, isAbsolute as isAbsolute3, join as
|
|
17893
|
+
import { basename as basename2, extname as extname3, isAbsolute as isAbsolute3, join as join8, resolve as resolve2 } from "node:path";
|
|
17019
17894
|
async function readSmallText(path, label) {
|
|
17020
17895
|
const { size } = await regularFile(path, label);
|
|
17021
17896
|
if (size > MAX_TEXT_BYTES) throw new Error(`${label} exceeds ${MAX_TEXT_BYTES} bytes: ${path}`);
|
|
@@ -17038,11 +17913,11 @@ async function readFieldDirectory(root, path, allowed, label) {
|
|
|
17038
17913
|
const entries = await readdir2(path, { withFileTypes: true });
|
|
17039
17914
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17040
17915
|
if (!entry.isFile() || entry.isSymbolicLink() || extname3(entry.name) !== ".txt") {
|
|
17041
|
-
throw new Error(`unknown ${label} input: ${sourcePath(root,
|
|
17916
|
+
throw new Error(`unknown ${label} input: ${sourcePath(root, join8(path, entry.name))}`);
|
|
17042
17917
|
}
|
|
17043
17918
|
const key = basename2(entry.name, ".txt");
|
|
17044
|
-
if (!allowed.has(key)) throw new Error(`unknown ${label} field: ${sourcePath(root,
|
|
17045
|
-
fields2[key] = await textValue(root,
|
|
17919
|
+
if (!allowed.has(key)) throw new Error(`unknown ${label} field: ${sourcePath(root, join8(path, entry.name))}`);
|
|
17920
|
+
fields2[key] = await textValue(root, join8(path, entry.name), `${label} field`);
|
|
17046
17921
|
}
|
|
17047
17922
|
return fields2;
|
|
17048
17923
|
}
|
|
@@ -17106,13 +17981,13 @@ async function readAppPreviews(root, previewRoot, allLocales, selectedLocales, a
|
|
|
17106
17981
|
}
|
|
17107
17982
|
for (const localeEntry of localeEntries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17108
17983
|
if (!localeEntry.isDirectory() || localeEntry.isSymbolicLink() || !allLocales.includes(localeEntry.name)) {
|
|
17109
|
-
throw new Error(`unknown app preview locale/input: ${sourcePath(root,
|
|
17984
|
+
throw new Error(`unknown app preview locale/input: ${sourcePath(root, join8(previewRoot, localeEntry.name))}`);
|
|
17110
17985
|
}
|
|
17111
17986
|
const locale2 = localeEntry.name;
|
|
17112
|
-
const files = await readdir2(
|
|
17987
|
+
const files = await readdir2(join8(previewRoot, locale2), { withFileTypes: true });
|
|
17113
17988
|
const positions = /* @__PURE__ */ new Map();
|
|
17114
17989
|
for (const file of files.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17115
|
-
const path =
|
|
17990
|
+
const path = join8(previewRoot, locale2, file.name);
|
|
17116
17991
|
if (!file.isFile() || file.isSymbolicLink() || !PREVIEW_EXTENSIONS.has(extname3(file.name).toLowerCase())) {
|
|
17117
17992
|
throw new Error(`unknown app preview input: ${sourcePath(root, path)}`);
|
|
17118
17993
|
}
|
|
@@ -17136,7 +18011,7 @@ async function readAppPreviews(root, previewRoot, allLocales, selectedLocales, a
|
|
|
17136
18011
|
return result;
|
|
17137
18012
|
}
|
|
17138
18013
|
async function readReviewAttachment(root, metadataRoot) {
|
|
17139
|
-
const pointer =
|
|
18014
|
+
const pointer = join8(metadataRoot, "app_review_attachment_file.txt");
|
|
17140
18015
|
let value;
|
|
17141
18016
|
try {
|
|
17142
18017
|
value = await readSmallText(pointer, "app review attachment pointer");
|
|
@@ -17157,7 +18032,7 @@ async function normalizeRepositoryReleaseInputs(repository, allLocales, selected
|
|
|
17157
18032
|
const root = resolve2(repository);
|
|
17158
18033
|
const info = await stat2(root).catch(() => null);
|
|
17159
18034
|
if (!info?.isDirectory()) throw new Error(`repository does not exist or is not a directory: ${repository}`);
|
|
17160
|
-
const metadataRoot =
|
|
18035
|
+
const metadataRoot = join8(root, "fastlane", "metadata");
|
|
17161
18036
|
const localized = [];
|
|
17162
18037
|
const app = {};
|
|
17163
18038
|
let review = {};
|
|
@@ -17175,7 +18050,7 @@ async function normalizeRepositoryReleaseInputs(repository, allLocales, selected
|
|
|
17175
18050
|
if (error.code !== "ENOENT") throw error;
|
|
17176
18051
|
}
|
|
17177
18052
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17178
|
-
const path =
|
|
18053
|
+
const path = join8(metadataRoot, entry.name);
|
|
17179
18054
|
if (entry.isSymbolicLink()) throw new Error(`metadata input must not be a symbolic link: ${sourcePath(root, path)}`);
|
|
17180
18055
|
if (entry.isDirectory()) {
|
|
17181
18056
|
if (entry.name === "review_information") review = await readFieldDirectory(root, path, REVIEW_METADATA, "review information");
|
|
@@ -17209,7 +18084,7 @@ async function normalizeRepositoryReleaseInputs(repository, allLocales, selected
|
|
|
17209
18084
|
compliance,
|
|
17210
18085
|
appPreviews: await readAppPreviews(
|
|
17211
18086
|
root,
|
|
17212
|
-
|
|
18087
|
+
join8(root, "fastlane", "app-previews"),
|
|
17213
18088
|
allLocales,
|
|
17214
18089
|
new Set(selectedLocales),
|
|
17215
18090
|
allOutputs,
|
|
@@ -17344,7 +18219,7 @@ var init_releaseRepository = __esm({
|
|
|
17344
18219
|
import { createReadStream as createReadStream2, createWriteStream } from "node:fs";
|
|
17345
18220
|
import { chmod as chmod2, lstat as lstat3, mkdir as mkdir2, mkdtemp as mkdtemp2, open as open2, readdir as readdir3, rename as rename2, rm as rm2, stat as stat3 } from "node:fs/promises";
|
|
17346
18221
|
import { writeFile as writeFile3 } from "node:fs/promises";
|
|
17347
|
-
import { dirname as
|
|
18222
|
+
import { dirname as dirname3, join as join9, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
|
|
17348
18223
|
import { pipeline } from "node:stream/promises";
|
|
17349
18224
|
async function secureDirectory(path) {
|
|
17350
18225
|
await mkdir2(path, { recursive: true, mode: 448 });
|
|
@@ -17401,9 +18276,9 @@ async function sealReleaseWorkspace(destination, manifest, contents, hooks = {})
|
|
|
17401
18276
|
await assertConverged(target, manifestText, existing.isDirectory() && !existing.isSymbolicLink());
|
|
17402
18277
|
return { reused: true };
|
|
17403
18278
|
}
|
|
17404
|
-
const parent =
|
|
18279
|
+
const parent = dirname3(target);
|
|
17405
18280
|
await secureDirectory(parent);
|
|
17406
|
-
const temporary = await mkdtemp2(
|
|
18281
|
+
const temporary = await mkdtemp2(join9(parent, `${TEMPORARY_WORKSPACE_PREFIX}${process.pid}-`));
|
|
17407
18282
|
if (process.platform !== "win32") await chmod2(temporary, 448);
|
|
17408
18283
|
let renamed = false;
|
|
17409
18284
|
try {
|
|
@@ -17411,9 +18286,9 @@ async function sealReleaseWorkspace(destination, manifest, contents, hooks = {})
|
|
|
17411
18286
|
for (const [path, artifacts] of expectedBlobs(manifest)) {
|
|
17412
18287
|
const content = contents.get(path);
|
|
17413
18288
|
if (!content) throw new Error(`sealed ${describe(artifacts)} has no bytes to write: ${path}`);
|
|
17414
|
-
const file =
|
|
17415
|
-
await secureDirectory(
|
|
17416
|
-
directories.add(
|
|
18289
|
+
const file = join9(temporary, ...path.split("/"));
|
|
18290
|
+
await secureDirectory(dirname3(file));
|
|
18291
|
+
directories.add(dirname3(file));
|
|
17417
18292
|
await copyBlob(content, file);
|
|
17418
18293
|
if (process.platform !== "win32") await chmod2(file, 384);
|
|
17419
18294
|
const written = await stat3(file);
|
|
@@ -17426,7 +18301,7 @@ async function sealReleaseWorkspace(destination, manifest, contents, hooks = {})
|
|
|
17426
18301
|
await fsyncPath(file, false);
|
|
17427
18302
|
await boundary("blob", path);
|
|
17428
18303
|
}
|
|
17429
|
-
const manifestPath =
|
|
18304
|
+
const manifestPath = join9(temporary, DESIRED_RELEASE_FILENAME);
|
|
17430
18305
|
await writeFile3(manifestPath, manifestText, { mode: 384 });
|
|
17431
18306
|
if (process.platform !== "win32") await chmod2(manifestPath, 384);
|
|
17432
18307
|
await fsyncPath(manifestPath, false);
|
|
@@ -17463,7 +18338,7 @@ async function assertConverged(target, manifestText, isDirectory) {
|
|
|
17463
18338
|
async function walkFiles(root, current, found) {
|
|
17464
18339
|
const entries = await readdir3(current, { withFileTypes: true });
|
|
17465
18340
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
17466
|
-
const path =
|
|
18341
|
+
const path = join9(current, entry.name);
|
|
17467
18342
|
if (entry.isSymbolicLink()) found.push(relative3(root, path).split(sep2).join("/"));
|
|
17468
18343
|
else if (entry.isDirectory()) await walkFiles(root, path, found);
|
|
17469
18344
|
else found.push(relative3(root, path).split(sep2).join("/"));
|
|
@@ -17474,7 +18349,7 @@ async function verifyReleaseWorkspace(path) {
|
|
|
17474
18349
|
const { manifest, text: text3 } = await readDesiredReleaseManifest(root);
|
|
17475
18350
|
const expected = expectedBlobs(manifest);
|
|
17476
18351
|
for (const [blob, artifacts] of expected) {
|
|
17477
|
-
const file =
|
|
18352
|
+
const file = join9(root, ...blob.split("/"));
|
|
17478
18353
|
const stats = await lstat3(file).catch(() => null);
|
|
17479
18354
|
if (!stats || stats.isSymbolicLink() || !stats.isFile()) {
|
|
17480
18355
|
throw new Error(`sealed ${describe(artifacts)} is missing or unsafe: ${blob}`);
|
|
@@ -17490,7 +18365,7 @@ async function verifyReleaseWorkspace(path) {
|
|
|
17490
18365
|
return { path: root, manifest, text: text3, unexpected };
|
|
17491
18366
|
}
|
|
17492
18367
|
function sealedBlobFile(workspace, digest4) {
|
|
17493
|
-
return
|
|
18368
|
+
return join9(resolve3(workspace), ...blobPath(digest4).split("/"));
|
|
17494
18369
|
}
|
|
17495
18370
|
async function collectReleaseWorkspaceGarbage(root, options = {}) {
|
|
17496
18371
|
const base = resolve3(root);
|
|
@@ -17505,7 +18380,7 @@ async function collectReleaseWorkspaceGarbage(root, options = {}) {
|
|
|
17505
18380
|
throw error;
|
|
17506
18381
|
}
|
|
17507
18382
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
17508
|
-
const path =
|
|
18383
|
+
const path = join9(base, entry.name);
|
|
17509
18384
|
if (!entry.name.startsWith(TEMPORARY_WORKSPACE_PREFIX)) {
|
|
17510
18385
|
result.kept += 1;
|
|
17511
18386
|
continue;
|
|
@@ -17515,7 +18390,7 @@ async function collectReleaseWorkspaceGarbage(root, options = {}) {
|
|
|
17515
18390
|
result.skipped.push({ path, reason: "not a real directory" });
|
|
17516
18391
|
continue;
|
|
17517
18392
|
}
|
|
17518
|
-
const complete = await lstat3(
|
|
18393
|
+
const complete = await lstat3(join9(path, DESIRED_RELEASE_FILENAME)).then((file) => file.isFile()).catch(() => false);
|
|
17519
18394
|
if (complete) {
|
|
17520
18395
|
result.skipped.push({ path, reason: "holds a complete desired release" });
|
|
17521
18396
|
continue;
|
|
@@ -17548,7 +18423,7 @@ var init_releaseWorkspace = __esm({
|
|
|
17548
18423
|
|
|
17549
18424
|
// src/release/releaseManifest.ts
|
|
17550
18425
|
import { homedir as homedir2 } from "node:os";
|
|
17551
|
-
import { isAbsolute as isAbsolute4, join as
|
|
18426
|
+
import { isAbsolute as isAbsolute4, join as join10, resolve as resolve4 } from "node:path";
|
|
17552
18427
|
function chosenAxis(all, explicit, label) {
|
|
17553
18428
|
const uniqueAll = Array.from(new Set(all.map((item) => item.trim()).filter(Boolean)));
|
|
17554
18429
|
if (uniqueAll.length === 0) throw new Error(`project has no authored ${label}`);
|
|
@@ -17582,7 +18457,7 @@ function resolveBundleId(explicit, project2, build) {
|
|
|
17582
18457
|
return expected;
|
|
17583
18458
|
}
|
|
17584
18459
|
function releaseWorkspaceRoot() {
|
|
17585
|
-
return
|
|
18460
|
+
return join10(process.env.SHOTOPS_CONFIG_DIR || join10(homedir2(), ".shotops"), "release-workspaces");
|
|
17586
18461
|
}
|
|
17587
18462
|
function releaseCopyWords(inputs, locale2) {
|
|
17588
18463
|
const entry = inputs.localized.find((candidate) => candidate.locale === locale2);
|
|
@@ -17604,7 +18479,7 @@ async function assembleDesiredRelease(input) {
|
|
|
17604
18479
|
const localeScope = chosenAxis(project2.locales, input.locales, "locales");
|
|
17605
18480
|
const outputScope = chosenAxis(project2.outputs.map((output2) => output2.id), input.outputs, "outputs");
|
|
17606
18481
|
const selectedOutputs2 = outputScope.selected.map((id) => project2.outputs.find((output2) => output2.id === id));
|
|
17607
|
-
const buildPath = input.build ? isAbsolute4(input.build) ? input.build :
|
|
18482
|
+
const buildPath = input.build ? isAbsolute4(input.build) ? input.build : join10(repository, input.build) : null;
|
|
17608
18483
|
const build = buildPath ? await inspectBuildArtifact(buildPath, repository) : null;
|
|
17609
18484
|
if (build && (isAbsolute4(build.source) || build.source.startsWith(".."))) {
|
|
17610
18485
|
throw new Error(`build must live inside the repository so the desired release records no absolute path: ${build.source}`);
|
|
@@ -17716,7 +18591,7 @@ async function assembleDesiredRelease(input) {
|
|
|
17716
18591
|
artifacts.push({
|
|
17717
18592
|
role: "app_preview",
|
|
17718
18593
|
custody: "sealed",
|
|
17719
|
-
path: seal(preview2.sha256, { file:
|
|
18594
|
+
path: seal(preview2.sha256, { file: join10(repository, ...preview2.source.split("/")) }),
|
|
17720
18595
|
sha256: preview2.sha256,
|
|
17721
18596
|
bytes: preview2.bytes,
|
|
17722
18597
|
mediaType: mediaTypeForPath(preview2.source),
|
|
@@ -17734,7 +18609,7 @@ async function assembleDesiredRelease(input) {
|
|
|
17734
18609
|
role: "review_attachment",
|
|
17735
18610
|
custody: "sealed",
|
|
17736
18611
|
path: seal(repositoryInputs.reviewAttachment.sha256, {
|
|
17737
|
-
file:
|
|
18612
|
+
file: join10(repository, ...repositoryInputs.reviewAttachment.source.split("/"))
|
|
17738
18613
|
}),
|
|
17739
18614
|
sha256: repositoryInputs.reviewAttachment.sha256,
|
|
17740
18615
|
bytes: repositoryInputs.reviewAttachment.bytes,
|
|
@@ -17751,11 +18626,11 @@ async function assembleDesiredRelease(input) {
|
|
|
17751
18626
|
const ccat = repositoryInputs.compliance?.export_compliance_ccat_file;
|
|
17752
18627
|
if (typeof ccat === "string" && ccat.trim()) {
|
|
17753
18628
|
if (isAbsolute4(ccat)) throw new Error("export_compliance_ccat_file must be repository-relative");
|
|
17754
|
-
const document = await referencedFile(repository,
|
|
18629
|
+
const document = await referencedFile(repository, join10(repository, ccat));
|
|
17755
18630
|
artifacts.push({
|
|
17756
18631
|
role: "compliance_file",
|
|
17757
18632
|
custody: "sealed",
|
|
17758
|
-
path: seal(document.sha256, { file:
|
|
18633
|
+
path: seal(document.sha256, { file: join10(repository, ...document.source.split("/")) }),
|
|
17759
18634
|
sha256: document.sha256,
|
|
17760
18635
|
bytes: document.bytes,
|
|
17761
18636
|
mediaType: mediaTypeForPath(document.source),
|
|
@@ -17857,7 +18732,7 @@ async function assembleDesiredRelease(input) {
|
|
|
17857
18732
|
};
|
|
17858
18733
|
const desiredId = computeDesiredId(payload);
|
|
17859
18734
|
const manifest = parseDesiredReleaseManifest({ ...payload, desiredId });
|
|
17860
|
-
const destination = resolve4(input.destination ||
|
|
18735
|
+
const destination = resolve4(input.destination || join10(releaseWorkspaceRoot(), desiredId));
|
|
17861
18736
|
const { reused } = await sealReleaseWorkspace(destination, manifest, sealed, input.seal ?? {});
|
|
17862
18737
|
if (reused) await verifyReleaseWorkspace(destination);
|
|
17863
18738
|
return { path: destination, manifest, reused };
|
|
@@ -18653,7 +19528,7 @@ var init_releaseOperations = __esm({
|
|
|
18653
19528
|
// src/release/releaseReviewArtifact.ts
|
|
18654
19529
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
18655
19530
|
import { lstat as lstat4, mkdir as mkdir3, readFile as readFile9, rename as rename3, unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
|
|
18656
|
-
import { basename as basename3, dirname as
|
|
19531
|
+
import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute5, join as join11, resolve as resolve5 } from "node:path";
|
|
18657
19532
|
import { strFromU8, strToU8 as strToU82, unzipSync, zipSync as zipSync2 } from "fflate";
|
|
18658
19533
|
function isRecord5(value) {
|
|
18659
19534
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -18752,7 +19627,7 @@ function screenshotReferences(plan) {
|
|
|
18752
19627
|
}
|
|
18753
19628
|
async function readScreenshot(root, reference) {
|
|
18754
19629
|
if (isAbsolute5(reference.source)) throw new Error(`release screenshot path must be workspace-relative: ${reference.source}`);
|
|
18755
|
-
const path =
|
|
19630
|
+
const path = join11(root, ...blobPath(reference.sha256).split("/"));
|
|
18756
19631
|
const info = await lstat4(path).catch(() => null);
|
|
18757
19632
|
if (!info?.isFile() || info.isSymbolicLink() || info.size !== reference.bytes) {
|
|
18758
19633
|
throw new Error(`release screenshot is missing or changed: ${reference.source}`);
|
|
@@ -18765,8 +19640,8 @@ async function readScreenshot(root, reference) {
|
|
|
18765
19640
|
return bytes;
|
|
18766
19641
|
}
|
|
18767
19642
|
async function atomicPrivateWrite(path, bytes) {
|
|
18768
|
-
await mkdir3(
|
|
18769
|
-
const temporary =
|
|
19643
|
+
await mkdir3(dirname4(path), { recursive: true, mode: 448 });
|
|
19644
|
+
const temporary = join11(dirname4(path), `.${basename3(path)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`);
|
|
18770
19645
|
try {
|
|
18771
19646
|
await writeFile4(temporary, bytes, { mode: 384 });
|
|
18772
19647
|
await rename3(temporary, path);
|
|
@@ -18892,16 +19767,16 @@ async function loadArtifact(path, now) {
|
|
|
18892
19767
|
async function loadPlan(path, now) {
|
|
18893
19768
|
const plan = await readCanonicalPlan(path);
|
|
18894
19769
|
const screenshots2 = /* @__PURE__ */ new Map();
|
|
18895
|
-
for (const reference of screenshotReferences(plan)) screenshots2.set(reference.sha256, await readScreenshot(
|
|
19770
|
+
for (const reference of screenshotReferences(plan)) screenshots2.set(reference.sha256, await readScreenshot(dirname4(path), reference));
|
|
18896
19771
|
return { plan, screenshots: screenshots2, sourcePath: path, portable: false, approval: releasePlanApproval(plan, now) };
|
|
18897
19772
|
}
|
|
18898
19773
|
async function loadReleaseReviewSource(source, now = /* @__PURE__ */ new Date()) {
|
|
18899
19774
|
let path = resolve5(source);
|
|
18900
19775
|
const info = await lstat4(path).catch(() => null);
|
|
18901
19776
|
if (info?.isDirectory()) {
|
|
18902
|
-
const artifact =
|
|
19777
|
+
const artifact = join11(path, "release-review.zip");
|
|
18903
19778
|
const directory = path;
|
|
18904
|
-
path = await lstat4(artifact).then(() => artifact).catch(() =>
|
|
19779
|
+
path = await lstat4(artifact).then(() => artifact).catch(() => join11(directory, "release-plan.json"));
|
|
18905
19780
|
if (!await lstat4(path).catch(() => null)) {
|
|
18906
19781
|
const inspected = await inspectDesiredReleaseWorkspace(directory).catch(() => null);
|
|
18907
19782
|
if (inspected) {
|
|
@@ -18937,7 +19812,7 @@ var init_releaseReviewArtifact = __esm({
|
|
|
18937
19812
|
// src/release/releaseJournal.ts
|
|
18938
19813
|
import { chmod as chmod3, mkdir as mkdir4, open as open3, readdir as readdir4, readFile as readFile10, rename as rename4, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
|
|
18939
19814
|
import { hostname } from "node:os";
|
|
18940
|
-
import { join as
|
|
19815
|
+
import { join as join12, resolve as resolve6 } from "node:path";
|
|
18941
19816
|
function assertDetail(detail) {
|
|
18942
19817
|
if (typeof detail !== "object" || detail === null || Array.isArray(detail)) {
|
|
18943
19818
|
throw new ReleaseJournalError("corrupt", "journal detail is not an object");
|
|
@@ -19118,8 +19993,8 @@ async function openReleaseAttemptJournal(options) {
|
|
|
19118
19993
|
await mkdir4(directory, { recursive: true, mode: 448 });
|
|
19119
19994
|
if (process.platform !== "win32") await chmod3(directory, 448).catch(() => {
|
|
19120
19995
|
});
|
|
19121
|
-
const path =
|
|
19122
|
-
const releaseLock = await acquireAttemptLock(
|
|
19996
|
+
const path = join12(directory, journalFileName(options.attemptId));
|
|
19997
|
+
const releaseLock = await acquireAttemptLock(join12(directory, lockFileName(options.attemptId)), now);
|
|
19123
19998
|
let entries;
|
|
19124
19999
|
let checksum;
|
|
19125
20000
|
try {
|
|
@@ -19204,7 +20079,7 @@ async function readLiveAttemptJournals(directory) {
|
|
|
19204
20079
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
19205
20080
|
const match = entry.isFile() ? JOURNAL_FILE.exec(entry.name) : null;
|
|
19206
20081
|
if (!match) continue;
|
|
19207
|
-
const path =
|
|
20082
|
+
const path = join12(directory, entry.name);
|
|
19208
20083
|
const text3 = await readFile10(path, "utf8").catch(() => null);
|
|
19209
20084
|
if (text3 === null) continue;
|
|
19210
20085
|
const header = safeJson(text3.split("\n", 1)[0]);
|
|
@@ -19218,7 +20093,7 @@ async function readLiveAttemptJournals(directory) {
|
|
|
19218
20093
|
return found;
|
|
19219
20094
|
}
|
|
19220
20095
|
function attemptDirectoryOf(workspace) {
|
|
19221
|
-
return
|
|
20096
|
+
return join12(resolve6(workspace), ATTEMPT_DIRECTORY_NAME);
|
|
19222
20097
|
}
|
|
19223
20098
|
async function holdsLiveAttemptJournal(candidate) {
|
|
19224
20099
|
const own = await readLiveAttemptJournals(attemptDirectoryOf(candidate));
|
|
@@ -19526,7 +20401,7 @@ var init_releaseReceipt = __esm({
|
|
|
19526
20401
|
|
|
19527
20402
|
// src/release/releaseCommitBuild.ts
|
|
19528
20403
|
import { chmod as chmod4, link, copyFile, mkdtemp as mkdtemp3, rm as rm3 } from "node:fs/promises";
|
|
19529
|
-
import { basename as basename4, dirname as
|
|
20404
|
+
import { basename as basename4, dirname as dirname5, join as join13 } from "node:path";
|
|
19530
20405
|
function buildEventsOf(entries, attemptId) {
|
|
19531
20406
|
const events = [];
|
|
19532
20407
|
for (const entry of entries) {
|
|
@@ -19680,10 +20555,10 @@ function journalVouchesFor(events, approved, evidence) {
|
|
|
19680
20555
|
return namedRemoteIds.size === 0 || namedRemoteIds.size === 1 && namedRemoteIds.has(evidence.remoteBuildId);
|
|
19681
20556
|
}
|
|
19682
20557
|
async function stageApprovedBytes(approved, workspace) {
|
|
19683
|
-
const dir = await mkdtemp3(
|
|
20558
|
+
const dir = await mkdtemp3(join13(dirname5(workspace), TRANSFER_STAGING_PREFIX));
|
|
19684
20559
|
if (process.platform !== "win32") await chmod4(dir, 448);
|
|
19685
20560
|
const name = approved.fileName.toLowerCase().endsWith(`.${approved.format}`) ? approved.fileName : `${approved.fileName}.${approved.format}`;
|
|
19686
|
-
const file =
|
|
20561
|
+
const file = join13(dir, name);
|
|
19687
20562
|
try {
|
|
19688
20563
|
await link(approved.blobFile, file);
|
|
19689
20564
|
} catch {
|
|
@@ -19841,7 +20716,7 @@ async function commitReleaseBuild(input, deps) {
|
|
|
19841
20716
|
const output2 = await deps.runTransporter(
|
|
19842
20717
|
input.transporterPath,
|
|
19843
20718
|
["-m", "upload", "-assetFile", staged.file, "-apiKey", input.setup.keyId, "-apiIssuer", input.setup.issuerId],
|
|
19844
|
-
{ ...process.env, API_PRIVATE_KEYS_DIR:
|
|
20719
|
+
{ ...process.env, API_PRIVATE_KEYS_DIR: dirname5(input.setup.keyPath) }
|
|
19845
20720
|
);
|
|
19846
20721
|
answer = readTransporterAnswer(output2);
|
|
19847
20722
|
} catch {
|
|
@@ -20014,7 +20889,7 @@ __export(releaseRecovery_exports, {
|
|
|
20014
20889
|
});
|
|
20015
20890
|
import { readFile as readFile11, readdir as readdir5, rename as rename5, stat as stat4 } from "node:fs/promises";
|
|
20016
20891
|
import { hostname as hostname2 } from "node:os";
|
|
20017
|
-
import { join as
|
|
20892
|
+
import { join as join14, relative as relative4, resolve as resolve7, sep as sep3 } from "node:path";
|
|
20018
20893
|
function normalizedNow(now) {
|
|
20019
20894
|
const value = now().toISOString();
|
|
20020
20895
|
if (new Date(value).toISOString() !== value) throw new ReleaseRecoveryError("recovery clock did not return a normalized timestamp");
|
|
@@ -20074,7 +20949,7 @@ function processIsAlive2(pid) {
|
|
|
20074
20949
|
}
|
|
20075
20950
|
}
|
|
20076
20951
|
async function lockState(root, attemptId) {
|
|
20077
|
-
const text3 = await readFile11(
|
|
20952
|
+
const text3 = await readFile11(join14(root, `attempt-${attemptId}.lock`), "utf8").catch((error) => {
|
|
20078
20953
|
if (error.code === "ENOENT") return null;
|
|
20079
20954
|
throw error;
|
|
20080
20955
|
});
|
|
@@ -20098,7 +20973,7 @@ async function inspectLocalEvidence(input) {
|
|
|
20098
20973
|
const workspacePath = resolve7(input.workspace);
|
|
20099
20974
|
const info = await stat4(workspacePath).catch(() => null);
|
|
20100
20975
|
if (!info?.isDirectory()) throw new ReleaseRecoveryError("the release workspace does not exist");
|
|
20101
|
-
const planPath = resolve7(input.planPath ??
|
|
20976
|
+
const planPath = resolve7(input.planPath ?? join14(workspacePath, PLAN_FILENAME));
|
|
20102
20977
|
const plan = await readPlan(planPath);
|
|
20103
20978
|
const attemptRoot = resolve7(input.attemptRoot ?? attemptDirectoryOf(workspacePath));
|
|
20104
20979
|
const diagnostics = [];
|
|
@@ -20117,8 +20992,8 @@ async function inspectLocalEvidence(input) {
|
|
|
20117
20992
|
let finalReceipt = null;
|
|
20118
20993
|
let partialReceipt = null;
|
|
20119
20994
|
let receiptState = "none";
|
|
20120
|
-
const finalPath =
|
|
20121
|
-
const partialPath =
|
|
20995
|
+
const finalPath = join14(attemptRoot, RECEIPT_FILENAME);
|
|
20996
|
+
const partialPath = join14(attemptRoot, PARTIAL_RECEIPT_FILENAME);
|
|
20122
20997
|
const final = await readJsonEvidence(finalPath);
|
|
20123
20998
|
if (final.exists) {
|
|
20124
20999
|
const location = safeEvidenceLocation(workspacePath, finalPath);
|
|
@@ -20152,7 +21027,7 @@ async function inspectLocalEvidence(input) {
|
|
|
20152
21027
|
for (const entry of directory.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
20153
21028
|
const match = entry.isFile() ? JOURNAL_FILE2.exec(entry.name) : null;
|
|
20154
21029
|
if (!match) continue;
|
|
20155
|
-
const path =
|
|
21030
|
+
const path = join14(attemptRoot, entry.name);
|
|
20156
21031
|
const location = safeEvidenceLocation(workspacePath, path);
|
|
20157
21032
|
if (location) evidenceLocations.push(location);
|
|
20158
21033
|
const attemptId2 = match[1];
|
|
@@ -20189,7 +21064,7 @@ async function inspectLocalEvidence(input) {
|
|
|
20189
21064
|
diagnostics.push("attempt_locked");
|
|
20190
21065
|
}
|
|
20191
21066
|
}
|
|
20192
|
-
const backup =
|
|
21067
|
+
const backup = join14(attemptRoot, "screenshot-backup");
|
|
20193
21068
|
if ((await stat4(backup).catch(() => null))?.isDirectory()) {
|
|
20194
21069
|
const location = safeEvidenceLocation(workspacePath, backup);
|
|
20195
21070
|
if (location) evidenceLocations.push(location);
|
|
@@ -20225,17 +21100,17 @@ function attemptIdOf(value) {
|
|
|
20225
21100
|
}
|
|
20226
21101
|
async function inspectForeignAttempt(attemptRoot, planId) {
|
|
20227
21102
|
const foreign = (value) => value !== null && typeof value.planId === "string" && SHA2565.test(value.planId) && value.planId !== planId;
|
|
20228
|
-
const final = await readLooseRecord(
|
|
21103
|
+
const final = await readLooseRecord(join14(attemptRoot, RECEIPT_FILENAME));
|
|
20229
21104
|
if (foreign(final)) {
|
|
20230
21105
|
return { attemptId: attemptIdOf(final.attemptId), planId: final.planId, finished: final.state === "completed" };
|
|
20231
21106
|
}
|
|
20232
|
-
const partial = await readLooseRecord(
|
|
21107
|
+
const partial = await readLooseRecord(join14(attemptRoot, PARTIAL_RECEIPT_FILENAME));
|
|
20233
21108
|
if (foreign(partial)) return { attemptId: attemptIdOf(partial.attemptId), planId: partial.planId, finished: false };
|
|
20234
21109
|
const directory = await readdir5(attemptRoot, { withFileTypes: true }).catch(() => []);
|
|
20235
21110
|
for (const entry of directory.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
20236
21111
|
const match = entry.isFile() ? JOURNAL_FILE2.exec(entry.name) : null;
|
|
20237
21112
|
if (!match) continue;
|
|
20238
|
-
const text3 = await readFile11(
|
|
21113
|
+
const text3 = await readFile11(join14(attemptRoot, entry.name), "utf8").catch(() => "");
|
|
20239
21114
|
let header = null;
|
|
20240
21115
|
try {
|
|
20241
21116
|
header = JSON.parse(text3.split("\n")[0] ?? "");
|
|
@@ -22012,7 +22887,7 @@ var init_releaseCommitListing = __esm({
|
|
|
22012
22887
|
// src/release/releaseCommitScreenshots.ts
|
|
22013
22888
|
import { createHash as createHash6 } from "node:crypto";
|
|
22014
22889
|
import { chmod as chmod5, lstat as lstat5, mkdir as mkdir5, mkdtemp as mkdtemp4, open as open4, readFile as readFile12, rename as rename6, rm as rm4, stat as stat5, writeFile as writeFile6 } from "node:fs/promises";
|
|
22015
|
-
import { dirname as
|
|
22890
|
+
import { dirname as dirname6, join as join15, resolve as resolve8 } from "node:path";
|
|
22016
22891
|
function fail4(message) {
|
|
22017
22892
|
throw new Error(message);
|
|
22018
22893
|
}
|
|
@@ -22176,7 +23051,7 @@ function parseScreenshotBackup(value) {
|
|
|
22176
23051
|
async function verifyBackup(root, manifest) {
|
|
22177
23052
|
for (const set of manifest.sets) {
|
|
22178
23053
|
for (const image of set.images) {
|
|
22179
|
-
const file =
|
|
23054
|
+
const file = join15(root, ...image.path.split("/"));
|
|
22180
23055
|
const info = await lstat5(file).catch(() => null);
|
|
22181
23056
|
if (!info?.isFile() || info.isSymbolicLink()) fail4(`the recovery backup is missing an image for ${set.locale}/${set.deviceSet}`);
|
|
22182
23057
|
if (info.size !== image.bytes) fail4(`a recovery backup image for ${set.locale}/${set.deviceSet} changed size`);
|
|
@@ -22185,7 +23060,7 @@ async function verifyBackup(root, manifest) {
|
|
|
22185
23060
|
}
|
|
22186
23061
|
}
|
|
22187
23062
|
async function readExistingBackup(root) {
|
|
22188
|
-
const text3 = await readFile12(
|
|
23063
|
+
const text3 = await readFile12(join15(root, BACKUP_FILENAME), "utf8").catch(() => null);
|
|
22189
23064
|
if (text3 === null) return null;
|
|
22190
23065
|
let value;
|
|
22191
23066
|
try {
|
|
@@ -22199,23 +23074,23 @@ async function readExistingBackup(root) {
|
|
|
22199
23074
|
}
|
|
22200
23075
|
async function sealBackup(destination, manifest, blobs) {
|
|
22201
23076
|
const target = resolve8(destination);
|
|
22202
|
-
const parent =
|
|
23077
|
+
const parent = dirname6(target);
|
|
22203
23078
|
await secureDirectory2(parent);
|
|
22204
|
-
const temporary = await mkdtemp4(
|
|
23079
|
+
const temporary = await mkdtemp4(join15(parent, `.shotops-screenshot-backup-${process.pid}-`));
|
|
22205
23080
|
let renamed = false;
|
|
22206
23081
|
try {
|
|
22207
23082
|
const directories = /* @__PURE__ */ new Set();
|
|
22208
23083
|
for (const [path, bytes] of blobs) {
|
|
22209
|
-
const file =
|
|
22210
|
-
await secureDirectory2(
|
|
22211
|
-
directories.add(
|
|
23084
|
+
const file = join15(temporary, ...path.split("/"));
|
|
23085
|
+
await secureDirectory2(dirname6(file));
|
|
23086
|
+
directories.add(dirname6(file));
|
|
22212
23087
|
await writeFile6(file, bytes, { mode: 384 });
|
|
22213
23088
|
const written = await stat5(file);
|
|
22214
23089
|
if (written.size !== bytes.byteLength) fail4("a recovery backup image changed size while it was being written");
|
|
22215
23090
|
if (await sha256File(file) !== sha256(bytes)) fail4("a recovery backup image changed content while it was being written");
|
|
22216
23091
|
await fsyncPath2(file, false);
|
|
22217
23092
|
}
|
|
22218
|
-
const manifestPath =
|
|
23093
|
+
const manifestPath = join15(temporary, BACKUP_FILENAME);
|
|
22219
23094
|
await writeFile6(manifestPath, canonicalJson(manifest), { mode: 384 });
|
|
22220
23095
|
await fsyncPath2(manifestPath, false);
|
|
22221
23096
|
for (const directory of [...directories].sort((left, right) => right.length - left.length)) {
|
|
@@ -22775,7 +23650,7 @@ __export(releasePlan_exports, {
|
|
|
22775
23650
|
import { createHash as createHash7 } from "node:crypto";
|
|
22776
23651
|
import { createReadStream as createReadStream3 } from "node:fs";
|
|
22777
23652
|
import { chmod as chmod6, lstat as lstat6, rename as rename7, writeFile as writeFile7 } from "node:fs/promises";
|
|
22778
|
-
import { basename as basename5, dirname as
|
|
23653
|
+
import { basename as basename5, dirname as dirname7, join as join16, resolve as resolve9 } from "node:path";
|
|
22779
23654
|
async function md5File(path) {
|
|
22780
23655
|
const hash = createHash7("md5");
|
|
22781
23656
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
@@ -23412,7 +24287,7 @@ function envelopeFor2(plan) {
|
|
|
23412
24287
|
};
|
|
23413
24288
|
}
|
|
23414
24289
|
async function atomicPrivateWrite2(path, value) {
|
|
23415
|
-
const temporary =
|
|
24290
|
+
const temporary = join16(dirname7(path), `.${basename5(path)}.${process.pid}.tmp`);
|
|
23416
24291
|
try {
|
|
23417
24292
|
await writeFile7(temporary, canonicalJson(value), { mode: 384 });
|
|
23418
24293
|
if (process.platform !== "win32") await chmod6(temporary, 384);
|
|
@@ -23424,9 +24299,9 @@ async function atomicPrivateWrite2(path, value) {
|
|
|
23424
24299
|
}
|
|
23425
24300
|
async function createReleasePlan(input, deps = {}) {
|
|
23426
24301
|
const workspace = resolve9(input.workspace);
|
|
23427
|
-
const output2 = resolve9(input.output ??
|
|
23428
|
-
const envelopePath =
|
|
23429
|
-
const artifactPath =
|
|
24302
|
+
const output2 = resolve9(input.output ?? join16(workspace, "release-plan.json"));
|
|
24303
|
+
const envelopePath = join16(dirname7(output2), "release-plan-envelope.json");
|
|
24304
|
+
const artifactPath = join16(dirname7(output2), "release-review.zip");
|
|
23430
24305
|
const loaded = await verifyReleaseWorkspace(workspace);
|
|
23431
24306
|
const assets = await desiredAssets(loaded.manifest, workspace);
|
|
23432
24307
|
const setup = await (deps.readConfig ?? readReleaseConfig)();
|
|
@@ -23990,13 +24865,13 @@ __export(releaseCommit_exports, {
|
|
|
23990
24865
|
validateReleaseCommitReceipt: () => validateReleaseCommitReceipt
|
|
23991
24866
|
});
|
|
23992
24867
|
import { readFile as readFile13 } from "node:fs/promises";
|
|
23993
|
-
import { join as
|
|
24868
|
+
import { join as join17, resolve as resolve10 } from "node:path";
|
|
23994
24869
|
async function commitApprovedRelease(input, deps = {}) {
|
|
23995
24870
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
23996
24871
|
const startedAt = now().toISOString();
|
|
23997
24872
|
const attemptRoot = resolve10(input.attemptRoot ?? attemptDirectoryOf(input.workspace));
|
|
23998
|
-
const receiptPath =
|
|
23999
|
-
const planId = await loosePlanIdAt(input.planPath ??
|
|
24873
|
+
const receiptPath = join17(attemptRoot, RECEIPT_FILENAME2);
|
|
24874
|
+
const planId = await loosePlanIdAt(input.planPath ?? join17(resolve10(input.workspace), PLAN_FILENAME2));
|
|
24000
24875
|
let foreign = planId ? await inspectForeignAttempt(attemptRoot, planId) : null;
|
|
24001
24876
|
if (foreign?.finished) {
|
|
24002
24877
|
const archived = await archiveForeignAttempt(attemptRoot, foreign, now);
|
|
@@ -24071,7 +24946,7 @@ async function commitApprovedRelease(input, deps = {}) {
|
|
|
24071
24946
|
durable = isReceipt(persisted, plan) ? persisted : receipt;
|
|
24072
24947
|
await authority.finalize(plan, attempt.attemptId, durable);
|
|
24073
24948
|
} else {
|
|
24074
|
-
await writePartialReceipt(
|
|
24949
|
+
await writePartialReceipt(join17(attemptRoot, PARTIAL_RECEIPT_FILENAME2), receipt);
|
|
24075
24950
|
}
|
|
24076
24951
|
await journal.record({
|
|
24077
24952
|
stage: "receipt",
|
|
@@ -24157,7 +25032,7 @@ async function runPreflight(input, deps, recovery) {
|
|
|
24157
25032
|
} catch (error) {
|
|
24158
25033
|
refuse2("workspace_corrupt", error.message);
|
|
24159
25034
|
}
|
|
24160
|
-
const plan = await readApprovedPlan(input.planPath ??
|
|
25035
|
+
const plan = await readApprovedPlan(input.planPath ?? join17(workspacePath, PLAN_FILENAME2));
|
|
24161
25036
|
if (plan.desired.desiredId !== workspace.manifest.desiredId) {
|
|
24162
25037
|
refuse2("plan_mismatch", "this plan was built from a different desired release than the workspace holds");
|
|
24163
25038
|
}
|
|
@@ -24325,7 +25200,7 @@ async function runStages(preflight, attemptId, journal, signal, deps, now) {
|
|
|
24325
25200
|
{
|
|
24326
25201
|
plan,
|
|
24327
25202
|
workspace: preflight.workspacePath,
|
|
24328
|
-
backup:
|
|
25203
|
+
backup: join17(preflight.attemptRoot, SCREENSHOT_BACKUP_DIRNAME)
|
|
24329
25204
|
},
|
|
24330
25205
|
{
|
|
24331
25206
|
token: preflight.appleToken,
|
|
@@ -24379,7 +25254,7 @@ async function runStages(preflight, attemptId, journal, signal, deps, now) {
|
|
|
24379
25254
|
}
|
|
24380
25255
|
function readSealedPayload(workspacePath) {
|
|
24381
25256
|
return async (reference) => {
|
|
24382
|
-
const bytes = await readFile13(
|
|
25257
|
+
const bytes = await readFile13(join17(workspacePath, ...reference.source.split("/")));
|
|
24383
25258
|
if (bytes.byteLength !== reference.bytes || sha256(bytes) !== reference.sha256) {
|
|
24384
25259
|
throw new Error("a sealed payload changed since this plan was approved");
|
|
24385
25260
|
}
|
|
@@ -24498,8 +25373,8 @@ ${receipt.remainingOwnerAction}` : ""
|
|
|
24498
25373
|
}
|
|
24499
25374
|
async function readReleaseCommitStatus(workspace, attemptRoot) {
|
|
24500
25375
|
const root = resolve10(attemptRoot ?? attemptDirectoryOf(workspace));
|
|
24501
|
-
const final = await readPersistedReceipt(
|
|
24502
|
-
const partial = isReceipt(final) ? null : await readPersistedReceipt(
|
|
25376
|
+
const final = await readPersistedReceipt(join17(root, RECEIPT_FILENAME2));
|
|
25377
|
+
const partial = isReceipt(final) ? null : await readPersistedReceipt(join17(root, PARTIAL_RECEIPT_FILENAME2));
|
|
24503
25378
|
const journals = await readLiveAttemptJournals(root);
|
|
24504
25379
|
return {
|
|
24505
25380
|
attemptRoot: root,
|
|
@@ -24870,7 +25745,7 @@ __export(releasePrepare_exports, {
|
|
|
24870
25745
|
runReleasePrepareCommand: () => runReleasePrepareCommand
|
|
24871
25746
|
});
|
|
24872
25747
|
import { readFile as readFile14 } from "node:fs/promises";
|
|
24873
|
-
import { isAbsolute as isAbsolute6, join as
|
|
25748
|
+
import { isAbsolute as isAbsolute6, join as join18, resolve as resolve11 } from "node:path";
|
|
24874
25749
|
function parseGcArgs(argv2) {
|
|
24875
25750
|
const values = /* @__PURE__ */ new Map();
|
|
24876
25751
|
let dryRun = false;
|
|
@@ -24978,7 +25853,7 @@ function parsePrepareArgs(argv2) {
|
|
|
24978
25853
|
project: project2,
|
|
24979
25854
|
repository,
|
|
24980
25855
|
...values.get("--bundle-id") ? { bundleId: values.get("--bundle-id").trim() } : {},
|
|
24981
|
-
...build ? { build: isAbsolute6(build) ? build :
|
|
25856
|
+
...build ? { build: isAbsolute6(build) ? build : join18(repository, build) } : {},
|
|
24982
25857
|
...values.get("--locales") ? { locales: csv(values.get("--locales"), "--locales") } : {},
|
|
24983
25858
|
...values.get("--outputs") ? { outputs: csv(values.get("--outputs"), "--outputs") } : {},
|
|
24984
25859
|
...values.get("--output") ? { destination: resolve11(values.get("--output")) } : {},
|
|
@@ -25985,7 +26860,7 @@ import {
|
|
|
25985
26860
|
writeFile as writeFile8
|
|
25986
26861
|
} from "node:fs/promises";
|
|
25987
26862
|
import { homedir as homedir3 } from "node:os";
|
|
25988
|
-
import { delimiter, dirname as
|
|
26863
|
+
import { delimiter, dirname as dirname8, extname as extname4, join as join19, resolve as resolve13 } from "node:path";
|
|
25989
26864
|
import { stdin, stdout } from "node:process";
|
|
25990
26865
|
import { createInterface } from "node:readline/promises";
|
|
25991
26866
|
function cleanIdentifier(value, label) {
|
|
@@ -25996,7 +26871,7 @@ function cleanIdentifier(value, label) {
|
|
|
25996
26871
|
return result;
|
|
25997
26872
|
}
|
|
25998
26873
|
function expandHome(path) {
|
|
25999
|
-
return path === "~" ? homedir3() : path.startsWith("~/") ?
|
|
26874
|
+
return path === "~" ? homedir3() : path.startsWith("~/") ? join19(homedir3(), path.slice(2)) : path;
|
|
26000
26875
|
}
|
|
26001
26876
|
async function validatePrivateKeyPath(keyPath) {
|
|
26002
26877
|
const requested = resolve13(expandHome(keyPath.trim()));
|
|
@@ -26031,7 +26906,7 @@ async function findExecutable(name, env = process.env) {
|
|
|
26031
26906
|
const extensions = process.platform === "win32" ? (env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
26032
26907
|
for (const dir of (env.PATH || "").split(delimiter).filter(Boolean)) {
|
|
26033
26908
|
for (const extension of extensions) {
|
|
26034
|
-
const candidate =
|
|
26909
|
+
const candidate = join19(dir, `${name}${extension}`);
|
|
26035
26910
|
try {
|
|
26036
26911
|
const info = await stat6(candidate);
|
|
26037
26912
|
if (!info.isFile()) continue;
|
|
@@ -26164,8 +27039,8 @@ async function probeAppleAccess(input, fetchImpl = fetch, apiOrigin = APP_STORE_
|
|
|
26164
27039
|
return { teams: [{ id: input.issuerId, apps }] };
|
|
26165
27040
|
}
|
|
26166
27041
|
function releaseConfigPath() {
|
|
26167
|
-
const dir = process.env.SHOTOPS_CONFIG_DIR ||
|
|
26168
|
-
return
|
|
27042
|
+
const dir = process.env.SHOTOPS_CONFIG_DIR || join19(homedir3(), ".shotops");
|
|
27043
|
+
return join19(dir, "release.json");
|
|
26169
27044
|
}
|
|
26170
27045
|
async function readReleaseConfig(target = releaseConfigPath()) {
|
|
26171
27046
|
let parsed;
|
|
@@ -26194,7 +27069,7 @@ async function writeReleaseConfig(input, target = releaseConfigPath()) {
|
|
|
26194
27069
|
keyPath: input.keyPath
|
|
26195
27070
|
}
|
|
26196
27071
|
};
|
|
26197
|
-
const dir =
|
|
27072
|
+
const dir = dirname8(target);
|
|
26198
27073
|
await mkdir6(dir, { recursive: true, mode: 448 });
|
|
26199
27074
|
if (process.platform !== "win32") await chmod7(dir, 448);
|
|
26200
27075
|
const temporary = `${target}.${process.pid}.tmp`;
|
|
@@ -26537,6 +27412,7 @@ wait, cancelled and cloud-only results.
|
|
|
26537
27412
|
});
|
|
26538
27413
|
|
|
26539
27414
|
// src/local.ts
|
|
27415
|
+
import { Console } from "node:console";
|
|
26540
27416
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
26541
27417
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
26542
27418
|
|
|
@@ -26563,6 +27439,7 @@ init_screenshotInput();
|
|
|
26563
27439
|
init_uploads();
|
|
26564
27440
|
init_resultContract();
|
|
26565
27441
|
init_renderCredits();
|
|
27442
|
+
init_browserInstall();
|
|
26566
27443
|
init_appstore();
|
|
26567
27444
|
init_productionOperationStart();
|
|
26568
27445
|
function dedupeLocales(list) {
|
|
@@ -26675,7 +27552,13 @@ function bundleProductionSnapshot(input) {
|
|
|
26675
27552
|
const locale2 = input.locales[0] ?? "en-US";
|
|
26676
27553
|
const projectFile = input.loaded?.projectFile;
|
|
26677
27554
|
const variant = projectFile ? projectRenderVariant(projectFile, locale2, input.panelPresetId ?? void 0) : void 0;
|
|
26678
|
-
const
|
|
27555
|
+
const variants = projectFile ? Array.from(new Map(
|
|
27556
|
+
(input.outputs.length > 0 ? input.outputs.map((id) => deviceById(id)?.panelPresetId) : [input.panelPresetId ?? void 0]).flatMap((preset) => input.locales.map((targetLocale) => {
|
|
27557
|
+
const target = projectRenderVariant(projectFile, targetLocale, preset);
|
|
27558
|
+
return [variantKey(target), target];
|
|
27559
|
+
}))
|
|
27560
|
+
).values()) : [];
|
|
27561
|
+
const sources = input.screens ? variants.flatMap((target) => plannedSourceCells(input.screens.plan(target))) : input.panelEntries && input.panelEntries.length > 0 ? input.panelSourceEntries ? input.locales.flatMap(
|
|
26679
27562
|
(targetLocale) => panelSourceCells(
|
|
26680
27563
|
input.panelSourceEntries.map(
|
|
26681
27564
|
(entry) => resolveLocaleVariant(entry, targetLocale)
|
|
@@ -26718,7 +27601,10 @@ function bundleProductionSnapshot(input) {
|
|
|
26718
27601
|
panelPresetId: input.panelPresetId
|
|
26719
27602
|
},
|
|
26720
27603
|
baseLocale: projectFile ? projectBaseLocale2(projectFile) : "en-US",
|
|
26721
|
-
...input.screens ? { gaps:
|
|
27604
|
+
...input.screens ? { gaps: [
|
|
27605
|
+
...repairGaps(input.screens.repairFindings),
|
|
27606
|
+
...projectCopyGaps(projectFile, input.locales, variant?.family ?? BASE_SCREEN_FAMILY)
|
|
27607
|
+
] } : {}
|
|
26722
27608
|
});
|
|
26723
27609
|
if (!resolved.ok) return resolved;
|
|
26724
27610
|
const readiness = requireReadiness({
|
|
@@ -26748,7 +27634,7 @@ async function handleEmitBundle(deps, args, signal) {
|
|
|
26748
27634
|
const scope = openChargeScope(deps);
|
|
26749
27635
|
const attempt = {};
|
|
26750
27636
|
try {
|
|
26751
|
-
return await runCharged(scope, () => emitBundle(deps, args, scope, attempt, signal));
|
|
27637
|
+
return await refuseOnRendererSetupFailure(() => runCharged(scope, () => emitBundle(deps, args, scope, attempt, signal)));
|
|
26752
27638
|
} finally {
|
|
26753
27639
|
finishProductionRender(attempt.access);
|
|
26754
27640
|
}
|
|
@@ -27223,7 +28109,8 @@ async function emitBundle(deps, args, scope, attempt, signal) {
|
|
|
27223
28109
|
unexpandedSavedNote(projectSizeCovered),
|
|
27224
28110
|
projectScreenUsageNote(result.shots ?? []),
|
|
27225
28111
|
projectStylingIgnoredNote(args),
|
|
27226
|
-
result.layoutNote
|
|
28112
|
+
result.layoutNote,
|
|
28113
|
+
result.setupNote
|
|
27227
28114
|
);
|
|
27228
28115
|
} else {
|
|
27229
28116
|
if (!screenshotSlots || screenshotSlots.length === 0) {
|
|
@@ -27318,7 +28205,7 @@ async function emitBundle(deps, args, scope, attempt, signal) {
|
|
|
27318
28205
|
previewPng = result.stripBase64 ? Buffer.from(result.stripBase64, "base64") : null;
|
|
27319
28206
|
projectRecord = result.projectRecord;
|
|
27320
28207
|
layout = result.layout;
|
|
27321
|
-
note = joinNotes(note, result.layoutNote);
|
|
28208
|
+
note = joinNotes(note, result.layoutNote, result.setupNote);
|
|
27322
28209
|
}
|
|
27323
28210
|
const declaredLocales = collectVariantLocales([...(screenshotSlots ?? []).flat(), ...args.panels ?? []]);
|
|
27324
28211
|
if (declaredLocales.length > 0) {
|
|
@@ -27442,6 +28329,7 @@ async function emitMultiOutputBundle(deps, args, outputs, locales, projectName2,
|
|
|
27442
28329
|
const projectShots = [];
|
|
27443
28330
|
let layout;
|
|
27444
28331
|
let layoutNote;
|
|
28332
|
+
let setupNote;
|
|
27445
28333
|
for (const output2 of outputs) {
|
|
27446
28334
|
const localeSlots = [];
|
|
27447
28335
|
for (const locale2 of locales) {
|
|
@@ -27466,6 +28354,7 @@ async function emitMultiOutputBundle(deps, args, outputs, locales, projectName2,
|
|
|
27466
28354
|
}))
|
|
27467
28355
|
});
|
|
27468
28356
|
if (!previewPng && result.stripBase64) previewPng = Buffer.from(result.stripBase64, "base64");
|
|
28357
|
+
setupNote ??= result.setupNote;
|
|
27469
28358
|
if (!layout && result.layout) {
|
|
27470
28359
|
layout = result.layout;
|
|
27471
28360
|
const scope2 = outputs.length > 1 || locales.length > 1 ? ` (measured on ${output2.label} / ${locale2})` : "";
|
|
@@ -27537,6 +28426,7 @@ async function emitMultiOutputBundle(deps, args, outputs, locales, projectName2,
|
|
|
27537
28426
|
projectScreenUsageNote(projectShots),
|
|
27538
28427
|
outputNote,
|
|
27539
28428
|
layoutNote,
|
|
28429
|
+
setupNote,
|
|
27540
28430
|
resolutionGapNote(plan.snapshot)
|
|
27541
28431
|
);
|
|
27542
28432
|
return textResult({
|
|
@@ -27598,7 +28488,9 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
|
|
|
27598
28488
|
const projectShots = [];
|
|
27599
28489
|
let layout;
|
|
27600
28490
|
let layoutNote;
|
|
28491
|
+
let setupNote;
|
|
27601
28492
|
const noteLayout = (result, loc) => {
|
|
28493
|
+
setupNote ??= result.setupNote;
|
|
27602
28494
|
if (layout || !result.layout) return;
|
|
27603
28495
|
layout = result.layout;
|
|
27604
28496
|
const scope2 = locales.length > 1 ? ` (measured on ${loc})` : "";
|
|
@@ -27704,7 +28596,7 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
|
|
|
27704
28596
|
});
|
|
27705
28597
|
if (!measuredReady.ok) return measuredReady.result;
|
|
27706
28598
|
const zip = buildMultiLocaleFastlaneBundleZip(localeSlots, { bundleId: args.bundleId, panelLabel });
|
|
27707
|
-
note = joinNotes(note, projectScreenUsageNote(projectShots), layoutNote);
|
|
28599
|
+
note = joinNotes(note, projectScreenUsageNote(projectShots), layoutNote, setupNote);
|
|
27708
28600
|
const filename2 = bundleZipFilename(projectName2);
|
|
27709
28601
|
const { mode: outputMode, note: outputNote } = resolveOutputMode(args.output, zip.byteLength, deps.forceOutput);
|
|
27710
28602
|
const storedZip = outputMode === "urls" ? await storeAsset(
|
|
@@ -28557,7 +29449,7 @@ async function handleDeleteAssets(deps, args) {
|
|
|
28557
29449
|
// src/runtime/widgets.ts
|
|
28558
29450
|
init_packagePaths();
|
|
28559
29451
|
import { readFileSync } from "node:fs";
|
|
28560
|
-
import { join as
|
|
29452
|
+
import { join as join3 } from "node:path";
|
|
28561
29453
|
var WIDGET_MIME = "text/html;profile=mcp-app";
|
|
28562
29454
|
var PANELS_WIDGET_URI = "ui://shotops/panels/v4.html";
|
|
28563
29455
|
var PANELS_WIDGET_URIS = [
|
|
@@ -28584,7 +29476,7 @@ function readBundle(name) {
|
|
|
28584
29476
|
if (cached !== void 0) return cached;
|
|
28585
29477
|
let bundle;
|
|
28586
29478
|
try {
|
|
28587
|
-
bundle = readFileSync(
|
|
29479
|
+
bundle = readFileSync(join3(WIDGET_DIST, name), "utf8");
|
|
28588
29480
|
} catch {
|
|
28589
29481
|
bundle = null;
|
|
28590
29482
|
}
|
|
@@ -28715,7 +29607,7 @@ function registerTools(server, deps) {
|
|
|
28715
29607
|
"render_strip",
|
|
28716
29608
|
{
|
|
28717
29609
|
title: "Render App Store screenshots",
|
|
28718
|
-
description: 'Make the store screenshots: raw app screen captures in, finished marketing panels out. Each screenshot is composited into a styled 3D device mockup, laid out as an App Store screenshot strip with the headlines you pass, and returned as per-panel PNGs at the App Store panel size `panelPresetId` names. This is the tool for a strip built from screenshots you have in hand; to reproduce one a designer already saved, use render_project. In ChatGPT, call import_screenshot first and pass the returned refs in the preferred flat form: { "screenshots": [{ "ref": "\u2026" }] } \u2014 each flat entry is one panel, and nested slots only when ONE panel should hold several phones. Art-direct it with `style`/`look` or a project\'s saved look, iterate with a low-res `preview` at a quarter the render cost, and take results `inline` or as `urls` for large payloads. Screenshots can vary per store locale via { "locales": \u2026 } entries \u2014 `locale` picks which variant renders (missing variants fall back to en-US). For real, full-resolution screenshots call request_screenshot_upload first \u2014 inline base64 is for small payloads only. CALL describe_look FIRST when nobody has said how the strip should look \u2014 it returns the five palette presets, the
|
|
29610
|
+
description: 'Make the store screenshots: raw app screen captures in, finished marketing panels out. Each screenshot is composited into a styled 3D device mockup, laid out as an App Store screenshot strip with the headlines you pass, and returned as per-panel PNGs at the App Store panel size `panelPresetId` names. This is the tool for a strip built from screenshots you have in hand; to reproduce one a designer already saved, use render_project. In ChatGPT, call import_screenshot first and pass the returned refs in the preferred flat form: { "screenshots": [{ "ref": "\u2026" }] } \u2014 each flat entry is one panel, and nested slots only when ONE panel should hold several phones. Art-direct it with `style`/`look` or a project\'s saved look, iterate with a low-res `preview` at a quarter the render cost, and take results `inline` or as `urls` for large payloads. Screenshots can vary per store locale via { "locales": \u2026 } entries \u2014 `locale` picks which variant renders (missing variants fall back to en-US). For real, full-resolution screenshots call request_screenshot_upload first \u2014 inline base64 is for small payloads only. CALL describe_look FIRST when nobody has said how the strip should look \u2014 it returns the five palette presets, the seven layout templates and every styling field with its default, and guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. PICK `style.layout` ONCE, BEFORE ANYTHING ELSE \u2014 off the EDGE the user asked for if they named one, otherwise off the LONGEST HEADLINE in the set: "standard" reserves 2 lines (~36 characters) with the whole device under them, "bleed" reserves 4 (~72) with the device running off the bottom edge, "top-bleed" inverts that. The side bleeds carry the device into the NEIGHBOURING screenshot so the set reads as one swipe \u2014 "lean-right" / "lean-left" put the headline in a narrow column beside it (~26 characters), "swipe" pushes a third of the device across and keeps the full-width headline, "corner-bleed" goes off the bottom and the right at once. A side bleed is for the WHOLE set: apply one to every panel or none, and never mirror it on neighbouring panels \u2014 two devices leaning toward each other overlap. The text region is RESERVED, NOT FITTED \u2014 held at full size whether or not the headline fills it, because a fixed reservation is the only thing that makes every device in a swiped SET land on the same line. Do not close a gap by shrinking vOffset on the short panels, and never assemble a composition out of raw phoneHeight/vOffset numbers: pick a layout, then override one field if you must (the template expands first, your field wins). Past the reservation the caption band grows down INTO the phone \u2014 the failure to avoid. THE RESPONSE ANSWERS BACK: an optional `layout` block names every caption sitting on a device, every caption collision and every device pair overlapping without reading as composed, by frame number and depth. When the repair is deterministic it also carries `layout.correctedLook` \u2014 pass that straight back as `look` to re-render clean. No `layout` key means the composition measured clean. Read it before you show the panels. This tool ONLY renders images: it never creates or updates a project, and the result says so \u2014 `contract.effects.projectMutation` is false on every render. A render with no `project` also carries a `nextStep` with action `offer_save_project`, which states that no project exists and names save_project as the door. That is an OFFER to put to the user, never consent already given: save only after they show they want this work preserved, shared, reused or kept editable, and if you cannot ask them, return the panels with the offer standing. No store credential is involved.',
|
|
28719
29611
|
inputSchema: renderStripAdvertisedShape,
|
|
28720
29612
|
outputSchema: renderStripToolOutputSchema,
|
|
28721
29613
|
// ADDITIVE, not destructive: this can mint private generated assets but cannot change a
|
|
@@ -28864,7 +29756,7 @@ function registerTools(server, deps) {
|
|
|
28864
29756
|
"describe_look",
|
|
28865
29757
|
{
|
|
28866
29758
|
title: "Design presets & style catalog",
|
|
28867
|
-
description: "Decide what the strip should LOOK like before rendering it. Returns five named starting-point presets (each bundling a background, device body, caption colour and typeface that agree with each other), a short two-question intake script for asking the user their vibe and their headlines, and the full styling field catalog + defaults (device/camera, background, captions, App Store panel sizes). CALL THIS BEFORE the first render of a new strip whenever the user has not said how it should look \u2014 guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. It is also the one complete copy of that material: the connect-time server instructions are truncated by most clients, so anything you need about PALETTE (presets: noir, daylight, pop, clay, studio \u2014 each bundling background + device body + caption colour + typeface so they agree) or COMPOSITION (layouts: standard, bleed, top-bleed \u2014 how many headline lines are reserved and where the device sits under them) is here and nowhere else in full. The two are independent axes and you usually want one of each: a preset never pins placement, a layout never pins colour. SKIP this call when the user already said what they want, when the project has a saved look (read_look / useSavedLook), or when you are iterating on a strip you already rendered.",
|
|
29759
|
+
description: "Decide what the strip should LOOK like before rendering it. Returns five named starting-point presets (each bundling a background, device body, caption colour and typeface that agree with each other), a short two-question intake script for asking the user their vibe and their headlines, and the full styling field catalog + defaults (device/camera, background, captions, App Store panel sizes). CALL THIS BEFORE the first render of a new strip whenever the user has not said how it should look \u2014 guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. It is also the one complete copy of that material: the connect-time server instructions are truncated by most clients, so anything you need about PALETTE (presets: noir, daylight, pop, clay, studio \u2014 each bundling background + device body + caption colour + typeface so they agree) or COMPOSITION (layouts: standard, bleed, top-bleed, lean-right, lean-left, swipe, corner-bleed \u2014 how many headline lines are reserved and where the device sits under them) is here and nowhere else in full. The two are independent axes and you usually want one of each: a preset never pins placement, a layout never pins colour. SKIP this call when the user already said what they want, when the project has a saved look (read_look / useSavedLook), or when you are iterating on a strip you already rendered.",
|
|
28868
29760
|
inputSchema: describeLookShape,
|
|
28869
29761
|
outputSchema: describeLookOutputSchema,
|
|
28870
29762
|
annotations: READ_ONLY,
|
|
@@ -29290,7 +30182,11 @@ function tokenFromArgv(argv2) {
|
|
|
29290
30182
|
return idx !== -1 ? argv2[idx + 1] : void 0;
|
|
29291
30183
|
}
|
|
29292
30184
|
var LOCAL_INTAKE = 'request_screenshot_upload is hosted-only; import_screenshot accepts { "path": "/abs/shot.png" } entries here (its { "url" } / { "file" } sources need a hosted account). Either way you can pass local files straight to render_strip / emit_bundle as { "path": "..." } screenshot entries.';
|
|
30185
|
+
function silenceStdoutConsole() {
|
|
30186
|
+
globalThis.console = new Console({ stdout: process.stderr, stderr: process.stderr });
|
|
30187
|
+
}
|
|
29293
30188
|
async function startStdioServer(token2) {
|
|
30189
|
+
silenceStdoutConsole();
|
|
29294
30190
|
const deps = localToolDeps(token2);
|
|
29295
30191
|
const localInstructions = token2 ? `${SERVER_INSTRUCTIONS}
|
|
29296
30192
|
|