mockpit 3.0.0 → 3.1.0
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 +11 -23
- package/bin/initDesign.js +11 -16
- package/bin/mockpit.js +242 -52
- package/bin/poster.js +64 -7
- package/dist/mcp/server.js +48 -6
- package/dist/server/apiViews.d.ts +562 -557
- package/dist/server/apiViews.js +18 -9
- package/dist/server/app.js +723 -206
- package/dist/server/cdnInline.js +6 -4
- package/dist/server/codeWorker.js +2 -2
- package/dist/server/contrast.d.ts +4 -0
- package/dist/server/contrast.js +157 -0
- package/dist/server/decisionList.d.ts +4 -1
- package/dist/server/decisionList.js +43 -5
- package/dist/server/designGuide.d.ts +13 -6
- package/dist/server/designGuide.js +206 -41
- package/dist/server/events.d.ts +6 -0
- package/dist/server/feedbackBatch.d.ts +4 -0
- package/dist/server/feedbackBatch.js +19 -1
- package/dist/server/guidance.d.ts +1 -1
- package/dist/server/guidance.js +2 -2
- package/dist/server/kits.d.ts +2 -2
- package/dist/server/kits.js +54 -1
- package/dist/server/mcpHttp.d.ts +2 -0
- package/dist/server/mcpHttp.js +13 -1
- package/dist/server/mcpSpec.d.ts +27 -27
- package/dist/server/mcpSpec.js +78 -10
- package/dist/server/run.js +22 -1
- package/dist/server/runApi.d.ts +2 -2
- package/dist/server/runApi.js +9 -2
- package/dist/server/sqlStore.d.ts +68 -39
- package/dist/server/sqlStore.js +163 -26
- package/dist/server/surfacePage.d.ts +4 -2
- package/dist/server/surfacePage.js +80 -32
- package/dist/server/system.d.ts +234 -0
- package/dist/server/system.js +1508 -0
- package/dist/server/systemChecks.d.ts +15 -0
- package/dist/server/systemChecks.js +224 -0
- package/dist/server/systemSpecimen.d.ts +2 -0
- package/dist/server/systemSpecimen.js +49 -0
- package/dist/server/themes.d.ts +9 -7
- package/dist/server/themes.js +43 -90
- package/dist/server/types.d.ts +43 -24
- package/dist/server/types.js +3 -1
- package/guide/AGENT_SETUP.md +3 -1
- package/guide/packs/foundations.md +35 -0
- package/guide/topics/asks.md +2 -1
- package/guide/topics/feedback.md +5 -3
- package/guide/topics/html.md +2 -2
- package/guide/topics/http.md +3 -1
- package/guide/topics/system.md +157 -0
- package/package.json +12 -12
- package/skills/mockpit/SKILL.md +7 -4
- package/viewer/dist/index.html +9 -8
package/dist/server/app.js
CHANGED
|
@@ -8,12 +8,16 @@ import { decodeBase64 } from "./base64.js";
|
|
|
8
8
|
import { draftHolds, designDetailView, postersFor, treeNodeView, designSummaryView, placeOf, sessionRowView, surfaceRef, } from "./apiViews.js";
|
|
9
9
|
import { askNudges } from "./askNudges.js";
|
|
10
10
|
import { EventBus } from "./events.js";
|
|
11
|
-
import { GUIDE_TOPICS, isGuideTopic, renderBriefGuide, unknownTopicMessage, } from "./designGuide.js";
|
|
11
|
+
import { GUIDE_TOPICS, isGuideTopic, renderBriefGuide, renderSystemTopic, unknownTopicMessage, } from "./designGuide.js";
|
|
12
12
|
import { buildFeedbackBatches } from "./feedbackBatch.js";
|
|
13
13
|
import { guidanceSections, MAX_RULES_CHARS, parseRulesFile, RULES_SETTING, USER_RULES_SETTING, } from "./guidance.js";
|
|
14
14
|
import { HANDOFF_NOTE, handoffReady, OWNER_BUSY_MS, ownerActiveAt, sessionName, supersedes, } from "./handoff.js";
|
|
15
15
|
import { bundledIconSets, expandIcons, iconCount, isIconName, mayHaveIcons, parseIconSet, resolverFor, } from "./icons.js";
|
|
16
16
|
import { checkProjectKit, KIT_IDS, kitSummaries } from "./kits.js";
|
|
17
|
+
import { activePalette, applySystemWrite, defaultPalette, describeChange, systemDiffers, emptySystem, restoreSystem, SYSTEM_WRITE_KEYS, PALETTE_STATUSES, hasSystemContent, projectKnobValues, systemAfterAnswers, systemExport, systemLine, } from "./system.js";
|
|
18
|
+
import { pageChecks, pairResults, purposeNudge, systemChecks } from "./systemChecks.js";
|
|
19
|
+
import { specimenHtml } from "./systemSpecimen.js";
|
|
20
|
+
import { checkCdnUrl } from "./cdn.js";
|
|
17
21
|
import { agentDecisionList, applyDecisionEdits, deriveDecisionLines, fullAgentDecisionList, parseDecisionEdits, } from "./decisionList.js";
|
|
18
22
|
import { chosenFills, decisionsFor, settledValues } from "./decisions.js";
|
|
19
23
|
import { checkAnswerFill, checkAnswerStyle, MAX_ANSWER_ADDITIONS, withAnswerAdditions, } from "./answerAdditions.js";
|
|
@@ -656,29 +660,10 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
656
660
|
const stored = await store.getSetting("mode");
|
|
657
661
|
return isMode(stored) ? stored : DEFAULT_MODE;
|
|
658
662
|
}
|
|
659
|
-
// A project's
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (!raw)
|
|
664
|
-
return null;
|
|
665
|
-
try {
|
|
666
|
-
const designSystem = JSON.parse(raw);
|
|
667
|
-
return {
|
|
668
|
-
...designSystem,
|
|
669
|
-
tailwindCss: typeof designSystem.tailwindCss === "string" ? designSystem.tailwindCss : "",
|
|
670
|
-
strippedImports: Array.isArray(designSystem.strippedImports)
|
|
671
|
-
? designSystem.strippedImports
|
|
672
|
-
: [],
|
|
673
|
-
iconSets: Array.isArray(designSystem.iconSets) ? designSystem.iconSets : [],
|
|
674
|
-
projectKits: Array.isArray(designSystem.projectKits) ? designSystem.projectKits : [],
|
|
675
|
-
designFiles: designSystem.designFiles ?? null,
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
catch {
|
|
679
|
-
return null;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
663
|
+
// A project's system, or null when nothing was ever written to it.
|
|
664
|
+
const systemFor = (project) => store.getSystem(project);
|
|
665
|
+
// The revision a version is published under; absent while there is none.
|
|
666
|
+
const systemRevOf = async (project) => (await store.getSystem(project))?.rev;
|
|
682
667
|
// Parsed installed icon sets by asset id. Asset bytes are immutable (the id
|
|
683
668
|
// is their hash), so an entry never goes stale; the bound only caps memory.
|
|
684
669
|
const iconSetCache = new Map();
|
|
@@ -699,9 +684,9 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
699
684
|
}
|
|
700
685
|
// The project's installed sets first, so installing a set under a bundled
|
|
701
686
|
// prefix (a newer lucide) overrides the bundled copy.
|
|
702
|
-
async function iconSetsFor(
|
|
687
|
+
async function iconSetsFor(system) {
|
|
703
688
|
const out = [];
|
|
704
|
-
for (const ref of
|
|
689
|
+
for (const ref of system?.iconSets ?? []) {
|
|
705
690
|
const set = await installedIconSet(ref.assetId);
|
|
706
691
|
if (set)
|
|
707
692
|
out.push({ prefix: ref.prefix, set, source: "installed" });
|
|
@@ -712,8 +697,8 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
712
697
|
}
|
|
713
698
|
return out;
|
|
714
699
|
}
|
|
715
|
-
async function iconResolverFor(
|
|
716
|
-
const sets = await iconSetsFor(
|
|
700
|
+
async function iconResolverFor(system) {
|
|
701
|
+
const sets = await iconSetsFor(system);
|
|
717
702
|
return { resolve: resolverFor(sets.map((s) => s.set)), prefixes: sets.map((s) => s.prefix) };
|
|
718
703
|
}
|
|
719
704
|
// Unknown icon names in a write's html surfaces, said at write time so the
|
|
@@ -722,7 +707,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
722
707
|
const html = surfaces.flatMap((s) => s.kind === "html" && mayHaveIcons(s.html) ? [s.html] : []);
|
|
723
708
|
if (html.length === 0)
|
|
724
709
|
return [];
|
|
725
|
-
const { resolve, prefixes } = await iconResolverFor(await
|
|
710
|
+
const { resolve, prefixes } = await iconResolverFor(await systemFor(project));
|
|
726
711
|
const unknown = new Set();
|
|
727
712
|
for (const h of html)
|
|
728
713
|
for (const u of expandIcons(h, resolve).unknown)
|
|
@@ -734,13 +719,18 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
734
719
|
async function briefFor(project) {
|
|
735
720
|
try {
|
|
736
721
|
const name = project ?? (await store.listProjects())[0]?.name ?? DEFAULT_PROJECT;
|
|
737
|
-
return renderBriefGuide(await
|
|
722
|
+
return renderBriefGuide(await systemFor(name), await guidanceFor(name), await componentIndex(name));
|
|
738
723
|
}
|
|
739
724
|
catch (err) {
|
|
740
725
|
console.warn("[mockpit] project design unavailable for the brief", err);
|
|
741
726
|
return renderBriefGuide(null);
|
|
742
727
|
}
|
|
743
728
|
}
|
|
729
|
+
// The `system` topic: its contract and this project's full lists.
|
|
730
|
+
async function systemTopicFor(project) {
|
|
731
|
+
const name = project || (await store.listProjects())[0]?.name || DEFAULT_PROJECT;
|
|
732
|
+
return renderSystemTopic(topics.system ?? "", await systemFor(name), await componentIndex(name));
|
|
733
|
+
}
|
|
744
734
|
// The user's file wins over the stored copy when the host can read it: the
|
|
745
735
|
// copy exists for hosts that can't.
|
|
746
736
|
async function userRulesText() {
|
|
@@ -861,7 +851,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
861
851
|
}
|
|
862
852
|
// The kit ids a surface of this project may name beyond the bundled ones.
|
|
863
853
|
const kitScope = async (project) => ({
|
|
864
|
-
projectKits: ((await
|
|
854
|
+
projectKits: ((await systemFor(project))?.projectKits ?? []).map((k) => k.id),
|
|
865
855
|
});
|
|
866
856
|
async function parseSurfaceInput(body, ctx, scope, base) {
|
|
867
857
|
if (body.html !== undefined) {
|
|
@@ -994,12 +984,24 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
994
984
|
if (!post || post.design !== design.id)
|
|
995
985
|
return fail(404, "variant not found");
|
|
996
986
|
const session = typeof body?.session === "string" ? body.session : "";
|
|
997
|
-
|
|
987
|
+
const rev = (await systemFor(design.project))?.rev ?? 0;
|
|
988
|
+
// A still mockpit took under an older system no longer shows; any agent's session
|
|
989
|
+
// may retake it, since a system write changes every variant's look at once.
|
|
990
|
+
const page = post.surfaces.find((s) => s.kind === "html");
|
|
991
|
+
const retake = page?.kind === "html" &&
|
|
992
|
+
!!page.poster &&
|
|
993
|
+
page.posterMode !== undefined &&
|
|
994
|
+
(page.posterSystem === undefined ? rev > 1 : page.posterSystem !== rev);
|
|
995
|
+
if (publisherOf(post) !== session &&
|
|
996
|
+
!(retake && session && (await store.getSession(session)))) {
|
|
998
997
|
return fail(403, `v${post.version} of ${post.variant} was published by another session`);
|
|
999
998
|
}
|
|
1000
999
|
if (body?.version !== post.version || (body?.rewrites ?? 0) !== (post.rewrites ?? 0)) {
|
|
1001
1000
|
return fail(409, `${post.variant} has changed since this still was taken`);
|
|
1002
1001
|
}
|
|
1002
|
+
if (body?.system !== undefined && body.system !== rev) {
|
|
1003
|
+
return fail(409, `${design.project}'s system has changed since this still was taken`);
|
|
1004
|
+
}
|
|
1003
1005
|
const poster = str(body?.poster, MAX_LABEL);
|
|
1004
1006
|
if (!poster)
|
|
1005
1007
|
return fail(400, '"poster" must be the asset id of an uploaded image');
|
|
@@ -1007,7 +1009,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
1007
1009
|
if (!asset?.contentType.startsWith("image/")) {
|
|
1008
1010
|
return fail(400, `poster "${poster}" is not an uploaded image`);
|
|
1009
1011
|
}
|
|
1010
|
-
const attached = await store.setPosterOnce(post.id, post.version, poster, isMode(body?.mode) ? body.mode : undefined);
|
|
1012
|
+
const attached = await store.setPosterOnce(post.id, post.version, poster, isMode(body?.mode) ? body.mode : undefined, rev);
|
|
1011
1013
|
if (!attached)
|
|
1012
1014
|
return fail(409, `${post.variant} already has a poster`);
|
|
1013
1015
|
bus.broadcast({ type: "design-updated", id: design.id, project: design.project });
|
|
@@ -1266,8 +1268,13 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
1266
1268
|
part: sl.part,
|
|
1267
1269
|
options: sl.fills.map((f) => ({ label: f.label ?? f.name, set: { [sl.part]: f.name } })),
|
|
1268
1270
|
}));
|
|
1271
|
+
const system = await systemFor(design.project);
|
|
1272
|
+
const pages = post.surfaces.flatMap((x) => (x.kind === "html" ? [x.html] : []));
|
|
1273
|
+
const purpose = design.kind === "component" ? purposeNudge(design.slug, design.notes?.text) : null;
|
|
1269
1274
|
const nudges = [
|
|
1270
1275
|
...(extra.nudges ?? []),
|
|
1276
|
+
...(system && pages.length ? pageChecks(system, pages) : []),
|
|
1277
|
+
...(purpose ? [purpose] : []),
|
|
1271
1278
|
...(unbound ? [unbound.nudge] : []),
|
|
1272
1279
|
...(slotAsks.length
|
|
1273
1280
|
? [
|
|
@@ -1420,6 +1427,13 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
1420
1427
|
session?.project ||
|
|
1421
1428
|
projectFromCwd(session?.cwd) ||
|
|
1422
1429
|
resolveProject(undefined, str(body.cwd, 4096)));
|
|
1430
|
+
{
|
|
1431
|
+
const projectKnobs = (await systemFor(project))?.knobs ?? {};
|
|
1432
|
+
const clash = [...Object.keys(knobs.value), ...Object.keys(variantKnobs.value)].find((p) => Object.hasOwn(projectKnobs, p));
|
|
1433
|
+
if (clash) {
|
|
1434
|
+
return fail(400, `knob "${clash}" is a project knob of ${project}'s system; name the design's knob something else`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1423
1437
|
let slug = design?.slug ?? slugify(slugIn);
|
|
1424
1438
|
design ??= await store.findDesign(project, slug);
|
|
1425
1439
|
// Told a design by its title, the agent means that design, not a new one named
|
|
@@ -1706,6 +1720,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
1706
1720
|
from,
|
|
1707
1721
|
prompt,
|
|
1708
1722
|
author,
|
|
1723
|
+
system: await systemRevOf(project),
|
|
1709
1724
|
});
|
|
1710
1725
|
if (!created)
|
|
1711
1726
|
return fail(404, "session not found");
|
|
@@ -1737,6 +1752,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
1737
1752
|
from,
|
|
1738
1753
|
prompt,
|
|
1739
1754
|
author,
|
|
1755
|
+
system: await systemRevOf(project),
|
|
1740
1756
|
});
|
|
1741
1757
|
if (!amended) {
|
|
1742
1758
|
return fail(409, `${variant} has a newer version than v${target.version}`, {
|
|
@@ -1785,6 +1801,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
1785
1801
|
prompt,
|
|
1786
1802
|
author,
|
|
1787
1803
|
...(session ? { publishedBy: session.id } : {}),
|
|
1804
|
+
system: await systemRevOf(project),
|
|
1788
1805
|
});
|
|
1789
1806
|
if (!updated)
|
|
1790
1807
|
return fail(404, "variant not found");
|
|
@@ -2171,7 +2188,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2171
2188
|
return out;
|
|
2172
2189
|
}
|
|
2173
2190
|
// --- the decision list ---
|
|
2174
|
-
const derivedDecisions = async (project, designs) => deriveDecisionLines(designs ?? (await store.listDesigns(project)), await store.listDecisionSources(project));
|
|
2191
|
+
const derivedDecisions = async (project, designs) => deriveDecisionLines(designs ?? (await store.listDesigns(project)), await store.listDecisionSources(project), await systemFor(project));
|
|
2175
2192
|
const decisionEdits = async (project) => parseDecisionEdits(await store.getSetting(`${DECISION_EDITS_SETTING}:${project}`));
|
|
2176
2193
|
const decisionLines = async (project, designs) => applyDecisionEdits(await derivedDecisions(project, designs), await decisionEdits(project));
|
|
2177
2194
|
// What every agent read carries, once the project has any: the first thing a
|
|
@@ -2227,7 +2244,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2227
2244
|
const project = str(query.project, MAX_TITLE);
|
|
2228
2245
|
const designs = await store.listDesigns(project);
|
|
2229
2246
|
const mode = await workspaceMode();
|
|
2230
|
-
const heads = headsByDesign(postersFor(await store.listPostHeads(project), mode));
|
|
2247
|
+
const heads = headsByDesign(postersFor(await store.listPostHeads(project), mode, project ? ((await systemFor(project))?.rev ?? 0) : undefined));
|
|
2231
2248
|
const rows = [];
|
|
2232
2249
|
const pending = [];
|
|
2233
2250
|
for (const m of designs) {
|
|
@@ -2236,8 +2253,12 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2236
2253
|
pending.push(pendingOf(m));
|
|
2237
2254
|
}
|
|
2238
2255
|
const open = rows.reduce((n, r) => n + r.open, 0);
|
|
2256
|
+
const system = project && !query.noDecisions ? await systemFor(project) : null;
|
|
2239
2257
|
return ok({
|
|
2240
2258
|
...(project ? { project, flows: await flowsView(project, designs) } : {}),
|
|
2259
|
+
...(system && hasSystemContent(system)
|
|
2260
|
+
? { system: systemLine(system, systemChecks(system).length) }
|
|
2261
|
+
: {}),
|
|
2241
2262
|
...(project && !query.noDecisions
|
|
2242
2263
|
? await decisionListField(project, undefined, designs)
|
|
2243
2264
|
: {}),
|
|
@@ -2257,7 +2278,9 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2257
2278
|
return fail(400, 'provide "project"');
|
|
2258
2279
|
const designs = await store.listDesigns(project);
|
|
2259
2280
|
const mode = await workspaceMode();
|
|
2260
|
-
const
|
|
2281
|
+
const system = await systemFor(project);
|
|
2282
|
+
const rev = system?.rev ?? 0;
|
|
2283
|
+
const heads = headsByDesign(postersFor(await store.listPostHeads(project), mode, rev));
|
|
2261
2284
|
const nodes = [];
|
|
2262
2285
|
const pending = [];
|
|
2263
2286
|
for (const m of designs) {
|
|
@@ -2265,7 +2288,9 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2265
2288
|
// Only embeds and fills need the html: most of a workspace's bytes are
|
|
2266
2289
|
// pages with neither, which this list has no reason to read.
|
|
2267
2290
|
const marked = designHeads.some((p) => p.surfaces.some((x) => x.marked));
|
|
2268
|
-
const posts = marked
|
|
2291
|
+
const posts = marked
|
|
2292
|
+
? postersFor(await store.listPosts({ designId: m.id }), mode, rev)
|
|
2293
|
+
: null;
|
|
2269
2294
|
const views = posts ? await embedViews(m, posts) : new Map();
|
|
2270
2295
|
const embeds = new Set();
|
|
2271
2296
|
for (const p of posts ?? []) {
|
|
@@ -2313,6 +2338,9 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2313
2338
|
}
|
|
2314
2339
|
return ok({
|
|
2315
2340
|
project,
|
|
2341
|
+
systemRev: rev,
|
|
2342
|
+
// Lights the System place in Home.
|
|
2343
|
+
hasSystem: hasSystemContent(system),
|
|
2316
2344
|
tree: nodes,
|
|
2317
2345
|
flows: await flowsView(project, designs),
|
|
2318
2346
|
areaOrder: await areaOrderOf(project),
|
|
@@ -2345,7 +2373,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2345
2373
|
const design = await resolveDesign(ref, query.project, query.session);
|
|
2346
2374
|
if (isResult(design))
|
|
2347
2375
|
return design;
|
|
2348
|
-
const posts = postersFor(await store.listPosts({ designId: design.id }), await workspaceMode());
|
|
2376
|
+
const posts = postersFor(await store.listPosts({ designId: design.id }), await workspaceMode(), (await systemFor(design.project))?.rev ?? 0);
|
|
2349
2377
|
return ok({
|
|
2350
2378
|
...(query.noDecisions ? {} : await decisionListField(design.project, design.slug)),
|
|
2351
2379
|
...designDetailView(design, posts, {
|
|
@@ -2355,6 +2383,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2355
2383
|
embeds: await embedViews(design, posts),
|
|
2356
2384
|
...(await shortlistReach(design)),
|
|
2357
2385
|
}),
|
|
2386
|
+
...(await systemPalettesField(design.project, query.viewer === true)),
|
|
2358
2387
|
pending: pendingOf(design),
|
|
2359
2388
|
});
|
|
2360
2389
|
}
|
|
@@ -2364,6 +2393,49 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2364
2393
|
const agent = await projectAgent(design.project);
|
|
2365
2394
|
return agent ? { repliesGoTo: agent.title ?? agent.agent } : {};
|
|
2366
2395
|
}
|
|
2396
|
+
// What a design view says about the system its frames wear: the revision, and for
|
|
2397
|
+
// the viewer the palettes to switch between (each a swatch of its first roles).
|
|
2398
|
+
async function systemPalettesField(project, viewer) {
|
|
2399
|
+
const system = await systemFor(project);
|
|
2400
|
+
const roles = (system?.color.roles ?? []).slice(0, 4).map((r) => r.name);
|
|
2401
|
+
return {
|
|
2402
|
+
systemRev: system?.rev ?? 0,
|
|
2403
|
+
...(viewer && system && system.color.palettes.length > 1
|
|
2404
|
+
? {
|
|
2405
|
+
palettes: system.color.palettes.map((p) => ({
|
|
2406
|
+
id: p.id,
|
|
2407
|
+
label: p.label || p.id,
|
|
2408
|
+
status: p.status,
|
|
2409
|
+
swatch: {
|
|
2410
|
+
light: roles.map((r) => p.light[r] ?? "transparent"),
|
|
2411
|
+
dark: roles.map((r) => p.dark[r] ?? "transparent"),
|
|
2412
|
+
},
|
|
2413
|
+
})),
|
|
2414
|
+
}
|
|
2415
|
+
: {}),
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
// Without a design, export hands the project's system to code.
|
|
2419
|
+
async function exportSystemFlow(projectArg) {
|
|
2420
|
+
const project = str(projectArg, MAX_TITLE);
|
|
2421
|
+
if (!project)
|
|
2422
|
+
return fail(400, 'provide "project", or "design" to export one');
|
|
2423
|
+
const system = await systemFor(project);
|
|
2424
|
+
if (!system || !hasSystemContent(system)) {
|
|
2425
|
+
return fail(404, `${project} has no system yet; start one with \`system\``);
|
|
2426
|
+
}
|
|
2427
|
+
const designs = await store.listDesigns(project);
|
|
2428
|
+
return ok({
|
|
2429
|
+
...systemExport(system),
|
|
2430
|
+
components: designs
|
|
2431
|
+
.filter((d) => d.kind === "component" && !d.archivedAt)
|
|
2432
|
+
.map((d) => ({
|
|
2433
|
+
slug: d.slug,
|
|
2434
|
+
purpose: (d.notes?.text ?? "").split("\n")[0].trim(),
|
|
2435
|
+
source: d.source ?? null,
|
|
2436
|
+
})),
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2367
2439
|
async function exportFlow(ref, query, ctx) {
|
|
2368
2440
|
const design = await resolveDesign(ref, query.project);
|
|
2369
2441
|
if (isResult(design))
|
|
@@ -2451,7 +2523,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2451
2523
|
});
|
|
2452
2524
|
}
|
|
2453
2525
|
// --- asks ---
|
|
2454
|
-
function sanitizeAsk(raw, design, posts, taken) {
|
|
2526
|
+
function sanitizeAsk(raw, design, posts, taken, palettes = new Set()) {
|
|
2455
2527
|
if (!raw || typeof raw !== "object")
|
|
2456
2528
|
return "each ask must be an object";
|
|
2457
2529
|
const text = str(raw.text, MAX_COMMENT_TEXT);
|
|
@@ -2524,6 +2596,17 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2524
2596
|
set = checked.value;
|
|
2525
2597
|
}
|
|
2526
2598
|
const pick = opt.recommended === true ? { recommended: true } : {};
|
|
2599
|
+
let palette;
|
|
2600
|
+
if (opt.palette !== undefined) {
|
|
2601
|
+
palette = str(opt.palette, 48);
|
|
2602
|
+
if (!palette || !palettes.has(palette)) {
|
|
2603
|
+
return `option "${label}" names palette "${String(opt.palette)}", which the project's system doesn't have${palettes.size ? ` (${[...palettes].join(", ")})` : ""}`;
|
|
2604
|
+
}
|
|
2605
|
+
if (variant || opt.version !== undefined)
|
|
2606
|
+
return `option "${label}": a palette option pictures every variant; name no variant`;
|
|
2607
|
+
if (scope === "part")
|
|
2608
|
+
return `option "${label}": a palette is the whole page's; ask with scope "design" or "state"`;
|
|
2609
|
+
}
|
|
2527
2610
|
if (opt.version !== undefined) {
|
|
2528
2611
|
const pinned = versionOption(opt.version, label, variant, { scope, state }, design, posts);
|
|
2529
2612
|
if (typeof pinned === "string")
|
|
@@ -2536,6 +2619,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2536
2619
|
label,
|
|
2537
2620
|
...(variant ? { variant } : {}),
|
|
2538
2621
|
...(set ? { set } : {}),
|
|
2622
|
+
...(palette ? { palette } : {}),
|
|
2539
2623
|
...pick,
|
|
2540
2624
|
});
|
|
2541
2625
|
}
|
|
@@ -2603,9 +2687,16 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2603
2687
|
// Takes what answers bring off their options (style, fill, html), checked before
|
|
2604
2688
|
// anything is written. Options then carry only what any option does: a `set`, or a
|
|
2605
2689
|
// `variant` whose `version` is filled in once its whole page exists.
|
|
2606
|
-
function answersBringing(raw, design, posts) {
|
|
2690
|
+
function answersBringing(raw, design, posts, system) {
|
|
2607
2691
|
const knobs = { ...design.knobs };
|
|
2608
2692
|
const additions = [...design.answerAdditions];
|
|
2693
|
+
const project = {
|
|
2694
|
+
knobs: { ...system?.knobs },
|
|
2695
|
+
additions: [...(system?.additions ?? [])],
|
|
2696
|
+
changed: false,
|
|
2697
|
+
};
|
|
2698
|
+
const nextRev = (system?.rev ?? 0) + 1;
|
|
2699
|
+
const revised = [];
|
|
2609
2700
|
const pages = [];
|
|
2610
2701
|
const renders = posts.flatMap((p) => (postAtVersion(p, currentVersion(p)) ?? p).surfaces.flatMap((s) => s.kind === "html" ? [s.html] : []));
|
|
2611
2702
|
const entries = [];
|
|
@@ -2650,6 +2741,36 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2650
2741
|
return `option "${name}": style and fill go with a set of one new value`;
|
|
2651
2742
|
}
|
|
2652
2743
|
const knob = set[0][0];
|
|
2744
|
+
if (system && Object.hasOwn(system.knobs, knob)) {
|
|
2745
|
+
if (fill !== undefined)
|
|
2746
|
+
return `option "${name}": "${knob}" is a project knob; it takes style, not fill`;
|
|
2747
|
+
const bad = typeof style === "string" ? checkAnswerStyle(style) : "style must be a string";
|
|
2748
|
+
if (bad)
|
|
2749
|
+
return `option "${name}": ${bad}`;
|
|
2750
|
+
const at = project.additions.findIndex((a) => a.knob === knob && a.value === value);
|
|
2751
|
+
if (at >= 0) {
|
|
2752
|
+
// Asked again with a new style, the value stays and its style is revised.
|
|
2753
|
+
if (project.additions[at].style !== style) {
|
|
2754
|
+
project.additions[at] = { knob, value, style, rev: nextRev };
|
|
2755
|
+
project.changed = true;
|
|
2756
|
+
revised.push(`${knob}=${value}`);
|
|
2757
|
+
}
|
|
2758
|
+
options.push(rest);
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
const config = explicitKnob(system.knobs[knob]);
|
|
2762
|
+
if (config.type !== "select") {
|
|
2763
|
+
return `option "${name}": project knob "${knob}" is a ${config.type}, which already carries its value; send the set alone`;
|
|
2764
|
+
}
|
|
2765
|
+
const known = (config.options ?? []).map((v) => typeof v === "object" && v !== null && "value" in v ? String(v.value) : String(v));
|
|
2766
|
+
if (!known.includes(value)) {
|
|
2767
|
+
project.knobs[knob] = { ...config, options: [...(config.options ?? []), value] };
|
|
2768
|
+
}
|
|
2769
|
+
project.additions.push({ knob, value, style, rev: nextRev });
|
|
2770
|
+
project.changed = true;
|
|
2771
|
+
options.push(rest);
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2653
2774
|
const addition = { knob, value };
|
|
2654
2775
|
if (style !== undefined) {
|
|
2655
2776
|
const bad = typeof style === "string" ? checkAnswerStyle(style) : "style must be a string";
|
|
@@ -2668,11 +2789,14 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2668
2789
|
}
|
|
2669
2790
|
addition.fill = fill;
|
|
2670
2791
|
}
|
|
2671
|
-
const
|
|
2672
|
-
if (
|
|
2673
|
-
// Asking again with what is already there is how a question grows
|
|
2792
|
+
const at = additions.findIndex((a) => a.knob === knob && a.value === value);
|
|
2793
|
+
if (at >= 0) {
|
|
2794
|
+
// Asking again with what is already there is how a question grows; with a
|
|
2795
|
+
// new style or fill, the value keeps its name and what it draws is revised.
|
|
2796
|
+
const same = additions[at];
|
|
2674
2797
|
if (same.style !== addition.style || same.fill !== addition.fill) {
|
|
2675
|
-
|
|
2798
|
+
additions[at] = { ...addition, rev: (same.rev ?? 1) + 1 };
|
|
2799
|
+
revised.push(`${knob}=${value}`);
|
|
2676
2800
|
}
|
|
2677
2801
|
options.push(rest);
|
|
2678
2802
|
continue;
|
|
@@ -2697,7 +2821,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2697
2821
|
}
|
|
2698
2822
|
entries.push({ ...entry, options });
|
|
2699
2823
|
}
|
|
2700
|
-
return { entries, knobs, additions, pages };
|
|
2824
|
+
return { entries, knobs, additions, pages, project, revised };
|
|
2701
2825
|
}
|
|
2702
2826
|
// A whole page an answer brought: a version the user only meets if they choose it.
|
|
2703
2827
|
// Asked again with the same page, the question keeps the version it already has.
|
|
@@ -2724,6 +2848,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2724
2848
|
embeds,
|
|
2725
2849
|
from: seen.version,
|
|
2726
2850
|
...(sessionId ? { publishedBy: sessionId } : {}),
|
|
2851
|
+
system: await systemRevOf(design.project),
|
|
2727
2852
|
});
|
|
2728
2853
|
if (!updated)
|
|
2729
2854
|
return fail(404, "variant not found");
|
|
@@ -2774,9 +2899,13 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2774
2899
|
if (raw.length > MAX_ASKS)
|
|
2775
2900
|
return fail(400, `at most ${MAX_ASKS} asks per call`);
|
|
2776
2901
|
const posts = await store.listPosts({ designId: design.id });
|
|
2777
|
-
const
|
|
2902
|
+
const system = await systemFor(design.project);
|
|
2903
|
+
const brought = answersBringing(raw, design, posts, system);
|
|
2778
2904
|
if (typeof brought === "string")
|
|
2779
2905
|
return fail(400, brought);
|
|
2906
|
+
// Options may set the project's knobs too, and choose its palettes.
|
|
2907
|
+
const askKnobs = { ...brought.project.knobs, ...brought.knobs };
|
|
2908
|
+
const palettes = new Set((system?.color.palettes ?? []).map((p) => p.id));
|
|
2780
2909
|
const asks = [...design.asks];
|
|
2781
2910
|
const taken = new Set(asks.map((a) => a.id));
|
|
2782
2911
|
const added = [];
|
|
@@ -2784,7 +2913,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2784
2913
|
const standings = { ...design.standings };
|
|
2785
2914
|
const stillHidden = [];
|
|
2786
2915
|
for (const entry of brought.entries) {
|
|
2787
|
-
const ask = sanitizeAsk(entry, { ...design, knobs:
|
|
2916
|
+
const ask = sanitizeAsk(entry, { ...design, knobs: askKnobs }, posts, taken, palettes);
|
|
2788
2917
|
if (typeof ask === "string")
|
|
2789
2918
|
return fail(400, ask);
|
|
2790
2919
|
const at = asks.findIndex((a) => a.id === ask.id);
|
|
@@ -2814,7 +2943,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2814
2943
|
// would wait where they no longer look. Asked again, a question it left
|
|
2815
2944
|
// unanswered is simply open again: guidance about what to ask lives in the
|
|
2816
2945
|
// conversation, not in a refusal here.
|
|
2817
|
-
const additionsGrew = brought.additions.length > design.answerAdditions.length;
|
|
2946
|
+
const additionsGrew = brought.additions.length > design.answerAdditions.length || brought.revised.length > 0;
|
|
2818
2947
|
const updated = await store.updateDesign(design.id, {
|
|
2819
2948
|
asks,
|
|
2820
2949
|
sessionId,
|
|
@@ -2828,6 +2957,41 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2828
2957
|
// Every render carries these now, and frames key on `rewrites`: open ones reload.
|
|
2829
2958
|
if (additionsGrew)
|
|
2830
2959
|
await store.bumpRewrites(design.id);
|
|
2960
|
+
if (system && brought.project.changed) {
|
|
2961
|
+
const made = brought.project.additions.filter((a) => a.rev === system.rev + 1);
|
|
2962
|
+
const summary = `answer styles for ${made.map((a) => `${a.knob}=${a.value}`).join(", ")}`;
|
|
2963
|
+
await serialSystem(design.project, async () => {
|
|
2964
|
+
// Another write may have landed since the read above: lay these over it.
|
|
2965
|
+
const fresh = (await systemFor(design.project)) ?? system;
|
|
2966
|
+
const rev = fresh.rev + 1;
|
|
2967
|
+
const key = (a) => `${a.knob}\u0000${a.value}`;
|
|
2968
|
+
const ours = new Set(made.map(key));
|
|
2969
|
+
const knobs = { ...fresh.knobs };
|
|
2970
|
+
for (const [path, config] of Object.entries(brought.project.knobs)) {
|
|
2971
|
+
if (JSON.stringify(config) !== JSON.stringify(system.knobs[path]))
|
|
2972
|
+
knobs[path] = config;
|
|
2973
|
+
}
|
|
2974
|
+
const next = {
|
|
2975
|
+
...fresh,
|
|
2976
|
+
knobs,
|
|
2977
|
+
additions: [
|
|
2978
|
+
...fresh.additions.filter((a) => !ours.has(key(a))),
|
|
2979
|
+
...made.map((a) => ({ ...a, rev })),
|
|
2980
|
+
],
|
|
2981
|
+
rev,
|
|
2982
|
+
updatedAt: new Date().toISOString(),
|
|
2983
|
+
};
|
|
2984
|
+
const saved = await store.putSystem(next, { sessionId: sessionId ?? null, summary });
|
|
2985
|
+
clearRenderCache();
|
|
2986
|
+
bus.broadcast({
|
|
2987
|
+
type: "system-changed",
|
|
2988
|
+
project: design.project,
|
|
2989
|
+
rev: saved.rev,
|
|
2990
|
+
summary,
|
|
2991
|
+
by: "agent",
|
|
2992
|
+
});
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2831
2995
|
bus.broadcast({ type: "design-updated", id: design.id, project: design.project });
|
|
2832
2996
|
// The questions also land in the thread, so the user reads them where they answer.
|
|
2833
2997
|
const session = sessionId ? await store.getSession(sessionId) : null;
|
|
@@ -2858,6 +3022,21 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
2858
3022
|
...(stillHidden.length
|
|
2859
3023
|
? [`The user hid ${stillHidden.join(", ")}: they stay folded away. Offer something else.`]
|
|
2860
3024
|
: []),
|
|
3025
|
+
...(brought.revised.length
|
|
3026
|
+
? [
|
|
3027
|
+
`revised what ${brought.revised.join(", ")} draw${brought.revised.length === 1 ? "s" : ""}: the value keeps its name, every render wearing it changes`,
|
|
3028
|
+
]
|
|
3029
|
+
: []),
|
|
3030
|
+
...(system
|
|
3031
|
+
? added.flatMap((a) => a.options.flatMap((o) => {
|
|
3032
|
+
const style = raw
|
|
3033
|
+
.flatMap((e) => e?.options ?? [])
|
|
3034
|
+
.find((x) => x?.label === o.label)?.style;
|
|
3035
|
+
return typeof style === "string"
|
|
3036
|
+
? pageChecks(system, [`<style>${style}</style>`], `option "${o.label}"'s style`)
|
|
3037
|
+
: [];
|
|
3038
|
+
}))
|
|
3039
|
+
: []),
|
|
2861
3040
|
];
|
|
2862
3041
|
return ok({
|
|
2863
3042
|
design: design.slug,
|
|
@@ -3078,7 +3257,8 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
3078
3257
|
const borrowed = sanitizeBorrowed(pick("borrowed"), posts);
|
|
3079
3258
|
if (typeof borrowed === "string")
|
|
3080
3259
|
return borrowed;
|
|
3081
|
-
const
|
|
3260
|
+
const projectKnobs = (await systemFor(design.project))?.knobs;
|
|
3261
|
+
const tuned = checkKnobValues(pick("tuned"), { knobs: { ...projectKnobs, ...design.knobs } }, posts, "tuned");
|
|
3082
3262
|
if (!tuned.ok)
|
|
3083
3263
|
return tuned.error;
|
|
3084
3264
|
const comments = sanitizePartComments(pick("comments"), design, posts);
|
|
@@ -3377,26 +3557,59 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
3377
3557
|
const standings = sentStandings(design, draft.answers);
|
|
3378
3558
|
if (Object.keys(standings.payload).length)
|
|
3379
3559
|
payload.standings = standings.payload;
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3560
|
+
// Serialized with the project's other system writes: the revision it makes is
|
|
3561
|
+
// built on the one it read.
|
|
3562
|
+
const { comment, systemWrite } = await serialSystem(design.project, async () => {
|
|
3563
|
+
const system = await systemFor(design.project);
|
|
3564
|
+
const decided = system
|
|
3565
|
+
? systemAfterAnswers(system, design.asks, draft.answers, standings.payload)
|
|
3566
|
+
: null;
|
|
3567
|
+
const systemWrite = decided
|
|
3568
|
+
? {
|
|
3569
|
+
next: { ...decided.system, rev: system.rev + 1, updatedAt: new Date().toISOString() },
|
|
3570
|
+
summary: decided.changes.join("; "),
|
|
3571
|
+
}
|
|
3572
|
+
: undefined;
|
|
3573
|
+
if (systemWrite) {
|
|
3574
|
+
payload.system = {
|
|
3575
|
+
project: design.project,
|
|
3576
|
+
rev: systemWrite.next.rev,
|
|
3577
|
+
changes: decided.changes,
|
|
3578
|
+
};
|
|
3579
|
+
}
|
|
3580
|
+
if (verdict) {
|
|
3581
|
+
const target = posts.find((p) => p.state === verdict.state && p.variant === verdict.variant);
|
|
3582
|
+
const pinned = verdict.kind === "use" && target ? flips.accept.get(target.id) : undefined;
|
|
3583
|
+
payload.verdict = pinned === undefined ? verdict : { ...verdict, version: pinned };
|
|
3584
|
+
}
|
|
3585
|
+
const comment = await store.commitReply({
|
|
3586
|
+
designId: design.id,
|
|
3587
|
+
sessionId,
|
|
3588
|
+
text: text ?? "",
|
|
3589
|
+
payload,
|
|
3590
|
+
asks,
|
|
3591
|
+
accept: [...flips.accept].map(([id, version]) => ({ id, version })),
|
|
3592
|
+
archive: [...flips.archive],
|
|
3593
|
+
release: [...flips.release],
|
|
3594
|
+
standings: standings.next,
|
|
3595
|
+
...(routed ? { owner: sessionId } : {}),
|
|
3596
|
+
...(systemWrite ? { system: systemWrite } : {}),
|
|
3597
|
+
});
|
|
3598
|
+
return { comment, systemWrite };
|
|
3396
3599
|
});
|
|
3397
3600
|
if (!comment)
|
|
3398
3601
|
return fail(409, `${design.slug} has no agent session to reply to`);
|
|
3399
3602
|
announceComment(comment);
|
|
3603
|
+
if (systemWrite) {
|
|
3604
|
+
clearRenderCache();
|
|
3605
|
+
bus.broadcast({
|
|
3606
|
+
type: "system-changed",
|
|
3607
|
+
project: design.project,
|
|
3608
|
+
rev: systemWrite.next.rev,
|
|
3609
|
+
summary: systemWrite.summary,
|
|
3610
|
+
by: "user",
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3400
3613
|
bus.broadcast({ type: "design-updated", id: design.id, project: design.project });
|
|
3401
3614
|
bus.broadcast({ type: "draft-updated", designId: design.id });
|
|
3402
3615
|
void announceEmbedders(design).catch((err) => console.warn("[mockpit] embedders", err));
|
|
@@ -3647,7 +3860,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
3647
3860
|
}))
|
|
3648
3861
|
: [],
|
|
3649
3862
|
};
|
|
3650
|
-
const asks = design.asks.map(({ dismissed:
|
|
3863
|
+
const asks = design.asks.map(({ dismissed: _dismissed, ...a }) => a);
|
|
3651
3864
|
const updated = await store.updateDesign(design.id, { asks, archivedAt: null });
|
|
3652
3865
|
if (!updated)
|
|
3653
3866
|
return fail(404, "design not found");
|
|
@@ -4183,6 +4396,11 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
4183
4396
|
...(post.description ? { description: post.description } : {}),
|
|
4184
4397
|
embeds,
|
|
4185
4398
|
copiedFrom: { design: design.id, state: post.state, variant: post.variant, version },
|
|
4399
|
+
// A copy looks as its original did.
|
|
4400
|
+
contract: postAtVersion(post, version)?.contract ?? 1,
|
|
4401
|
+
...(postAtVersion(post, version)?.system === undefined
|
|
4402
|
+
? {}
|
|
4403
|
+
: { system: postAtVersion(post, version).system }),
|
|
4186
4404
|
});
|
|
4187
4405
|
if (!created)
|
|
4188
4406
|
return fail(409, `${post.variant} has no agent session to copy it under`);
|
|
@@ -4636,6 +4854,9 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
4636
4854
|
from: version,
|
|
4637
4855
|
prompt: `restored v${version}`,
|
|
4638
4856
|
author: "user",
|
|
4857
|
+
// Restored, it looks as it did.
|
|
4858
|
+
contract: old.contract ?? 1,
|
|
4859
|
+
...(old.system === undefined ? {} : { system: old.system }),
|
|
4639
4860
|
});
|
|
4640
4861
|
if (!updated)
|
|
4641
4862
|
return fail(404, "variant not found");
|
|
@@ -4760,6 +4981,9 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
4760
4981
|
// Flows are declared apart from kind, so a page that stops being one stays
|
|
4761
4982
|
// in them until the agent takes it out.
|
|
4762
4983
|
const nudges = [...titleNudges(title)];
|
|
4984
|
+
const purpose = next.kind === "component" ? purposeNudge(next.slug, next.notes?.text) : null;
|
|
4985
|
+
if (purpose)
|
|
4986
|
+
nudges.push(purpose);
|
|
4763
4987
|
if (notesKept) {
|
|
4764
4988
|
nudges.push("The user edited these notes by hand, so your draft was not applied: work from theirs, in notes.text. They stand until the user clears them");
|
|
4765
4989
|
}
|
|
@@ -5670,6 +5894,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
5670
5894
|
body: flag(c, "body"),
|
|
5671
5895
|
history: flag(c, "history"),
|
|
5672
5896
|
noDecisions: withoutDecisions(c),
|
|
5897
|
+
viewer: isViewerRequest(c.req.raw),
|
|
5673
5898
|
}));
|
|
5674
5899
|
});
|
|
5675
5900
|
app.get("/api/designs/:id/export", async (c) => {
|
|
@@ -5844,6 +6069,8 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
5844
6069
|
return c.text(withOrigin(await briefFor(c.req.query("project")), c));
|
|
5845
6070
|
if (!isGuideTopic(topic))
|
|
5846
6071
|
return c.json({ error: unknownTopicMessage(topic), topics: GUIDE_TOPICS }, 400);
|
|
6072
|
+
if (topic === "system")
|
|
6073
|
+
return c.text(withOrigin(await systemTopicFor(c.req.query("project")), c));
|
|
5847
6074
|
return c.text(withOrigin(topics[topic] ?? "", c));
|
|
5848
6075
|
});
|
|
5849
6076
|
// Opt-in html kits available on this workspace (id, label, summary, classes) —
|
|
@@ -5851,7 +6078,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
5851
6078
|
// `?project=`, that project's own kits follow the bundled ones.
|
|
5852
6079
|
app.get("/api/kits", async (c) => {
|
|
5853
6080
|
const project = c.req.query("project");
|
|
5854
|
-
return c.json(kitSummaries(project ? ((await
|
|
6081
|
+
return c.json(kitSummaries(project ? ((await systemFor(project))?.projectKits ?? []) : []));
|
|
5855
6082
|
});
|
|
5856
6083
|
// --- theme: one palette, so the workspace setting is just its mode ---
|
|
5857
6084
|
app.get("/api/theme", async (c) => c.json({ mode: await workspaceMode() }));
|
|
@@ -5864,8 +6091,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
5864
6091
|
bus.broadcast({ type: "theme-changed", mode });
|
|
5865
6092
|
return c.json({ mode });
|
|
5866
6093
|
});
|
|
5867
|
-
// The project's
|
|
5868
|
-
// every html surface of the project (see renderHtmlPage).
|
|
6094
|
+
// The project's decision list (server/decisionList.ts).
|
|
5869
6095
|
app.get("/api/projects/:name/decisions", async (c) => send(c, await decisionsFlow({ project: c.req.param("name") }, flowCtx(c))));
|
|
5870
6096
|
app.put("/api/projects/:name/decisions", async (c) => send(c, await editDecisionFlow(c.req.param("name"), await jsonBody(c), flowCtx(c))));
|
|
5871
6097
|
app.get("/api/projects/:name/tree", async (c) => send(c, await treeFlow({
|
|
@@ -5875,153 +6101,402 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
5875
6101
|
})));
|
|
5876
6102
|
app.get("/api/projects/:name/flows", async (c) => send(c, await flowOutlineFlow(c.req.param("name"))));
|
|
5877
6103
|
app.patch("/api/projects/:name", async (c) => send(c, await editProjectFlow(c.req.param("name"), await jsonBody(c), flowCtx(c))));
|
|
5878
|
-
app.get("/api/projects/:name/
|
|
6104
|
+
app.get("/api/projects/:name/system", async (c) => send(c, await systemFlow(c.req.param("name"), undefined, flowCtx(c))));
|
|
6105
|
+
app.put("/api/projects/:name/system", async (c) => send(c, await systemFlow(c.req.param("name"), await jsonBody(c), flowCtx(c))));
|
|
6106
|
+
app.post("/api/projects/:name/system/decide", async (c) => send(c, await systemDecideFlow(c.req.param("name"), await jsonBody(c), flowCtx(c))));
|
|
6107
|
+
// The System page's specimen: the scales in the project's fonts, as a sandboxed
|
|
6108
|
+
// document. Addressed by project name, so it sits under /api (never public),
|
|
6109
|
+
// not /s/.
|
|
6110
|
+
app.get("/api/projects/:name/system/specimen", async (c) => {
|
|
6111
|
+
const system = await systemFor(c.req.param("name"));
|
|
6112
|
+
if (!system)
|
|
6113
|
+
return c.text("No system", 404);
|
|
6114
|
+
const mode = c.req.query("mode") === "dark"
|
|
6115
|
+
? "dark"
|
|
6116
|
+
: c.req.query("mode") === "light"
|
|
6117
|
+
? "light"
|
|
6118
|
+
: undefined;
|
|
6119
|
+
const palette = activePalette(system, c.req.query("p"))?.id;
|
|
6120
|
+
const page = renderHtmlPage({
|
|
6121
|
+
title: "System",
|
|
6122
|
+
html: specimenHtml(system),
|
|
6123
|
+
origin: new URL(c.req.url).origin,
|
|
6124
|
+
theme: themeById(DEFAULT_THEME_ID),
|
|
6125
|
+
mode,
|
|
6126
|
+
system,
|
|
6127
|
+
palette,
|
|
6128
|
+
});
|
|
6129
|
+
const { doc } = await cdnInliner.inline(page, "");
|
|
6130
|
+
surfaceDocHeaders(c, false);
|
|
6131
|
+
return c.html(doc);
|
|
6132
|
+
});
|
|
6133
|
+
app.get("/api/projects/:name/system/export", async (c) => send(c, await exportSystemFlow(c.req.param("name"))));
|
|
6134
|
+
app.get("/api/projects/:name/system/revisions", async (c) => c.json({ revisions: await store.listSystemRevisions(c.req.param("name")) }));
|
|
6135
|
+
app.get("/api/projects/:name/system/revisions/:rev", async (c) => {
|
|
6136
|
+
const system = await store.getSystemRevision(c.req.param("name"), Number(c.req.param("rev")));
|
|
6137
|
+
return system ? c.json(system) : c.json({ error: "no such revision" }, 404);
|
|
6138
|
+
});
|
|
5879
6139
|
// A project's rules file (.mockpit/rules.md, sent by `mockpit rules` and
|
|
5880
6140
|
// `mockpit init`) and the user's own (`mockpit rules --mine`).
|
|
5881
6141
|
app.get("/api/projects/:name/rules", async (c) => c.json({ text: (await store.getSetting(`${RULES_SETTING}:${c.req.param("name")}`)) ?? "" }));
|
|
5882
6142
|
app.put("/api/projects/:name/rules", async (c) => send(c, await saveRules(`${RULES_SETTING}:${c.req.param("name")}`, await jsonBody(c))));
|
|
5883
6143
|
app.get("/api/rules", async (c) => c.json({ text: (await userRulesText()) ?? "" }));
|
|
5884
6144
|
app.put("/api/rules", async (c) => send(c, await saveRules(USER_RULES_SETTING, await jsonBody(c))));
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
const
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
6145
|
+
// --- the project's system ---
|
|
6146
|
+
// Writes to one project's system run one at a time: each reads the revision
|
|
6147
|
+
// it bumps.
|
|
6148
|
+
const systemQueue = new Map();
|
|
6149
|
+
function serialSystem(project, fn) {
|
|
6150
|
+
const prev = systemQueue.get(project) ?? Promise.resolve();
|
|
6151
|
+
const run = prev.then(fn, fn);
|
|
6152
|
+
const settled = run.catch(() => { });
|
|
6153
|
+
systemQueue.set(project, settled);
|
|
6154
|
+
void settled.then(() => {
|
|
6155
|
+
if (systemQueue.get(project) === settled)
|
|
6156
|
+
systemQueue.delete(project);
|
|
6157
|
+
});
|
|
6158
|
+
return run;
|
|
6159
|
+
}
|
|
6160
|
+
// Who hears the user's hand on the system: the agent that last wrote it if it is
|
|
6161
|
+
// listening, else one listening in the project, else that writer or the project's
|
|
6162
|
+
// latest session, which hears it on its next call. Null: no agent to tell yet.
|
|
6163
|
+
async function systemReplySession(project) {
|
|
6164
|
+
const writer = (await store.listSystemRevisions(project)).find((r) => r.sessionId)?.sessionId ?? null;
|
|
6165
|
+
if (writer && listening.has(writer))
|
|
6166
|
+
return writer;
|
|
6167
|
+
for (const id of listening.keys()) {
|
|
6168
|
+
if ((await store.getSession(id))?.project === project)
|
|
6169
|
+
return id;
|
|
5899
6170
|
}
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
6171
|
+
if (writer && (await store.getSession(writer)))
|
|
6172
|
+
return writer;
|
|
6173
|
+
const sessions = await store.listSessions(project);
|
|
6174
|
+
sessions.sort((a, b) => ownerActiveAt(b).localeCompare(ownerActiveAt(a)));
|
|
6175
|
+
return sessions[0]?.id ?? null;
|
|
6176
|
+
}
|
|
6177
|
+
// The System page: the user sets a palette's status or restores a revision. One
|
|
6178
|
+
// new revision, and one reply through the agent's cursor in the same write.
|
|
6179
|
+
async function systemDecideFlow(name, body, ctx) {
|
|
6180
|
+
if (!ctx.viewer)
|
|
6181
|
+
return viewerOnly();
|
|
6182
|
+
const project = str(name, MAX_TITLE);
|
|
6183
|
+
if (!project)
|
|
6184
|
+
return fail(400, 'provide "project"');
|
|
6185
|
+
if (!isRecord(body))
|
|
6186
|
+
return fail(400, "invalid JSON body");
|
|
6187
|
+
const restoring = body.restore !== undefined;
|
|
6188
|
+
if (restoring === (body.palette !== undefined)) {
|
|
6189
|
+
return fail(400, "send {palette, status} or {restore}");
|
|
6190
|
+
}
|
|
6191
|
+
const status = body.status;
|
|
6192
|
+
if (!restoring && !PALETTE_STATUSES.includes(status)) {
|
|
6193
|
+
return fail(400, `"status" must be one of ${PALETTE_STATUSES.join(", ")}`);
|
|
6194
|
+
}
|
|
6195
|
+
return serialSystem(project, async () => {
|
|
6196
|
+
const current = await systemFor(project);
|
|
6197
|
+
if (!current)
|
|
6198
|
+
return fail(404, `${project} has no system`);
|
|
6199
|
+
let next;
|
|
6200
|
+
let changes;
|
|
6201
|
+
if (restoring) {
|
|
6202
|
+
const rev = Number(body.restore);
|
|
6203
|
+
const old = Number.isInteger(rev) ? await store.getSystemRevision(project, rev) : null;
|
|
6204
|
+
if (!old)
|
|
6205
|
+
return fail(404, `no revision ${String(body.restore)} of ${project}'s system`);
|
|
6206
|
+
next = restoreSystem(current, old).system;
|
|
6207
|
+
if (!systemDiffers(current, next))
|
|
6208
|
+
return ok({ rev: current.rev, changes: [] });
|
|
6209
|
+
const what = describeChange(current, next);
|
|
6210
|
+
changes = [`restored r${rev}: ${what.join("; ")}`];
|
|
6211
|
+
}
|
|
6212
|
+
else {
|
|
6213
|
+
const target = current.color.palettes.find((p) => p.id === body.palette);
|
|
6214
|
+
if (!target)
|
|
6215
|
+
return fail(404, `${project}'s system has no palette "${String(body.palette)}"`);
|
|
6216
|
+
if (target.status === status)
|
|
6217
|
+
return ok({ rev: current.rev, changes: [] });
|
|
6218
|
+
if (target.status === "default") {
|
|
6219
|
+
return fail(400, "make another palette the default first: a project always has one");
|
|
6220
|
+
}
|
|
6221
|
+
const was = defaultPalette(current);
|
|
6222
|
+
next = {
|
|
6223
|
+
...current,
|
|
6224
|
+
color: {
|
|
6225
|
+
...current.color,
|
|
6226
|
+
palettes: current.color.palettes.map((p) => p.id === target.id
|
|
6227
|
+
? { ...p, status }
|
|
6228
|
+
: status === "default" && p.status === "default"
|
|
6229
|
+
? { ...p, status: "kept" }
|
|
6230
|
+
: p),
|
|
6231
|
+
},
|
|
6232
|
+
};
|
|
6233
|
+
changes = [
|
|
6234
|
+
status === "default"
|
|
6235
|
+
? `palette "${target.id}" is the default${was ? ` ("${was.id}" kept)` : ""}`
|
|
6236
|
+
: `palette "${target.id}" ${status}`,
|
|
6237
|
+
];
|
|
6238
|
+
}
|
|
6239
|
+
const rev = current.rev + 1;
|
|
6240
|
+
const sessionId = await systemReplySession(project);
|
|
6241
|
+
const summary = `by the user: ${changes.join("; ")}`;
|
|
6242
|
+
const comment = await store.commitSystemDecision({
|
|
6243
|
+
system: { ...next, rev, updatedAt: new Date().toISOString() },
|
|
6244
|
+
summary,
|
|
6245
|
+
tell: sessionId
|
|
6246
|
+
? {
|
|
6247
|
+
sessionId,
|
|
6248
|
+
payload: {
|
|
6249
|
+
designId: "",
|
|
6250
|
+
version: 0,
|
|
6251
|
+
answers: {},
|
|
6252
|
+
borrowed: {},
|
|
6253
|
+
tuned: {},
|
|
6254
|
+
comments: [],
|
|
6255
|
+
system: { project, rev, changes },
|
|
6256
|
+
},
|
|
6257
|
+
}
|
|
6258
|
+
: null,
|
|
6259
|
+
});
|
|
6260
|
+
if (comment)
|
|
6261
|
+
announceComment(comment);
|
|
6262
|
+
clearRenderCache();
|
|
6263
|
+
bus.broadcast({ type: "system-changed", project, rev, summary, by: "user" });
|
|
6264
|
+
return ok({ rev, changes, told: sessionId });
|
|
6265
|
+
});
|
|
6266
|
+
}
|
|
6267
|
+
// Knob paths the project's designs declare, so a project knob can't take one.
|
|
6268
|
+
async function designKnobPaths(project) {
|
|
6269
|
+
const out = new Map();
|
|
6270
|
+
for (const d of await store.listDesigns(project)) {
|
|
6271
|
+
for (const path of Object.keys(d.knobs))
|
|
6272
|
+
if (!out.has(path))
|
|
6273
|
+
out.set(path, d.slug);
|
|
6274
|
+
}
|
|
6275
|
+
return out;
|
|
6276
|
+
}
|
|
6277
|
+
// What init read, cut to the sizes a frame and a brief can carry.
|
|
6278
|
+
function checkRepo(raw) {
|
|
6279
|
+
if (raw === null)
|
|
6280
|
+
return null;
|
|
6281
|
+
if (!isRecord(raw))
|
|
6282
|
+
return "repo is what `mockpit init` read";
|
|
6283
|
+
const designFiles = checkDesignFiles(raw.designFiles);
|
|
5913
6284
|
if (typeof designFiles === "string")
|
|
5914
|
-
return
|
|
5915
|
-
const
|
|
5916
|
-
|
|
6285
|
+
return designFiles;
|
|
6286
|
+
const d = raw.detected;
|
|
6287
|
+
return {
|
|
6288
|
+
detected: isRecord(d)
|
|
5917
6289
|
? {
|
|
5918
|
-
tailwind:
|
|
5919
|
-
shadcn:
|
|
5920
|
-
cssVars: Number(
|
|
5921
|
-
fonts: Array.isArray(
|
|
5922
|
-
?
|
|
6290
|
+
tailwind: d.tailwind === true,
|
|
6291
|
+
shadcn: d.shadcn === true,
|
|
6292
|
+
cssVars: Number(d.cssVars) || 0,
|
|
6293
|
+
fonts: Array.isArray(d.fonts)
|
|
6294
|
+
? d.fonts.filter((f) => typeof f === "string").slice(0, 20)
|
|
5923
6295
|
: [],
|
|
5924
6296
|
}
|
|
5925
6297
|
: null,
|
|
5926
|
-
|
|
5927
|
-
? body.palette
|
|
5928
|
-
: null,
|
|
5929
|
-
kit,
|
|
5930
|
-
cssVars: typeof body.cssVars === "string" ? body.cssVars.slice(0, 64_000) : "",
|
|
6298
|
+
cssVars: typeof raw.cssVars === "string" ? raw.cssVars.slice(0, 64_000) : "",
|
|
5931
6299
|
// A cut stylesheet would not compile, so an oversized one is dropped
|
|
5932
6300
|
// whole and the frame falls back to `cssVars`.
|
|
5933
|
-
tailwindCss: typeof
|
|
5934
|
-
?
|
|
6301
|
+
tailwindCss: typeof raw.tailwindCss === "string" && raw.tailwindCss.length <= 128_000
|
|
6302
|
+
? raw.tailwindCss
|
|
5935
6303
|
: "",
|
|
5936
|
-
strippedImports: Array.isArray(
|
|
5937
|
-
?
|
|
6304
|
+
strippedImports: Array.isArray(raw.strippedImports)
|
|
6305
|
+
? raw.strippedImports
|
|
5938
6306
|
.filter((s) => typeof s === "string")
|
|
5939
6307
|
.map((s) => s.slice(0, 200))
|
|
5940
6308
|
.slice(0, 50)
|
|
5941
6309
|
: [],
|
|
5942
|
-
iconSets,
|
|
5943
|
-
projectKits,
|
|
5944
6310
|
designFiles,
|
|
5945
|
-
updatedAt: new Date().toISOString(),
|
|
5946
6311
|
};
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
async function
|
|
5950
|
-
await store.
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
function
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
if (!body
|
|
5990
|
-
return
|
|
5991
|
-
const
|
|
5992
|
-
const
|
|
5993
|
-
if (
|
|
5994
|
-
return
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6312
|
+
}
|
|
6313
|
+
// The user's own system decisions that went to an agent, and whether it has them.
|
|
6314
|
+
async function systemTold(project) {
|
|
6315
|
+
const decided = (await store.listComments({ noDesign: true })).filter((c) => c.kind === "reply" && c.payload?.system?.project === project);
|
|
6316
|
+
return (await withDelivered(decided)).map((c) => ({
|
|
6317
|
+
rev: c.payload.system.rev,
|
|
6318
|
+
delivered: c.delivered,
|
|
6319
|
+
}));
|
|
6320
|
+
}
|
|
6321
|
+
// The system as an agent reads it: everything, with the repo's compiled
|
|
6322
|
+
// Tailwind input said by size, plus its revisions and what fails its checks.
|
|
6323
|
+
async function systemView(project, system, ctx) {
|
|
6324
|
+
const s = system ?? emptySystem(project);
|
|
6325
|
+
return {
|
|
6326
|
+
...s,
|
|
6327
|
+
...(s.repo?.tailwindCss
|
|
6328
|
+
? {
|
|
6329
|
+
repo: {
|
|
6330
|
+
...s.repo,
|
|
6331
|
+
tailwindCss: `${s.repo.tailwindCss.length} characters, compiled in every frame`,
|
|
6332
|
+
},
|
|
6333
|
+
}
|
|
6334
|
+
: {}),
|
|
6335
|
+
checks: systemChecks(s),
|
|
6336
|
+
revisions: (await store.listSystemRevisions(project)).slice(0, 10),
|
|
6337
|
+
// What the System page draws beside the palettes; agents read the checks.
|
|
6338
|
+
...(ctx.viewer
|
|
6339
|
+
? {
|
|
6340
|
+
contrast: pairResults(s, true),
|
|
6341
|
+
components: await componentIndex(project),
|
|
6342
|
+
told: await systemTold(project),
|
|
6343
|
+
}
|
|
6344
|
+
: {}),
|
|
6345
|
+
url: `${ctx.base}/project/${encodeURIComponent(project)}?sec=system`,
|
|
6346
|
+
};
|
|
6347
|
+
}
|
|
6348
|
+
async function systemFlow(projectArg, body, ctx) {
|
|
6349
|
+
const project = str(projectArg, MAX_TITLE);
|
|
6350
|
+
if (!project)
|
|
6351
|
+
return fail(400, 'provide "project"');
|
|
6352
|
+
if (body === undefined)
|
|
6353
|
+
return ok(await systemView(project, await systemFor(project), ctx));
|
|
6354
|
+
if (!isRecord(body))
|
|
6355
|
+
return fail(400, "invalid JSON body");
|
|
6356
|
+
const known = new Set([...SYSTEM_WRITE_KEYS, "restore", "session", "project", "agent"]);
|
|
6357
|
+
const unknown = Object.keys(body).filter((k) => !known.has(k));
|
|
6358
|
+
if (unknown.length) {
|
|
6359
|
+
return fail(400, `unknown ${unknown.length === 1 ? "argument" : "arguments"} ${unknown.map((k) => `"${k}"`).join(", ")}`, {
|
|
6360
|
+
arguments: [
|
|
6361
|
+
...SYSTEM_WRITE_KEYS.filter((k) => !["repo", "projectKits", "removeKits", "iconSets", "kit"].includes(k)),
|
|
6362
|
+
"restore",
|
|
6363
|
+
],
|
|
6364
|
+
});
|
|
6365
|
+
}
|
|
6366
|
+
const writing = Object.keys(body).some((k) => k !== "session" && k !== "project" && k !== "agent");
|
|
6367
|
+
if (!writing)
|
|
6368
|
+
return ok(await systemView(project, await systemFor(project), ctx));
|
|
6369
|
+
// The user's changes go through decide, which tells the agent.
|
|
6370
|
+
if (ctx.viewer)
|
|
6371
|
+
return fail(400, "the viewer changes the system through /system/decide");
|
|
6372
|
+
const sessionId = typeof body.session === "string" && (await store.getSession(body.session))
|
|
6373
|
+
? body.session
|
|
6374
|
+
: null;
|
|
6375
|
+
if (sessionId)
|
|
6376
|
+
await store.touchAgent(sessionId);
|
|
6377
|
+
let write = body;
|
|
6378
|
+
if (body.iconSets !== undefined) {
|
|
6379
|
+
const checked = await checkIconSets(body.iconSets);
|
|
6380
|
+
if (typeof checked === "string")
|
|
6381
|
+
return fail(400, checked);
|
|
6382
|
+
write = { ...write, iconSets: checked };
|
|
6383
|
+
}
|
|
6384
|
+
if (body.repo !== undefined) {
|
|
6385
|
+
const repo = checkRepo(body.repo);
|
|
6386
|
+
if (typeof repo === "string")
|
|
6387
|
+
return fail(400, repo);
|
|
6388
|
+
write = { ...write, repo };
|
|
6389
|
+
}
|
|
6390
|
+
const kitPaths = body.knobs !== undefined ? await designKnobPaths(project) : new Map();
|
|
6391
|
+
return serialSystem(project, async () => {
|
|
6392
|
+
const current = (await systemFor(project)) ?? emptySystem(project);
|
|
6393
|
+
let next;
|
|
6394
|
+
let summary;
|
|
6395
|
+
let changed;
|
|
6396
|
+
// What the user decided, which an agent's restore leaves standing.
|
|
6397
|
+
let keptOnRestore = [];
|
|
6398
|
+
if (body.restore !== undefined) {
|
|
6399
|
+
if (Object.keys(body).some((k) => !["restore", "session", "project", "agent"].includes(k)))
|
|
6400
|
+
return fail(400, "restore stands alone: restore first, then write");
|
|
6401
|
+
const rev = Number(body.restore);
|
|
6402
|
+
const old = Number.isInteger(rev) ? await store.getSystemRevision(project, rev) : null;
|
|
6403
|
+
if (!old) {
|
|
6404
|
+
const kept = (await store.listSystemRevisions(project)).map((r) => r.rev);
|
|
6405
|
+
return fail(404, `no revision ${String(body.restore)} of ${project}'s system`, { kept });
|
|
6406
|
+
}
|
|
6407
|
+
const restored = restoreSystem(current, old, { keepDecided: true });
|
|
6408
|
+
next = restored.system;
|
|
6409
|
+
keptOnRestore = restored.kept;
|
|
6410
|
+
changed = describeChange(current, next);
|
|
6411
|
+
summary = `restored r${rev}${changed.length ? `: ${changed.join("; ")}` : ""}`;
|
|
6412
|
+
}
|
|
6413
|
+
else {
|
|
6414
|
+
const applied = applySystemWrite(current, write, {
|
|
6415
|
+
checkUrl: (url) => {
|
|
6416
|
+
const r = checkCdnUrl(url, "font url");
|
|
6417
|
+
return "error" in r ? r.error : null;
|
|
6418
|
+
},
|
|
6419
|
+
checkKnobs: (raw) => checkKnobs(raw),
|
|
6420
|
+
checkKit: (raw) => checkProjectKit(raw),
|
|
6421
|
+
kitIds: KIT_IDS,
|
|
6422
|
+
designKnobPaths: kitPaths,
|
|
6423
|
+
});
|
|
6424
|
+
if ("error" in applied)
|
|
6425
|
+
return fail(400, applied.error);
|
|
6426
|
+
next = applied.system;
|
|
6427
|
+
changed = describeChange(current, next);
|
|
6428
|
+
summary = changed.join("; ");
|
|
6429
|
+
}
|
|
6430
|
+
const checks = systemChecks(next);
|
|
6431
|
+
// What init derived, said on every rerun, changed or not.
|
|
6432
|
+
const derivedOf = (sys) => body.repo !== undefined
|
|
6433
|
+
? {
|
|
6434
|
+
derived: {
|
|
6435
|
+
tokens: sys.tokens.filter((t) => t.owner === "repo").length,
|
|
6436
|
+
roles: sys.color.roles.filter((r) => r.owner === "repo").length,
|
|
6437
|
+
palette: sys.color.palettes.some((p) => p.owner === "repo"),
|
|
6438
|
+
untyped: sys.repo?.untyped ?? [],
|
|
6439
|
+
},
|
|
6440
|
+
}
|
|
6441
|
+
: {};
|
|
6442
|
+
if (!systemDiffers(current, next)) {
|
|
6443
|
+
return ok({
|
|
6444
|
+
rev: current.rev,
|
|
6445
|
+
changed: [],
|
|
6446
|
+
...(keptOnRestore.length ? { kept: keptOnRestore } : {}),
|
|
6447
|
+
checks,
|
|
6448
|
+
...derivedOf(current),
|
|
6449
|
+
url: `${ctx.base}/project/${encodeURIComponent(project)}?sec=system`,
|
|
6450
|
+
feedback: await feedbackFor(body.session, ctx.base),
|
|
6451
|
+
});
|
|
6452
|
+
}
|
|
6453
|
+
const saved = await store.putSystem({ ...next, rev: current.rev + 1 }, { sessionId, summary });
|
|
6454
|
+
// Every rendered surface of the project wears the system, so none stays.
|
|
6455
|
+
clearRenderCache();
|
|
6456
|
+
bus.broadcast({ type: "system-changed", project, rev: saved.rev, summary, by: "agent" });
|
|
6457
|
+
return ok({
|
|
6458
|
+
rev: saved.rev,
|
|
6459
|
+
changed,
|
|
6460
|
+
...(keptOnRestore.length ? { kept: keptOnRestore } : {}),
|
|
6461
|
+
checks,
|
|
6462
|
+
...derivedOf(saved),
|
|
6463
|
+
...(checks.length ? { nudges: checks } : {}),
|
|
6464
|
+
url: `${ctx.base}/project/${encodeURIComponent(project)}?sec=system`,
|
|
6465
|
+
feedback: await feedbackFor(body.session, ctx.base),
|
|
6466
|
+
});
|
|
6019
6467
|
});
|
|
6020
|
-
|
|
6021
|
-
|
|
6468
|
+
}
|
|
6469
|
+
// Components by slug, with the first line of their notes and how many screens
|
|
6470
|
+
// embed them: the brief's index of what to reuse.
|
|
6471
|
+
async function componentIndex(project) {
|
|
6472
|
+
const designs = await store.listDesigns(project);
|
|
6473
|
+
const components = designs.filter((d) => d.kind === "component" && !d.archivedAt);
|
|
6474
|
+
if (components.length === 0)
|
|
6475
|
+
return [];
|
|
6476
|
+
const kindOf = new Map(designs.map((d) => [d.id, d]));
|
|
6477
|
+
const screens = new Map();
|
|
6478
|
+
for (const head of await store.listPostHeads(project)) {
|
|
6479
|
+
if (!isLive(head.status))
|
|
6480
|
+
continue;
|
|
6481
|
+
const d = kindOf.get(head.design);
|
|
6482
|
+
if (!d || d.kind !== "screen")
|
|
6483
|
+
continue;
|
|
6484
|
+
for (const e of head.embeds) {
|
|
6485
|
+
let set = screens.get(e.slug);
|
|
6486
|
+
if (!set)
|
|
6487
|
+
screens.set(e.slug, (set = new Set()));
|
|
6488
|
+
set.add(d.slug);
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
return components.map((d) => ({
|
|
6492
|
+
slug: d.slug,
|
|
6493
|
+
purpose: (d.notes?.text ?? "").split("\n")[0].trim(),
|
|
6494
|
+
screens: screens.get(d.slug)?.size ?? 0,
|
|
6495
|
+
}));
|
|
6496
|
+
}
|
|
6022
6497
|
// Every set an html surface of this project can name, installed first.
|
|
6023
6498
|
app.get("/api/projects/:name/icons", async (c) => {
|
|
6024
|
-
const sets = await iconSetsFor(await
|
|
6499
|
+
const sets = await iconSetsFor(await systemFor(c.req.param("name")));
|
|
6025
6500
|
return c.json({
|
|
6026
6501
|
sets: sets.map((s) => ({ prefix: s.prefix, count: iconCount(s.set), source: s.source })),
|
|
6027
6502
|
});
|
|
@@ -6072,6 +6547,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6072
6547
|
count: Math.max(0, Math.floor(Number(tokens.count) || 0)),
|
|
6073
6548
|
cssVars: tokens.cssVars === true,
|
|
6074
6549
|
values: flat(tokens.values, 400),
|
|
6550
|
+
...(isRecord(tokens.types) ? { types: flat(tokens.types, 400) } : {}),
|
|
6075
6551
|
};
|
|
6076
6552
|
}
|
|
6077
6553
|
const shadcn = raw.shadcn;
|
|
@@ -6159,7 +6635,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6159
6635
|
// agent publishing over one origin must never be served to a viewer on another.
|
|
6160
6636
|
// `knobs` is the canonical (validated, path-sorted) JSON of `?k=`, so two
|
|
6161
6637
|
// spellings of the same values share an entry.
|
|
6162
|
-
const renderKey = (o) => `${o.postId}:${o.idx}:${o.version}:${o.mode ?? "os"}:${o.origin}:${o.knobs ?? ""}:${o.embeds ?? ""}${o.rewrites ? `:w${o.rewrites}` : ""}`;
|
|
6638
|
+
const renderKey = (o) => `${o.postId}:${o.idx}:${o.version}:${o.mode ?? "os"}:${o.origin}:${o.knobs ?? ""}:${o.embeds ?? ""}${o.rewrites ? `:w${o.rewrites}` : ""}:y${o.system ?? 0}:${o.palette ?? ""}`;
|
|
6163
6639
|
// After a version is rewritten in place: a frame still naming the old `w`
|
|
6164
6640
|
// must miss the cache and get the version as it is now.
|
|
6165
6641
|
const dropPostRenders = (postId) => {
|
|
@@ -6177,7 +6653,11 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6177
6653
|
// Embedded components are expanded server-side (the caller passes the
|
|
6178
6654
|
// expansion), so the whole page is still ONE sandboxed document rather
|
|
6179
6655
|
// than nested frames.
|
|
6180
|
-
const html = withAnswerAdditions(args.composed ?? surface.html,
|
|
6656
|
+
const html = withAnswerAdditions(args.composed ?? surface.html, [
|
|
6657
|
+
...(args.design?.answerAdditions ?? []),
|
|
6658
|
+
...(args.system?.additions ?? []),
|
|
6659
|
+
]);
|
|
6660
|
+
const knobs = { ...projectKnobValues(args.system), ...args.knobs };
|
|
6181
6661
|
const page = renderHtmlPage({
|
|
6182
6662
|
title: args.title,
|
|
6183
6663
|
html,
|
|
@@ -6185,12 +6665,13 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6185
6665
|
theme,
|
|
6186
6666
|
mode,
|
|
6187
6667
|
kits: surface.kits,
|
|
6188
|
-
|
|
6668
|
+
system: args.system,
|
|
6669
|
+
palette: args.palette,
|
|
6670
|
+
// A version from before contracts stored none: it renders as it was approved.
|
|
6671
|
+
contract: args.contract ?? 1,
|
|
6189
6672
|
version: args.version,
|
|
6190
|
-
knobs:
|
|
6191
|
-
resolveIcon: mayHaveIcons(html)
|
|
6192
|
-
? (await iconResolverFor(args.designSystem)).resolve
|
|
6193
|
-
: undefined,
|
|
6673
|
+
knobs: Object.keys(knobs).length ? knobs : undefined,
|
|
6674
|
+
resolveIcon: mayHaveIcons(html) ? (await iconResolverFor(args.system)).resolve : undefined,
|
|
6194
6675
|
});
|
|
6195
6676
|
return cdnInliner.inline(page, html);
|
|
6196
6677
|
}
|
|
@@ -6254,7 +6735,8 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6254
6735
|
if (!origin)
|
|
6255
6736
|
return;
|
|
6256
6737
|
void (async () => {
|
|
6257
|
-
const
|
|
6738
|
+
const system = await systemFor(design.project);
|
|
6739
|
+
const palette = system ? activePalette(system, undefined)?.id : undefined;
|
|
6258
6740
|
let warmed = 0;
|
|
6259
6741
|
for (const [idx, surface] of post.surfaces.entries()) {
|
|
6260
6742
|
if (warmed >= PREWARM_SURFACE_LIMIT)
|
|
@@ -6274,6 +6756,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6274
6756
|
origin,
|
|
6275
6757
|
embeds: composed ? embedsKey(composed) : undefined,
|
|
6276
6758
|
rewrites: post.rewrites,
|
|
6759
|
+
system: system?.rev,
|
|
6277
6760
|
});
|
|
6278
6761
|
if (renderCache.has(key))
|
|
6279
6762
|
continue;
|
|
@@ -6285,7 +6768,9 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6285
6768
|
version: post.version,
|
|
6286
6769
|
mode,
|
|
6287
6770
|
origin,
|
|
6288
|
-
|
|
6771
|
+
system,
|
|
6772
|
+
palette,
|
|
6773
|
+
contract: post.contract,
|
|
6289
6774
|
composed: composed?.html,
|
|
6290
6775
|
}));
|
|
6291
6776
|
}
|
|
@@ -6306,6 +6791,12 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6306
6791
|
const embedParam = c.req.query("embed");
|
|
6307
6792
|
// `w`: the variant's rewrites the caller saw, since an amend keeps the number.
|
|
6308
6793
|
const wParam = Number(c.req.query("w") ?? 0) || 0;
|
|
6794
|
+
// `y`: the system revision the caller saw; `sys`: a peek at an older one;
|
|
6795
|
+
// `p`: the palette to wear (absent: the default).
|
|
6796
|
+
const yParam = Number(c.req.query("y") ?? 0) || 0;
|
|
6797
|
+
const sysParam = Number(c.req.query("sys") ?? 0) || 0;
|
|
6798
|
+
const pRaw = c.req.query("p");
|
|
6799
|
+
const pParam = pRaw && /^[a-z][a-z0-9-]{0,47}$/.test(pRaw) ? pRaw : undefined;
|
|
6309
6800
|
const origin = requestOrigin(c.req.raw);
|
|
6310
6801
|
// Cache-first. A hit needs nothing from the post row, and the row is the
|
|
6311
6802
|
// expensive part of this route (surfaces + every retained version). Only a
|
|
@@ -6325,6 +6816,8 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6325
6816
|
origin,
|
|
6326
6817
|
embeds: eParam,
|
|
6327
6818
|
rewrites: wParam,
|
|
6819
|
+
system: sysParam || yParam,
|
|
6820
|
+
palette: pParam,
|
|
6328
6821
|
}));
|
|
6329
6822
|
if (hit !== undefined) {
|
|
6330
6823
|
surfaceDocHeaders(c, true);
|
|
@@ -6338,6 +6831,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6338
6831
|
let title = post.title;
|
|
6339
6832
|
let surfaces = post.surfaces;
|
|
6340
6833
|
let version = post.version;
|
|
6834
|
+
let contract = post.contract;
|
|
6341
6835
|
if (ver && Number(ver) !== post.version) {
|
|
6342
6836
|
const old = post.history.find((h) => h.version === Number(ver));
|
|
6343
6837
|
if (!old)
|
|
@@ -6345,6 +6839,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6345
6839
|
title = old.title;
|
|
6346
6840
|
surfaces = old.surfaces;
|
|
6347
6841
|
version = old.version;
|
|
6842
|
+
contract = old.contract;
|
|
6348
6843
|
}
|
|
6349
6844
|
const idx = Number(surfaceParam ?? 0);
|
|
6350
6845
|
const surface = surfaces[idx];
|
|
@@ -6371,7 +6866,8 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6371
6866
|
catch {
|
|
6372
6867
|
return bad("k must be URL-encoded JSON of {path: value}");
|
|
6373
6868
|
}
|
|
6374
|
-
const
|
|
6869
|
+
const project = design ? (await systemFor(design.project))?.knobs : undefined;
|
|
6870
|
+
const checked = checkKnobValues(raw, { knobs: { ...project, ...design?.knobs } }, [post], "k");
|
|
6375
6871
|
if (!checked.ok)
|
|
6376
6872
|
return bad(checked.error);
|
|
6377
6873
|
const paths = Object.keys(checked.value).sort();
|
|
@@ -6407,6 +6903,16 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6407
6903
|
// Cache the finished document under the same key the pre-warm uses; the
|
|
6408
6904
|
// resolved `version` and embeds key make it immutable, so a hit is always
|
|
6409
6905
|
// correct.
|
|
6906
|
+
let system = design ? await systemFor(design.project) : null;
|
|
6907
|
+
const currentRev = system?.rev ?? 0;
|
|
6908
|
+
if (sysParam && design) {
|
|
6909
|
+
system = await store.getSystemRevision(design.project, sysParam);
|
|
6910
|
+
if (!system) {
|
|
6911
|
+
surfaceDocHeaders(c, false);
|
|
6912
|
+
return c.text(`System revision ${sysParam} not available`, 404);
|
|
6913
|
+
}
|
|
6914
|
+
}
|
|
6915
|
+
const palette = system ? activePalette(system, pParam)?.id : undefined;
|
|
6410
6916
|
const cacheKey = renderKey({
|
|
6411
6917
|
postId: post.id,
|
|
6412
6918
|
idx,
|
|
@@ -6416,8 +6922,9 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6416
6922
|
knobs: knobsKey,
|
|
6417
6923
|
embeds: eKey,
|
|
6418
6924
|
rewrites: post.rewrites,
|
|
6925
|
+
system: system?.rev,
|
|
6926
|
+
palette: pParam,
|
|
6419
6927
|
});
|
|
6420
|
-
const designSystem = design ? await designSystemFor(design.project) : null;
|
|
6421
6928
|
const { doc, complete } = await cachedRender(cacheKey, () => buildSurfaceDoc({
|
|
6422
6929
|
post,
|
|
6423
6930
|
design,
|
|
@@ -6426,7 +6933,9 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6426
6933
|
version,
|
|
6427
6934
|
mode,
|
|
6428
6935
|
origin,
|
|
6429
|
-
|
|
6936
|
+
system,
|
|
6937
|
+
palette,
|
|
6938
|
+
contract,
|
|
6430
6939
|
knobs,
|
|
6431
6940
|
composed: composed?.html,
|
|
6432
6941
|
}));
|
|
@@ -6437,7 +6946,9 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6437
6946
|
surfaceDocHeaders(c, complete &&
|
|
6438
6947
|
ver != null &&
|
|
6439
6948
|
(eKey === undefined || eKey === eParam) &&
|
|
6440
|
-
wParam === (post.rewrites ?? 0)
|
|
6949
|
+
wParam === (post.rewrites ?? 0) &&
|
|
6950
|
+
// The system follows its latest revision unless the URL names one.
|
|
6951
|
+
(sysParam > 0 || !system || yParam === currentRev));
|
|
6441
6952
|
return c.html(doc);
|
|
6442
6953
|
};
|
|
6443
6954
|
app.get("/s/:id", renderPostPage);
|
|
@@ -6652,6 +7163,7 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6652
7163
|
const mcpFlows = {
|
|
6653
7164
|
publish: (body, ctx) => publishFlow(body, ctx),
|
|
6654
7165
|
list: (query) => listDesignsFlow(query),
|
|
7166
|
+
exportSystem: (project) => exportSystemFlow(project),
|
|
6655
7167
|
tree: (query) => treeFlow(query),
|
|
6656
7168
|
decisions: (query) => decisionsFlow(query),
|
|
6657
7169
|
get: (ref, query) => getDesignFlow(ref, query),
|
|
@@ -6662,8 +7174,13 @@ export function createApp({ store, viewerHtml, setupText, topics = {}, packs = {
|
|
|
6662
7174
|
feedback: (query, ctx) => feedbackFlow(query, ctx),
|
|
6663
7175
|
edit: (ref, body, ctx) => editDesignFlow(ref, body, ctx),
|
|
6664
7176
|
editProject: (name, body, ctx) => editProjectFlow(name, body, ctx),
|
|
7177
|
+
system: (project, body, ctx) => systemFlow(project, body, ctx),
|
|
6665
7178
|
};
|
|
6666
|
-
const guide = (project, topic) => topic
|
|
7179
|
+
const guide = (project, topic) => topic === "system"
|
|
7180
|
+
? systemTopicFor(project)
|
|
7181
|
+
: topic
|
|
7182
|
+
? (topics[topic] ?? "")
|
|
7183
|
+
: briefFor(project);
|
|
6667
7184
|
// --- run: an agent script over the same flows (server/run.ts) ---
|
|
6668
7185
|
const runFlow = createRunFlow({
|
|
6669
7186
|
store,
|