yichus 0.3.0 → 0.3.2
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 +2 -2
- package/dist/cli.js +17 -3192
- package/dist/cli.js.map +1 -1
- package/dist/main.js +3762 -0
- package/dist/main.js.map +1 -0
- package/dist/studio-page/system-map-record.js +3 -3
- package/dist/studio-page/system-map.js +41 -9
- package/dist/studio-page/what-happens-record.js +11 -4
- package/dist/studio-page/what-happens.js +48 -29
- package/dist/studio-reference/balance-listed-laws.bosatsu +29 -0
- package/dist/studio-reference/balance.bosatsu +7 -0
- package/dist/studio-reference/calculator.md +20 -0
- package/dist/studio-reference/consent-laws.bosatsu +37 -0
- package/dist/studio-reference/consent.bosatsu +15 -0
- package/dist/studio-reference/deploy-platforms.md +6 -6
- package/dist/studio-reference/laws.md +62 -0
- package/dist/studio-reference/porting.md +3 -1
- package/dist/studio-reference/protocol-cases.md +11 -0
- package/dist/studio-reference/{protocol-specification.txt → protocol-specification.md} +16 -18
- package/dist/studio-reference/refund-desk-one-round.bosatsu +21 -18
- package/dist/studio-reference/refund-desk.bosatsu +21 -18
- package/dist/studio-reference/render.yaml +42 -0
- package/dist/studio-reference/server.mjs +33 -0
- package/dist/studio-reference/store.mjs +16 -0
- package/dist/studio-reference/the-page.md +34 -11
- package/dist/studio-reference/withdrawal.bosatsu +11 -15
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,3201 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// src/run.ts
|
|
8
|
-
import fs8 from "fs";
|
|
9
|
-
import path8 from "path";
|
|
10
|
-
import { isDeepStrictEqual, parseArgs as parseArgs2 } from "util";
|
|
11
|
-
import { z as z11 } from "zod";
|
|
12
|
-
|
|
13
|
-
// src/engine-results.ts
|
|
14
|
-
import { z } from "zod";
|
|
15
|
-
var PropertyStatus = z.enum(["proven", "warning", "violated", "blocked"]);
|
|
16
|
-
var Property = z.looseObject({
|
|
17
|
-
id: z.string(),
|
|
18
|
-
title: z.string().optional(),
|
|
19
|
-
status: PropertyStatus,
|
|
20
|
-
details: z.array(z.string()),
|
|
21
|
-
// The declared assumption an accepted warning rests on, e.g. "instances=1".
|
|
22
|
-
accepted: z.string().optional()
|
|
23
|
-
});
|
|
24
|
-
var VerifyResult = z.looseObject({
|
|
25
|
-
topology: z.string().optional(),
|
|
26
|
-
verdict: z.looseObject({
|
|
27
|
-
properties: z.array(Property),
|
|
28
|
-
errors: z.array(z.string()).default([]),
|
|
29
|
-
// Present only when the caller passed block_on_warnings: the warnings
|
|
30
|
-
// among the listed properties, which made the verdict not proven.
|
|
31
|
-
blockedWarnings: z.array(z.string()).optional()
|
|
32
|
-
})
|
|
33
|
-
});
|
|
34
|
-
var VerdictErrors = z.object({ verdict: z.object({ errors: z.array(z.string()) }) });
|
|
35
|
-
var EngineError = z.object({ error: z.string(), errorKinds: z.array(z.string()).optional() });
|
|
36
|
-
var Count = z.int().nonnegative();
|
|
37
|
-
var AbstractionMap = z.looseObject({
|
|
38
|
-
measures: z.looseObject({
|
|
39
|
-
definitions: Count,
|
|
40
|
-
edges: Count,
|
|
41
|
-
layers: Count,
|
|
42
|
-
layerSkips: Count,
|
|
43
|
-
usedOnce: Count,
|
|
44
|
-
usedMany: Count,
|
|
45
|
-
clonePairs: Count,
|
|
46
|
-
duplicatedMass: Count,
|
|
47
|
-
maxSize: Count,
|
|
48
|
-
largestDefinition: z.string().optional()
|
|
49
|
-
})
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// src/verdict.ts
|
|
53
|
-
var title = (p) => p.title ?? p.id;
|
|
54
|
-
var MEANING = {
|
|
55
|
-
proven: "holds for every input",
|
|
56
|
-
warning: "holds only under the acceptance its details name",
|
|
57
|
-
violated: "is broken; the details are the evidence",
|
|
58
|
-
blocked: "was not checked: the check could not run, could not finish, or could not see its subject"
|
|
59
|
-
};
|
|
60
|
-
var ACCEPTANCE = {
|
|
61
|
-
"instances=1": "Safe while you run one instance (--instances 1). Before running more than one:"
|
|
62
|
-
};
|
|
63
|
-
var acceptance = (a) => ACCEPTANCE[a] ?? `Safe under the declared ${a}. Before dropping it:`;
|
|
64
|
-
var label = (p) => p.status === "warning" && p.accepted !== void 0 ? "accepted" : p.status;
|
|
65
|
-
var indent = (text3, prefix) => text3.split("\n").map((line) => prefix + line).join("\n");
|
|
66
|
-
function count(props) {
|
|
67
|
-
const order = [];
|
|
68
|
-
const counts = /* @__PURE__ */ new Map();
|
|
69
|
-
for (const p of props) {
|
|
70
|
-
const status = label(p);
|
|
71
|
-
if (!counts.has(status)) order.push(status);
|
|
72
|
-
counts.set(status, (counts.get(status) ?? 0) + 1);
|
|
73
|
-
}
|
|
74
|
-
return order.map((status) => `${counts.get(status)} ${status}`).join(", ");
|
|
75
|
-
}
|
|
76
|
-
function verifySummary(result, proven, artifact) {
|
|
77
|
-
const parsed = VerifyResult.safeParse(result);
|
|
78
|
-
if (!parsed.success) {
|
|
79
|
-
const said = EngineError.safeParse(result);
|
|
80
|
-
const reason = said.success ? ` The engine said: ${said.data.error}` : "";
|
|
81
|
-
const errors2 = VerdictErrors.safeParse(result);
|
|
82
|
-
const lines2 = [
|
|
83
|
-
proven ? "Proven, but the verdict has no readable property list." : `Not proven. The engine returned no readable verdict.${reason}`
|
|
84
|
-
];
|
|
85
|
-
if (errors2.success && errors2.data.verdict.errors.length)
|
|
86
|
-
lines2.push("Errors:", ...errors2.data.verdict.errors.map((e) => indent(e, " ")));
|
|
87
|
-
lines2.push(`The full result is in ${artifact}.`);
|
|
88
|
-
return `${lines2.join("\n")}
|
|
89
|
-
`;
|
|
90
|
-
}
|
|
91
|
-
const checked = parsed.data.verdict.properties;
|
|
92
|
-
const errors = parsed.data.verdict.errors;
|
|
93
|
-
const topology2 = parsed.data.topology !== void 0 ? ` Topology: ${parsed.data.topology}.` : "";
|
|
94
|
-
const lines = [];
|
|
95
|
-
if (proven) {
|
|
96
|
-
lines.push(`Proven: ${checked.length} properties checked (${count(checked)}).${topology2}`);
|
|
97
|
-
} else {
|
|
98
|
-
lines.push(`Not proven: ${checked.length} properties checked (${count(checked)}).${topology2}`);
|
|
99
|
-
const blocking = new Set(parsed.data.verdict.blockedWarnings ?? []);
|
|
100
|
-
const first = checked.find((p) => p.status === "violated") ?? checked.find((p) => p.status !== "proven" && p.status !== "warning") ?? checked.find((p) => blocking.has(p.id));
|
|
101
|
-
if (first) {
|
|
102
|
-
const why = blocking.has(first.id) && first.status === "warning" ? " It blocks because --block-on-warning named it." : "";
|
|
103
|
-
lines.push(`First failure (${first.status}): ${title(first)}. This property ${MEANING[first.status]}.${why}`);
|
|
104
|
-
for (const detail of first.details) lines.push(indent(detail, " "));
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
lines.push("Every property:");
|
|
108
|
-
const width = Math.max(...checked.map((p) => label(p).length));
|
|
109
|
-
for (const p of checked) {
|
|
110
|
-
lines.push(` ${label(p).padEnd(width)} ${title(p)}`);
|
|
111
|
-
const pad = ` ${" ".repeat(width)} `;
|
|
112
|
-
if (p.status === "warning" && p.accepted !== void 0) lines.push(pad + acceptance(p.accepted));
|
|
113
|
-
if (p.status === "warning") for (const detail of p.details) lines.push(indent(detail, pad));
|
|
114
|
-
}
|
|
115
|
-
const shown2 = new Set(checked.flatMap((p) => p.details));
|
|
116
|
-
const unshown = errors.filter((e) => !shown2.has(e));
|
|
117
|
-
if (unshown.length) lines.push("Errors:", ...unshown.map((e) => indent(e, " ")));
|
|
118
|
-
lines.push(`The full verdict, with every property's details, is in ${artifact}.`);
|
|
119
|
-
return `${lines.join("\n")}
|
|
120
|
-
`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// src/lean-verdict.ts
|
|
124
|
-
import { LEAN_WASM_INSTALL_COMMAND, LeanCheckResult } from "@yichus/api-mcp";
|
|
125
|
-
import { z as z2 } from "zod";
|
|
126
|
-
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
127
|
-
var indent2 = (value, prefix) => value.split("\n").map((line) => prefix + line).join("\n");
|
|
128
|
-
var SIDE = {
|
|
129
|
-
rejected: "checker",
|
|
130
|
-
"axiom-audit": "checker",
|
|
131
|
-
"not-admitted": "checker",
|
|
132
|
-
"runtime-missing": "harness",
|
|
133
|
-
timeout: "harness",
|
|
134
|
-
"runtime-error": "harness"
|
|
135
|
-
};
|
|
136
|
-
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
137
|
-
var MAX_TIMEOUT_MS = 3e5;
|
|
138
|
-
function remedy(cause, timeoutMs, files) {
|
|
139
|
-
const sources = files.map((f) => `--source ${f}`).join(" ");
|
|
140
|
-
switch (cause) {
|
|
141
|
-
case "runtime-missing":
|
|
142
|
-
return [
|
|
143
|
-
"Install the pinned runtime once (about 290 MB to download), then pass that folder as --lean-wasm-root:",
|
|
144
|
-
` ${LEAN_WASM_INSTALL_COMMAND.replace("NEW_DIRECTORY", "/absolute/new/folder")}`
|
|
145
|
-
];
|
|
146
|
-
case "timeout":
|
|
147
|
-
return [timeoutMs < MAX_TIMEOUT_MS ? `The ${timeoutMs} ms budget ran out before Lean finished. Rerun with a larger --timeout-ms (the default is ${DEFAULT_TIMEOUT_MS}, the most is ${MAX_TIMEOUT_MS}).` : `The ${timeoutMs} ms budget, the most allowed, ran out before Lean finished. Split the laws across separate files and check them one at a time.`];
|
|
148
|
-
case "runtime-error":
|
|
149
|
-
return [
|
|
150
|
-
"The Lean Wasm runtime could not run. If it was changed or only partly installed, install a fresh copy into a new folder:",
|
|
151
|
-
` ${LEAN_WASM_INSTALL_COMMAND.replace("NEW_DIRECTORY", "/absolute/new/folder")}`
|
|
152
|
-
];
|
|
153
|
-
case "rejected":
|
|
154
|
-
return [
|
|
155
|
-
"A rejected theorem means the law fails for some input, its witness matches no value, or the generated Lean is wrong; Lean's words above say which.",
|
|
156
|
-
`To see a failing input, run: yichus call api_law_check ${sources}. Fix the code, not the law.`
|
|
157
|
-
];
|
|
158
|
-
case "axiom-audit":
|
|
159
|
-
return ["Lean accepted the theorem, but its axiom audit was missing or named an axiom other than propext, Classical.choice and Quot.sound, so it is not counted as proven."];
|
|
160
|
-
case "not-admitted":
|
|
161
|
-
return [`Lean Wasm proves Bool and structural List[Bool] laws with one direct witness. Check other laws with: yichus call api_law_check ${sources}`];
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
var unreadable = (artifact, why) => ({
|
|
165
|
-
exit: 2,
|
|
166
|
-
text: `Not checked: the Lean result could not be read (${why}). The full result is in ${artifact}.
|
|
167
|
-
`
|
|
168
|
-
});
|
|
169
|
-
var EngineError2 = z2.object({ ok: z2.literal(false), error: z2.string().min(1), proof: z2.undefined().optional() });
|
|
170
|
-
var issue = (error) => {
|
|
171
|
-
const first = error.issues[0];
|
|
172
|
-
return first.path.length ? `${first.path.join(".")}: ${first.message}` : first.message;
|
|
173
|
-
};
|
|
174
|
-
function leanSummary(raw, artifact, timeoutMs, files) {
|
|
175
|
-
const refused = EngineError2.safeParse(raw);
|
|
176
|
-
if (refused.success) return unreadable(artifact, `the engine said: ${refused.data.error}`);
|
|
177
|
-
const parsed = LeanCheckResult.safeParse(raw);
|
|
178
|
-
if (!parsed.success) return unreadable(artifact, issue(parsed.error));
|
|
179
|
-
const proof = parsed.data.proof;
|
|
180
|
-
const laws = proof.obligations;
|
|
181
|
-
const lines = [];
|
|
182
|
-
if (proof.status === "verified") {
|
|
183
|
-
lines.push(`Proven: ${plural(laws.length, "law")} verified by the pinned Lean Wasm kernel.`);
|
|
184
|
-
for (const row of laws) {
|
|
185
|
-
if (row.status !== "verified") continue;
|
|
186
|
-
const { check, witness, domain } = row.proof.subject;
|
|
187
|
-
lines.push(` ${row.binding}: ${check} holds for ${domain}, and ${witness} holds for at least one.`);
|
|
188
|
-
}
|
|
189
|
-
lines.push(`The proof receipt is in ${artifact}. It is a local Wasm kernel check (independentKernelReplay: false).`);
|
|
190
|
-
return { exit: 0, text: `${lines.join("\n")}
|
|
191
|
-
` };
|
|
192
|
-
}
|
|
193
|
-
if (!laws.length) {
|
|
194
|
-
const cause = proof.cause;
|
|
195
|
-
const checker2 = SIDE[cause] === "checker";
|
|
196
|
-
lines.push(`${checker2 ? "Not proven" : "Not checked"}: ${proof.reason}.`);
|
|
197
|
-
if (cause === "not-admitted") lines.push("Fix the source, or add a Yichus/Law Obligation for lean to prove.");
|
|
198
|
-
else lines.push(...remedy(cause, timeoutMs, files));
|
|
199
|
-
lines.push(`The full result is in ${artifact}.`);
|
|
200
|
-
return { exit: checker2 ? 1 : 2, text: `${lines.join("\n")}
|
|
201
|
-
` };
|
|
202
|
-
}
|
|
203
|
-
const failed = laws.flatMap((row) => row.status === "inconclusive" ? [row] : []);
|
|
204
|
-
const causes = [...new Set(failed.map((row) => row.cause))];
|
|
205
|
-
const checker = causes.some((cause) => SIDE[cause] === "checker");
|
|
206
|
-
lines.push(checker ? `Not proven: ${failed.length} of ${plural(laws.length, "law")} did not verify.` : `Not checked: ${failed.length} of ${plural(laws.length, "law")} could not be checked.`);
|
|
207
|
-
for (const row of laws) {
|
|
208
|
-
if (row.status === "verified") {
|
|
209
|
-
lines.push(` verified ${row.binding}`);
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
lines.push(` ${row.cause.padEnd(12)} ${row.binding}: ${row.reason}.`);
|
|
213
|
-
if (row.diagnostics?.length) lines.push(" Lean said:", ...row.diagnostics.map((d) => indent2(d, " ")));
|
|
214
|
-
}
|
|
215
|
-
for (const cause of causes) lines.push(...remedy(cause, timeoutMs, files));
|
|
216
|
-
lines.push(`The full result is in ${artifact}.`);
|
|
217
|
-
return { exit: checker ? 1 : 2, text: `${lines.join("\n")}
|
|
218
|
-
` };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// src/call-summary.ts
|
|
222
|
-
var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
223
|
-
function mapSummary(result) {
|
|
224
|
-
const parsed = AbstractionMap.safeParse(result);
|
|
225
|
-
if (!parsed.success) return ["The map has no readable measures block; read the JSON for its contents."];
|
|
226
|
-
const m = parsed.data.measures;
|
|
227
|
-
const lines = [
|
|
228
|
-
`Layers: ${plural2(m.definitions, "definition")} stacked ${plural2(m.layers, "layer")} deep (a definition sits one layer above the highest one it uses).`,
|
|
229
|
-
m.layerSkips === 0 ? `Layer skips: none of the ${plural2(m.edges, "dependency edge")} reaches past the layer just below.` : `Layer skips: ${m.layerSkips} of ${plural2(m.edges, "dependency edge")} reach past the layer just below, using a definition two or more layers down; they are the "edges" with "span" above 1.`,
|
|
230
|
-
`Reuse: ${plural2(m.usedMany, "definition is", "definitions are")} used by several others, ${m.usedOnce} by exactly one (each node's "fanIn").`,
|
|
231
|
-
m.clonePairs === 0 ? "Copies: no two definitions have the same compiled body." : `Copies: ${plural2(m.clonePairs, "pair")} of definitions have the same compiled body; see "clonePairs".`,
|
|
232
|
-
m.duplicatedMass === 0 ? "Repeats: no block of code repeats inside a definition." : `Repeats: blocks repeated inside one definition add ${plural2(m.duplicatedMass, "node")}; nodes with "repeatedBlockMass" above 0 list each block's places in "repeatedBlocks".`
|
|
233
|
-
];
|
|
234
|
-
if (m.largestDefinition !== void 0)
|
|
235
|
-
lines.push(`Largest definition: ${m.largestDefinition} (${m.maxSize} nodes).`);
|
|
236
|
-
return lines;
|
|
237
|
-
}
|
|
238
|
-
var SUMMARIES = {
|
|
239
|
-
api_abstraction_map: {
|
|
240
|
-
noun: "the abstraction map",
|
|
241
|
-
progress: (files) => `Mapping ${files === void 0 ? "the program" : plural2(files, "Bosatsu file")}: compiling it and measuring every definition, which can take half a minute or more for a large program...`,
|
|
242
|
-
summary: mapSummary
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
function callProgress(tool, files) {
|
|
246
|
-
const known = SUMMARIES[tool];
|
|
247
|
-
return known ? `${known.progress(files)}
|
|
248
|
-
` : void 0;
|
|
249
|
-
}
|
|
250
|
-
function callSummary(tool, result, output) {
|
|
251
|
-
const known = SUMMARIES[tool];
|
|
252
|
-
const lines = [];
|
|
253
|
-
if (output) lines.push(`Wrote ${known?.noun ?? `the ${tool} result`} to ${output}.`);
|
|
254
|
-
if (known) lines.push(...known.summary(result));
|
|
255
|
-
return lines.length ? `${lines.join("\n")}
|
|
256
|
-
` : void 0;
|
|
257
|
-
}
|
|
258
|
-
var NEXT_STEP = {
|
|
259
|
-
"duplicate-package": "Each package can be declared by only one file. Give each file its own package name, or pass only one of them."
|
|
260
|
-
};
|
|
261
|
-
function callFailure(tool, result, artifact) {
|
|
262
|
-
const said = EngineError.safeParse(result);
|
|
263
|
-
const lines = said.success ? [`${tool} failed:`, said.data.error] : [`${tool} failed without saying why.`];
|
|
264
|
-
if (said.success) {
|
|
265
|
-
for (const kind of said.data.errorKinds ?? []) if (Object.hasOwn(NEXT_STEP, kind)) lines.push(`Next: ${NEXT_STEP[kind]}`);
|
|
266
|
-
}
|
|
267
|
-
lines.push(`The full result is in ${artifact}.`);
|
|
268
|
-
return `${lines.join("\n")}
|
|
269
|
-
`;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// src/studio/command.ts
|
|
273
|
-
import fs7 from "fs";
|
|
274
|
-
import path7 from "path";
|
|
275
|
-
import { parseArgs } from "util";
|
|
276
|
-
|
|
277
|
-
// src/studio/director.ts
|
|
278
|
-
import fs3 from "fs";
|
|
279
|
-
import path3 from "path";
|
|
280
|
-
import { z as z6 } from "zod";
|
|
281
|
-
|
|
282
|
-
// ../../web/system-map-record.js
|
|
283
|
-
var MARKS_VERSION = "yichus-marks-3";
|
|
284
|
-
var VOCABULARY = {
|
|
285
|
-
version: MARKS_VERSION,
|
|
286
|
-
fixedPlaces: {
|
|
287
|
-
callers: "left of the wall: one glyph per transport kind, HTTP above, importer below, each with a bracket over the openings it calls; the kind the program lacks is the same glyph struck through <- callers[], ghostCallers[]",
|
|
288
|
-
openings: "the left wall: one opening per entry <- entries[]",
|
|
289
|
-
hullBand: "the middle band of the wall: wrappers sit here and nowhere else; empty band = no entry goes through a shared wrapper <- wrappers[]",
|
|
290
|
-
afterBand: "between the hull band and the reply bars: work an entry does after its wrapper returns, with its stations and checks; empty = none <- entries[].beside",
|
|
291
|
-
replyBars: "one bar per entry at the same x, past the hull band <- entries[]",
|
|
292
|
-
rail: "the bottom of the wall: a handle handed in from outside, with a riser to each use; a ? on the rail where a use is handed in on some paths only <- uses[how=handed|mixed]",
|
|
293
|
-
rack: "right of the wall: db, model, net, always in this order; struck = the engine checked and found none <- resources[], ghostResources[]",
|
|
294
|
-
programBand: "the bottom of the wall, under the rail: one container per pure program, its request openings on its right edge, the engine lanes right of it; empty = the engine found no program <- programs[]",
|
|
295
|
-
quietLine: "bottom inside the wall: parts on no entry path, one stretch per package, with their forks, sockets, generic letters and steps <- keyed[], library.applied[], steps[]"
|
|
296
|
-
},
|
|
297
|
-
marks: {
|
|
298
|
-
entryHead: "arrowhead at an opening; solid = the entry writes, hollow = it only reads <- entries[].writes",
|
|
299
|
-
lock: "closed padlock by the path: sign-in with a secret key; open padlock with a raised shackle: a publishable key may call it <- entries[].authority",
|
|
300
|
-
role: "red word beside the lock: a role beyond sign-in <- entries[].roles",
|
|
301
|
-
limit: "gauge beside the path: a declared rate limit <- entries[].limits",
|
|
302
|
-
station: "ring on a path: code touching a table, with the verb and table beside it; filled = writes <- entries[].reach, wrappers[].effects, entries[].beside.effects",
|
|
303
|
-
modelRing: "violet ring: the entry reaches the model <- entries[].model",
|
|
304
|
-
reachedBy: "under each resource in the rack: how many routes reach it, the resource drawn at that share of the routes (never smaller than its name) <- entries[].db, model, net, beside.resources",
|
|
305
|
-
netRing: "orange ring: the entry reaches the network <- entries[].net",
|
|
306
|
-
guard: "red diamond on the path where the check happens, its name above, failure stem below <- entries[].guards, wrappers[].guards, entries[].passed[].guards, entries[].beside.guards",
|
|
307
|
-
hull: "translucent region in the hull band: paths merge into it, run one shared stretch, and split; everything inside it runs inside the wrapper <- wrappers[]",
|
|
308
|
-
socket: "broken-rim ring: a function the caller hands in runs here; its centre fills like a station <- entries[].passed[]",
|
|
309
|
-
token: "small grey word under a path: the value the caller fixes for the wrapper <- entries[].policy",
|
|
310
|
-
afterDot: "dot in the after band: a call the entry makes after its wrapper returns <- entries[].beside.calls",
|
|
311
|
-
builder: "plus-disc under a station with the call that builds the handle there <- uses[how=local].builtBy",
|
|
312
|
-
unknown: 'a ? in a circle on the rail, with "partly traced" beside it and a sentence on hover: the engine traced the handle only on some paths <- uses[how=mixed]',
|
|
313
|
-
fork: "three prongs and a type: a choice keyed on a plain String or Int; under the mark that makes it, else on the quiet line <- keyed[]",
|
|
314
|
-
bundle: 'one cable with how many routes it carries ("4 routes"): entries with the same wrapper and identical marks; its title names every route; opening it draws each one <- bundles[]',
|
|
315
|
-
anchor: "where the rail starts: platform when no definition of the program can make the handle; ? when the trace stops and the program could <- uses[].anchor",
|
|
316
|
-
step: 'loop arc with a type on the quiet line: functions of the package take that type and return it; gathered, a count in words ("8 steps"), as are choices, db uses and generic parts <- steps[]',
|
|
317
|
-
loop: "loop arc with package names: packages whose types lead back to themselves <- phases.cycles",
|
|
318
|
-
composes: "hull round a chain: a definition takes the type the chain hands in at each step and applies it <- library.applied[].fedBy",
|
|
319
|
-
port: "plug strip on the right: a record of functions; each adapter a plug, hollow when nothing plugs it in <- library.ports[]",
|
|
320
|
-
generic: "the letter \u2200 with its variables over a part that never looks inside what it carries <- library.generic[]",
|
|
321
|
-
program: `large container: a pure program, its step and package named, with its package's own definition count (as the layer stack counts them) and how many more it uses from other packages, the most used named with their counts; routes that act on one step's answer per call are named as its starters ("one step per call of"), and routes that only replay it are named on hover <- programs[], definitions, usesElsewhere, elsewhere[], starters[advances=false], replays[]`,
|
|
322
|
-
decision: "bar and number in the program: its decisions, on one scale shared with its engines' bars <- programs[].decisions",
|
|
323
|
-
struck: 'on a program, a struck "effects": no definition in its package performs an effect <- programs[].beside = []',
|
|
324
|
-
programEffect: "red disc and count on a program: definitions in its own package that perform effects, named in its title <- programs[].beside[]",
|
|
325
|
-
request: "opening on the program's right edge: a request constructor its step can return, with its payload's types <- programs[].requests[]",
|
|
326
|
-
variant: 'short opening under a request: a constructor of a user sum type its payload carries, with what the program calls it ("BriefCase \u2192 Lawyer") where a total match maps each kind to a plain constructor; eight drawn, the rest counted with their names in a title <- programs[].requests[].variants[], namings[]',
|
|
327
|
-
starter: "dashed line from a route's reply bar into the program top: the route calls the step with its values and acts on the answer, found by value flow; a route that runs an engine is named under its loop instead <- programs[].starters[advances=false]",
|
|
328
|
-
gap: "struck opening: a request constructor no engine handles <- programs[].gaps[]",
|
|
329
|
-
wire: "line from an opening to the lane that handles it, ending in the names of the resources the arm reaches, or a dot each where they do not fit <- programs[].requests[].handledBy",
|
|
330
|
-
lane: `thin vertical line on the resource side: an engine that performs the program's requests, its package above it and what it does written from its type (its own parameters \u2192 what its effect yields), its code name and whole type on hover; it ends in the resources its arms reach, then dashed pills under "also" for resources it reaches for no request <- programs[].engines[], pkgLabel, type, resources, also`,
|
|
331
|
-
laneFork: "bar and number on a lane: the engine's decisions, on the program's scale; 0 = a rule-free engine <- programs[].engines[].decisions",
|
|
332
|
-
replyLoop: `one solid arc from a lane's foot back into the program, with one loop node naming what repeats the step within one call, and "each call of \u2026 runs it" naming the routes that run the rounds <- programs[].engines[].replies, loop, starters`,
|
|
333
|
-
replaced: 'a check named "(in place of \u2026)": in the compared base record the same route had that other check at the same place, and it is on that route no longer; says nothing about whether the two do the same <- compare.replaced[]'
|
|
334
|
-
},
|
|
335
|
-
rule: "every guard, wrapper, socket, fork, handle, station, step and resource the engine records is drawn at its fixed place, in every view; where nothing is drawn, the engine checked and found none."
|
|
336
|
-
};
|
|
337
|
-
var bare = (id) => String(id).split("::").pop();
|
|
338
|
-
var pkgOf = (id) => String(id).split("::")[0];
|
|
339
|
-
var uniq = (xs) => [...new Set(xs)];
|
|
340
|
-
var OPS = { read: "read", query: "read", delete: "delete" };
|
|
341
|
-
function effectsOf(steps) {
|
|
342
|
-
return (steps || []).filter((e) => e.resource === "db").flatMap((e) => e.tables.map((table) => ({ op: OPS[e.op] || "write", table })));
|
|
343
|
-
}
|
|
344
|
-
var isWrite = (op) => op !== "read";
|
|
345
|
-
var uniqBy = (xs, key) => {
|
|
346
|
-
const seen = /* @__PURE__ */ new Set();
|
|
347
|
-
return xs.filter((x) => !seen.has(key(x)) && seen.add(key(x)));
|
|
348
|
-
};
|
|
349
|
-
var RUNNER = { CompileProgram: "compile", RunProgram: "run" };
|
|
350
|
-
var effectResource = (e) => e.resource || RUNNER[e.kind] || "io";
|
|
351
|
-
var LANE_RES = ["db", "model", "net", "compile", "run", "io"];
|
|
352
|
-
var resOrder = (rs) => LANE_RES.filter((r) => rs.includes(r));
|
|
353
|
-
function programsOf(st, layers = null) {
|
|
354
|
-
const found = st.programs && st.programs.programs || [];
|
|
355
|
-
const ownCount = (pkg) => {
|
|
356
|
-
const lp = layers && layers.packages.find((x) => x.name === pkg);
|
|
357
|
-
return lp ? lp.definitions.length : null;
|
|
358
|
-
};
|
|
359
|
-
return found.map((p) => {
|
|
360
|
-
const engines = p.engines.map((e) => {
|
|
361
|
-
const resources = resOrder(uniq(e.handles.flatMap((h) => h.effects.map(effectResource))));
|
|
362
|
-
return {
|
|
363
|
-
id: e.id,
|
|
364
|
-
label: bare(e.id),
|
|
365
|
-
pkg: pkgOf(e.id),
|
|
366
|
-
pkgLabel: pkgOf(e.id).split("/").pop(),
|
|
367
|
-
handles: e.handles.map((h) => h.constructor),
|
|
368
|
-
resources,
|
|
369
|
-
// effects the engine performs that are no request's: drawn, not hidden
|
|
370
|
-
also: resOrder(uniq((e.effects || []).map(effectResource))).filter((r) => !resources.includes(r)),
|
|
371
|
-
replies: resOrder(uniq(e.replies.map(effectResource))),
|
|
372
|
-
decisions: +e.decisions,
|
|
373
|
-
entries: e.entries,
|
|
374
|
-
truncated: e.truncated,
|
|
375
|
-
loop: e.loop ? { kind: e.loop.kind, by: e.loop.by, label: e.loop.by ? bare(e.loop.by) : null } : { kind: "sequence", by: null, label: null },
|
|
376
|
-
starters: (p.starters || []).filter((s) => s.advances && s.via.some((v) => v === e.id || e.entries.includes(s.entry))).map((s) => s.entry),
|
|
377
|
-
// what it does, from its type: the program's own and standard parameters, and what its effect yields; the whole type on hover
|
|
378
|
-
type: {
|
|
379
|
-
takes: e.labels.takes,
|
|
380
|
-
returns: e.labels.returns,
|
|
381
|
-
plainTakes: e.labels.plainTakes,
|
|
382
|
-
yields: e.labels.yields,
|
|
383
|
-
from: "structure.programs.programs[].engines[].labels"
|
|
384
|
-
},
|
|
385
|
-
from: "structure.programs.programs[].engines[]"
|
|
386
|
-
};
|
|
387
|
-
});
|
|
388
|
-
const requests = p.requests.map((q) => ({
|
|
389
|
-
constructor: q.constructor,
|
|
390
|
-
fields: q.fields.map((f) => f.label),
|
|
391
|
-
variants: (q.variants || []).map((v) => ({
|
|
392
|
-
path: v.path,
|
|
393
|
-
type: bare(v.type),
|
|
394
|
-
constructors: v.constructors,
|
|
395
|
-
// what the program calls each kind: total matches onto plain constructors, the type's own package first
|
|
396
|
-
namings: (v.namings || []).slice().sort((a, b) => (pkgOf(b.definition) === pkgOf(v.type)) - (pkgOf(a.definition) === pkgOf(v.type))).map((n) => ({
|
|
397
|
-
definition: n.definition,
|
|
398
|
-
label: bare(n.definition),
|
|
399
|
-
type: bare(n.type),
|
|
400
|
-
names: n.names.map((x) => ({ constructor: x.constructor, name: x.name })),
|
|
401
|
-
from: "structure.programs.programs[].requests[].variants[].namings[]"
|
|
402
|
-
})),
|
|
403
|
-
from: "structure.programs.programs[].requests[].variants[]"
|
|
404
|
-
})),
|
|
405
|
-
handledBy: p.engines.filter((e) => e.handles.some((h) => h.constructor === q.constructor)).map((e) => ({
|
|
406
|
-
engine: e.id,
|
|
407
|
-
resources: resOrder(uniq(e.handles.find((h) => h.constructor === q.constructor).effects.map(effectResource)))
|
|
408
|
-
})),
|
|
409
|
-
gap: p.gaps.some((g) => g.constructor === q.constructor),
|
|
410
|
-
from: "structure.programs.programs[].requests[] + engines[].handles[]"
|
|
411
|
-
}));
|
|
412
|
-
return {
|
|
413
|
-
id: p.step,
|
|
414
|
-
label: bare(p.step),
|
|
415
|
-
pkg: p.package,
|
|
416
|
-
pkgLabel: p.package.split("/").pop(),
|
|
417
|
-
result: bare(p.result),
|
|
418
|
-
requestType: bare(p.requestType),
|
|
419
|
-
carrier: p.carrier,
|
|
420
|
-
decisions: +p.decisions,
|
|
421
|
-
finishes: p.finishes,
|
|
422
|
-
// definitions: the package's own, as the layer stack counts them; reached: everything the program's definitions use
|
|
423
|
-
definitions: ownCount(p.package) ?? +(p.ownDefinitions || 0),
|
|
424
|
-
reached: p.definitions.length,
|
|
425
|
-
// of what it reaches, the definitions that live in other packages
|
|
426
|
-
usesElsewhere: p.definitions.filter((d) => pkgOf(d) !== p.package).length,
|
|
427
|
-
// and which packages those are, the most used first
|
|
428
|
-
elsewhere: Object.entries(p.definitions.filter((d) => pkgOf(d) !== p.package).reduce((m, d) => ({ ...m, [pkgOf(d)]: (m[pkgOf(d)] || 0) + 1 }), {})).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([pkg, n]) => ({ pkg, label: pkg.split("/").pop(), n, from: "structure.programs.programs[].definitions[]" })),
|
|
429
|
-
requests,
|
|
430
|
-
engines,
|
|
431
|
-
starters: (p.starters || []).map((s) => ({
|
|
432
|
-
entry: s.entry,
|
|
433
|
-
path: s.entry.replace(/^route:/, ""),
|
|
434
|
-
advances: s.advances,
|
|
435
|
-
via: s.via,
|
|
436
|
-
from: "structure.programs.programs[].starters[]"
|
|
437
|
-
})),
|
|
438
|
-
// routes that call the step and act on nothing it says: they read where the program is
|
|
439
|
-
replays: (p.replays || []).map((s) => ({ entry: s.entry, path: s.entry.replace(/^route:/, ""), via: s.via, from: "structure.programs.programs[].replays[]" })),
|
|
440
|
-
// what each reaches, as the structure lens's effect sites say (io when none is a resource)
|
|
441
|
-
beside: (p.beside || []).map((b) => ({
|
|
442
|
-
id: b.definition,
|
|
443
|
-
label: bare(b.definition),
|
|
444
|
-
kinds: b.kinds,
|
|
445
|
-
resources: resOrder(uniq((st.ioKinds || []).filter((k) => k.definition === b.definition).flatMap((k) => k.resources))),
|
|
446
|
-
from: "structure.programs.programs[].beside[], structure.ioKinds[].resources"
|
|
447
|
-
})),
|
|
448
|
-
gaps: p.gaps.map((g) => ({ constructor: g.constructor, from: "structure.programs.programs[].gaps[]" })),
|
|
449
|
-
from: "structure.programs.programs[]"
|
|
450
|
-
};
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
var outsideResourcesOf = (steps) => uniq((steps || []).map((e) => e.resource).filter((r) => r === "model" || r === "net"));
|
|
454
|
-
function buildSystemMap(engine, { program }) {
|
|
455
|
-
for (const [k, v] of Object.entries(engine)) if (!v.ok) throw new Error(`${k}: ${v.error}`);
|
|
456
|
-
const report = engine.report, st = engine.structure, abs = engine.abstractions.abstractions, layers = engine.layers.layers;
|
|
457
|
-
const routes = (report.surface.routes || []).slice().sort((a, b) => a.path.localeCompare(b.path));
|
|
458
|
-
const engEntry = new Map(st.entries.map((e) => [e.entry, e]));
|
|
459
|
-
const shapes = new Map(abs.routeShapes.map((r) => [r.path, r]));
|
|
460
|
-
const gens = new Map(abs.generators.map((g) => [g.id, g]));
|
|
461
|
-
const insts = abs.instantiations;
|
|
462
|
-
const besideOf = (inst) => ({
|
|
463
|
-
calls: inst.besideCalls.map(bare),
|
|
464
|
-
callIds: inst.besideCalls,
|
|
465
|
-
guards: inst.besideGuards.map(bare),
|
|
466
|
-
guardIds: inst.besideGuards,
|
|
467
|
-
effects: effectsOf(inst.besideEffectSteps),
|
|
468
|
-
resources: outsideResourcesOf(inst.besideEffectSteps),
|
|
469
|
-
from: "abstractions.instantiations[].beside*"
|
|
470
|
-
});
|
|
471
|
-
const entries = routes.map((r) => {
|
|
472
|
-
const key = `route:${r.path}`;
|
|
473
|
-
const ee = engEntry.get(key);
|
|
474
|
-
const rs = shapes.get(r.path);
|
|
475
|
-
const wrapper = rs.shapedByGenerator;
|
|
476
|
-
const inst = wrapper ? insts.find((i) => i.callerId === rs.handlerId && i.generatorId === wrapper) : null;
|
|
477
|
-
const passed = (inst ? inst.slotBindings : []).filter((sb) => ["function", "effect"].includes(sb.slotRole)).map((sb) => ({
|
|
478
|
-
slot: sb.slotName,
|
|
479
|
-
id: sb.targetId || null,
|
|
480
|
-
label: sb.targetId ? bare(sb.targetId) : null,
|
|
481
|
-
kind: sb.passedKind,
|
|
482
|
-
guards: (sb.guards || []).map(bare),
|
|
483
|
-
guardIds: sb.guards || [],
|
|
484
|
-
effects: effectsOf(sb.effectSteps),
|
|
485
|
-
from: "abstractions.instantiations[].slotBindings[slotRole=function|effect]"
|
|
486
|
-
}));
|
|
487
|
-
const reach = wrapper ? [
|
|
488
|
-
...effectsOf(rs.effectsInGeneratorSteps).map((e) => ({ ...e, where: "wrapper", from: "abstractions.routeShapes[].effectsInGeneratorSteps" })),
|
|
489
|
-
...effectsOf(rs.effectsInPassedFunctionSteps).map((e) => ({ ...e, where: "passed", from: "abstractions.routeShapes[].effectsInPassedFunctionSteps" })),
|
|
490
|
-
...effectsOf(rs.effectsDirectOrBesideSteps).map((e) => ({ ...e, where: "beside", from: "abstractions.routeShapes[].effectsDirectOrBesideSteps" }))
|
|
491
|
-
] : uniqBy([
|
|
492
|
-
...effectsOf(r.inferredSteps).map((e) => ({ ...e, where: "direct", from: "report.surface.routes[].inferredSteps" })),
|
|
493
|
-
...effectsOf(rs.effectsDirectOrBesideSteps).map((e) => ({ ...e, where: "direct", from: "abstractions.routeShapes[].effectsDirectOrBesideSteps" }))
|
|
494
|
-
], (e) => `${e.op} ${e.table}`);
|
|
495
|
-
const limits = ee.limits.written === "literal" ? ee.limits.each.map((l) => ({ requests: +l.requests, seconds: +l.seconds, by: l.by })) : [];
|
|
496
|
-
return {
|
|
497
|
-
key,
|
|
498
|
-
path: r.path,
|
|
499
|
-
handler: r.handler,
|
|
500
|
-
handlerId: rs.handlerId,
|
|
501
|
-
transport: ee.transport,
|
|
502
|
-
authority: ee.authority,
|
|
503
|
-
signedIn: !["public", "anonymous"].includes(ee.authority),
|
|
504
|
-
roles: r.requiredRoles || [],
|
|
505
|
-
limits,
|
|
506
|
-
limitComputed: ee.limits.written === "computed",
|
|
507
|
-
reachKinds: ee.reaches,
|
|
508
|
-
db: ee.resources.includes("db"),
|
|
509
|
-
model: ee.resources.includes("model"),
|
|
510
|
-
net: ee.resources.includes("net"),
|
|
511
|
-
writes: reach.some((e) => isWrite(e.op)),
|
|
512
|
-
guards: rs.handlerGuards.map(bare),
|
|
513
|
-
guardIds: rs.handlerGuards,
|
|
514
|
-
wrapper,
|
|
515
|
-
via: rs.viaSpecialization ? bare(rs.viaSpecialization) : null,
|
|
516
|
-
policy: (inst ? inst.slotBindings : []).filter((s) => s.slotRole === "semantic-object").map((s) => s.value).filter(Boolean),
|
|
517
|
-
passed,
|
|
518
|
-
beside: inst ? besideOf(inst) : null,
|
|
519
|
-
reach,
|
|
520
|
-
from: { entry: "report.surface.routes[]", transport: "structure.entries[]", shape: "abstractions.routeShapes[]", head: "entries[].reach (writes)" }
|
|
521
|
-
};
|
|
522
|
-
});
|
|
523
|
-
const partOf = (g) => {
|
|
524
|
-
const own = insts.filter((i) => i.generatorId === g.id);
|
|
525
|
-
return {
|
|
526
|
-
id: g.id,
|
|
527
|
-
label: g.name || bare(g.id),
|
|
528
|
-
pkg: pkgOf(g.id),
|
|
529
|
-
guards: g.envelope.guards.map(bare),
|
|
530
|
-
guardIds: g.envelope.guards,
|
|
531
|
-
effects: effectsOf(g.envelope.effectSteps),
|
|
532
|
-
groups: (g.parameterGroups || []).map((grp) => grp.map((sl) => ({ name: sl.name, role: sl.role, type: sl.type, guardsFed: (sl.guardsFed || []).map(bare) }))),
|
|
533
|
-
instantiationCount: own.length,
|
|
534
|
-
instantiatingPackages: uniq(own.map((i) => pkgOf(i.callerId))).sort(),
|
|
535
|
-
besideEmpty: own.every((i) => !i.besideCalls.length && !i.besideGuards.length && !i.besideEffects.length),
|
|
536
|
-
callers: own.map((i) => ({
|
|
537
|
-
id: i.callerId,
|
|
538
|
-
label: i.callerName || bare(i.callerId),
|
|
539
|
-
pkg: pkgOf(i.callerId),
|
|
540
|
-
routes: i.routes || [],
|
|
541
|
-
via: i.viaSpecialization ? bare(i.viaSpecialization) : null,
|
|
542
|
-
tokens: i.slotBindings.filter((s) => s.slotRole === "semantic-object").map((s) => s.value).filter(Boolean),
|
|
543
|
-
passed: i.slotBindings.filter((s) => ["function", "effect"].includes(s.slotRole)).map((s) => ({
|
|
544
|
-
slot: s.slotName,
|
|
545
|
-
id: s.targetId || null,
|
|
546
|
-
label: s.targetId ? bare(s.targetId) : null,
|
|
547
|
-
guards: (s.guards || []).map(bare),
|
|
548
|
-
guardIds: s.guards || [],
|
|
549
|
-
effects: effectsOf(s.effectSteps)
|
|
550
|
-
})),
|
|
551
|
-
beside: besideOf(i)
|
|
552
|
-
})),
|
|
553
|
-
from: "abstractions.generators[] + abstractions.instantiations[generatorId]"
|
|
554
|
-
};
|
|
555
|
-
};
|
|
556
|
-
const parts = abs.generators.filter((g) => insts.some((i) => i.generatorId === g.id)).map(partOf);
|
|
557
|
-
const wrapperIds = uniq(entries.map((e) => e.wrapper).filter(Boolean)).sort();
|
|
558
|
-
const wrappers = wrapperIds.map((id) => ({
|
|
559
|
-
...parts.find((x) => x.id === id) || partOf(gens.get(id)),
|
|
560
|
-
entries: entries.filter((e) => e.wrapper === id).map((e) => e.key),
|
|
561
|
-
from: "abstractions.generators[] shaping routeShapes[].shapedByGenerator"
|
|
562
|
-
}));
|
|
563
|
-
const handlerToEntry = new Map(entries.map((e) => [e.handlerId, e.key]));
|
|
564
|
-
const passedToEntry = new Map(entries.flatMap((e) => e.passed.filter((p) => p.id).map((p) => [p.id, e.key])));
|
|
565
|
-
const uses = [];
|
|
566
|
-
for (const s of st.supply) {
|
|
567
|
-
const kind = s.via === "record" ? "record" : s.resource;
|
|
568
|
-
if (kind !== "db" && kind !== "record") continue;
|
|
569
|
-
const ids = /* @__PURE__ */ new Set([s.definition]);
|
|
570
|
-
for (const t of s.traces) {
|
|
571
|
-
for (const id of t.through) ids.add(id);
|
|
572
|
-
if (t.end.where) ids.add(t.end.where);
|
|
573
|
-
if (t.end.referrer) ids.add(t.end.referrer);
|
|
574
|
-
}
|
|
575
|
-
let ats;
|
|
576
|
-
if (handlerToEntry.has(s.definition)) ats = [{ node: "entry", entry: handlerToEntry.get(s.definition) }];
|
|
577
|
-
else if (wrapperIds.includes(s.definition)) ats = [{ node: "wrapper", wrapper: s.definition }];
|
|
578
|
-
else if (passedToEntry.has(s.definition)) ats = [{ node: "passed", entry: passedToEntry.get(s.definition), id: s.definition }];
|
|
579
|
-
else {
|
|
580
|
-
const ents = uniq([...ids].map((i) => handlerToEntry.get(i)).filter(Boolean));
|
|
581
|
-
ats = ents.length ? ents.map((entry) => ({ node: "entry", entry, helper: bare(s.definition) })) : [{ node: "definition", id: s.definition }];
|
|
582
|
-
}
|
|
583
|
-
const builtBy = s.traces.map((t) => t.end.kind === "builds" && t.end.by ? bare(t.end.by) : t.end.kind === "reachesFor" ? bare(t.end.definition) : null).find(Boolean) || null;
|
|
584
|
-
const anchor = !s.leaves ? "none" : s.leaves.builtBy.length ? "program" : "platform";
|
|
585
|
-
for (const at of ats) uses.push({
|
|
586
|
-
kind,
|
|
587
|
-
how: s.supply,
|
|
588
|
-
definition: s.definition,
|
|
589
|
-
effect: s.effect || null,
|
|
590
|
-
writes: s.via === "effect" && isWrite(OPS[s.op] || "write"),
|
|
591
|
-
at,
|
|
592
|
-
builtBy,
|
|
593
|
-
untracedSource: s.traces.some((t) => ["asValue", "entry"].includes(t.end.kind)),
|
|
594
|
-
anchor,
|
|
595
|
-
handleBuiltBy: s.leaves ? s.leaves.builtBy : [],
|
|
596
|
-
records: s.records || [],
|
|
597
|
-
ends: s.traces.map((t) => ({ kind: t.end.kind, where: t.end.where || null, referrer: t.end.referrer || null, definition: t.end.definition || null, by: t.end.by || null })),
|
|
598
|
-
from: "structure.supply[]"
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
const present = uniq(st.ioKinds.flatMap((b) => b.resources));
|
|
602
|
-
const tables = uniq(entries.flatMap((e) => [...e.reach, ...e.beside ? e.beside.effects : []].map((r) => r.table))).sort();
|
|
603
|
-
const resources = ["db", "model", "net"].filter((k) => present.includes(k)).map((k) => ({ kind: k, ...k === "db" ? { tables } : {}, from: "structure.ioKinds[].resources" }));
|
|
604
|
-
const ghostResources = ["db", "model", "net"].filter((k) => !present.includes(k)).map((k) => ({ kind: k, from: "structure.ioKinds[] checked: no site of this kind" }));
|
|
605
|
-
const transports = uniq(st.entries.map((e) => e.transport));
|
|
606
|
-
const callers = [];
|
|
607
|
-
if (transports.includes("http")) callers.push({ kind: "http", keyKinds: uniq(entries.map(keyKindOf)), from: "structure.entries[transport=http] + authority" });
|
|
608
|
-
if (transports.includes("link")) callers.push({ kind: "link", from: "structure.entries[transport=link]" });
|
|
609
|
-
const ghostCallers = ["http", "link"].filter((k) => !transports.includes(k)).map((k) => ({ kind: k, from: "structure.entries[]: no entry with this transport" }));
|
|
610
|
-
const library = routes.length ? null : {
|
|
611
|
-
packages: layers.packages.map((p) => ({ name: p.name, label: p.name.split("/").pop(), exports: p.definitions.filter((d) => d.exported).map((d) => d.name) })),
|
|
612
|
-
ports: st.ports.map((p) => ({
|
|
613
|
-
id: p.id,
|
|
614
|
-
label: bare(p.id),
|
|
615
|
-
pkg: pkgOf(p.id),
|
|
616
|
-
fields: p.fields,
|
|
617
|
-
heldBy: p.heldBy,
|
|
618
|
-
adapters: p.values.map((v) => ({ id: v.value, label: bare(v.value), usedBy: v.usedBy.map((u) => ({ id: u, label: bare(u), pkg: pkgOf(u) })) })),
|
|
619
|
-
from: "structure.ports[]"
|
|
620
|
-
})),
|
|
621
|
-
chains: st.chains.map((c) => ({
|
|
622
|
-
value: c.value,
|
|
623
|
-
label: bare(c.value),
|
|
624
|
-
pkg: pkgOf(c.value),
|
|
625
|
-
by: bare(c.by),
|
|
626
|
-
start: c.start.map((i) => i.id ? bare(i.id) : i.text),
|
|
627
|
-
steps: c.steps.map((s) => s.map((i) => ({ id: i.id || null, label: i.id ? bare(i.id) : i.text, pkg: i.id ? pkgOf(i.id) : null, applied: i.applied }))),
|
|
628
|
-
from: "structure.chains[]"
|
|
629
|
-
})),
|
|
630
|
-
combinations: st.combinations,
|
|
631
|
-
applied: st.applied.map((a) => ({
|
|
632
|
-
id: a.definition,
|
|
633
|
-
label: bare(a.definition),
|
|
634
|
-
pkg: pkgOf(a.definition),
|
|
635
|
-
fn: a.function,
|
|
636
|
-
insideBranch: a.insideBranch,
|
|
637
|
-
fedBy: a.fedBy,
|
|
638
|
-
from: "structure.applied[]"
|
|
639
|
-
})),
|
|
640
|
-
generic: st.generic.map((g) => ({ id: g.definition, label: bare(g.definition), vars: g.vars, signature: g.signature, from: "structure.generic[]" })),
|
|
641
|
-
exported: st.entries.filter((e) => e.transport === "link").map((e) => e.entry)
|
|
642
|
-
};
|
|
643
|
-
const steps = st.phases.phases.flatMap((ph) => ph.steps.map((s) => ({ pkg: ph.package, type: s.type, label: bare(s.type), by: s.by, from: "structure.phases.phases[].steps" })));
|
|
644
|
-
const keyed = st.keyed.map((k) => ({ id: k.definition, label: bare(k.definition), pkg: pkgOf(k.definition), types: uniq(k.parameters.map((p) => p.type)), from: "structure.keyed[]" }));
|
|
645
|
-
return {
|
|
646
|
-
schema: "system-map-2",
|
|
647
|
-
marks: MARKS_VERSION,
|
|
648
|
-
vocabulary: VOCABULARY,
|
|
649
|
-
program,
|
|
650
|
-
class: report.headline.class,
|
|
651
|
-
kind: routes.length ? "service" : "library",
|
|
652
|
-
callers,
|
|
653
|
-
ghostCallers,
|
|
654
|
-
entries,
|
|
655
|
-
wrappers,
|
|
656
|
-
parts,
|
|
657
|
-
uses,
|
|
658
|
-
resources,
|
|
659
|
-
ghostResources,
|
|
660
|
-
library,
|
|
661
|
-
keyed,
|
|
662
|
-
steps,
|
|
663
|
-
programs: programsOf(st, layers),
|
|
664
|
-
bundles: routes.length > 12 ? bundlesOf(entries, uses, keyed) : null,
|
|
665
|
-
phases: st.phases,
|
|
666
|
-
evidence: {
|
|
667
|
-
tools: ["api_report", "api_organize:abstractions", "api_organize:structure", "api_organize:layers"],
|
|
668
|
-
headline: report.headline,
|
|
669
|
-
derived: ["bundles: entries grouped by wrapper and identical marks"]
|
|
670
|
-
}
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
var keyKindOf = (e) => e.authority === "publishable" ? "publishable" : e.signedIn ? "secret" : "public";
|
|
674
|
-
function bundlesOf(entries, uses, keyed) {
|
|
675
|
-
const keyedIds = new Set(keyed.map((k) => k.id));
|
|
676
|
-
const sup = (e) => uniq(uses.filter((u) => u.kind === "db" && u.at.entry === e.key).flatMap((u) => [u.how, ...u.untracedSource ? [`?${u.anchor}`] : []])).sort().join(" ");
|
|
677
|
-
const fill = (l) => l.length ? l.some((x) => isWrite(x.op)) ? "W" : "R" : "-";
|
|
678
|
-
const sig = (e) => {
|
|
679
|
-
const p = e.passed[0], b = e.beside;
|
|
680
|
-
return JSON.stringify([
|
|
681
|
-
e.wrapper,
|
|
682
|
-
keyKindOf(e),
|
|
683
|
-
e.roles.length > 0,
|
|
684
|
-
e.writes,
|
|
685
|
-
e.guards.length > 0,
|
|
686
|
-
fill(e.reach.filter((r) => r.where === "direct")),
|
|
687
|
-
fill(e.reach.filter((r) => r.where === "passed")),
|
|
688
|
-
!!(p && p.guards.length),
|
|
689
|
-
e.model,
|
|
690
|
-
e.net,
|
|
691
|
-
sup(e),
|
|
692
|
-
e.limits.length > 0,
|
|
693
|
-
!!(b && b.calls.length),
|
|
694
|
-
!!(b && b.guards.length),
|
|
695
|
-
fill(b ? b.effects : []),
|
|
696
|
-
e.policy.length > 0,
|
|
697
|
-
[e.handlerId, ...e.guardIds, ...p ? [p.id, ...p.guardIds] : []].some((id) => keyedIds.has(id))
|
|
698
|
-
]);
|
|
699
|
-
};
|
|
700
|
-
const groups = /* @__PURE__ */ new Map();
|
|
701
|
-
for (const e of entries) {
|
|
702
|
-
const k = sig(e);
|
|
703
|
-
if (!groups.has(k)) groups.set(k, []);
|
|
704
|
-
groups.get(k).push(e.key);
|
|
705
|
-
}
|
|
706
|
-
const pathOf = new Map(entries.map((e) => [e.key, e.path]));
|
|
707
|
-
return [...groups.values()].map((keys, i) => ({
|
|
708
|
-
id: `bundle:${i + 1}`,
|
|
709
|
-
entries: keys,
|
|
710
|
-
count: keys.length,
|
|
711
|
-
label: pathPattern(keys.map((k) => pathOf.get(k))),
|
|
712
|
-
from: "derived: entries[] grouped by wrapper and identical marks"
|
|
713
|
-
}));
|
|
714
|
-
}
|
|
715
|
-
function pathPattern(paths) {
|
|
716
|
-
if (paths.length === 1) return paths[0];
|
|
717
|
-
const segs = paths.map((p) => p.split("/"));
|
|
718
|
-
const n = Math.min(...segs.map((s) => s.length));
|
|
719
|
-
const out = [];
|
|
720
|
-
for (let i = 0; i < n; i++) {
|
|
721
|
-
const v = uniq(segs.map((s) => s[i]));
|
|
722
|
-
out.push(v.length === 1 ? v[0] : "*");
|
|
723
|
-
}
|
|
724
|
-
if (segs.every((s) => s.length === n)) return out.join("/");
|
|
725
|
-
let k = 0;
|
|
726
|
-
while (k < n && out[k] !== "*") k++;
|
|
727
|
-
return out.slice(0, Math.max(k, 1)).join("/") + "/**";
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// ../../web/system-map-names.js
|
|
731
|
-
var SKIP = /* @__PURE__ */ new Set(["vocabulary", "evidence", "schema", "marks", "program", "class"]);
|
|
732
|
-
var skipped = (key, v) => SKIP.has(key) || key === "from" && !Array.isArray(v);
|
|
733
|
-
var STRUCTURAL = /* @__PURE__ */ new Set([
|
|
734
|
-
"kind",
|
|
735
|
-
"op",
|
|
736
|
-
"how",
|
|
737
|
-
"transport",
|
|
738
|
-
"authority",
|
|
739
|
-
"where",
|
|
740
|
-
"slotRole",
|
|
741
|
-
"role",
|
|
742
|
-
"passedKind",
|
|
743
|
-
"anchor",
|
|
744
|
-
"node",
|
|
745
|
-
"keyKinds",
|
|
746
|
-
"reachKinds",
|
|
747
|
-
"resources",
|
|
748
|
-
"replies",
|
|
749
|
-
"also",
|
|
750
|
-
"kinds",
|
|
751
|
-
"within",
|
|
752
|
-
"vars",
|
|
753
|
-
"id"
|
|
754
|
-
]);
|
|
755
|
-
var WHAT = {
|
|
756
|
-
path: "an action the app answers",
|
|
757
|
-
routes: "an action the app answers",
|
|
758
|
-
table: "a kind of record the app keeps",
|
|
759
|
-
tables: "a kind of record the app keeps",
|
|
760
|
-
roles: "a role a person can hold",
|
|
761
|
-
policy: "a setting an action passes to shared code",
|
|
762
|
-
tokens: "a setting an action passes to shared code",
|
|
763
|
-
fields: "an operation a pluggable part offers"
|
|
764
|
-
};
|
|
765
|
-
var PACKAGE = "a section of the program";
|
|
766
|
-
var DEFINITION = "a piece of the program";
|
|
767
|
-
var VALUE = "a kind of value";
|
|
768
|
-
var TYPES = /* @__PURE__ */ new Set(["type", "types", "signature"]);
|
|
769
|
-
var WORD = /[A-Za-z_][\w/]*::\w+|[A-Za-z_]\w*/g;
|
|
770
|
-
var leaves = (record, visit) => {
|
|
771
|
-
const walk3 = (v, key) => {
|
|
772
|
-
if (typeof v === "string") visit(v, key);
|
|
773
|
-
else if (Array.isArray(v)) v.forEach((x) => walk3(x, key));
|
|
774
|
-
else if (v && typeof v === "object") {
|
|
775
|
-
for (const [k, x] of Object.entries(v)) if (!skipped(k, x)) walk3(x, k);
|
|
776
|
-
}
|
|
777
|
-
};
|
|
778
|
-
walk3(record, null);
|
|
779
|
-
};
|
|
780
|
-
function nameableParts(map) {
|
|
781
|
-
const found = /* @__PURE__ */ new Map();
|
|
782
|
-
const add = (code, what) => {
|
|
783
|
-
if (code && !found.has(code)) found.set(code, what);
|
|
784
|
-
};
|
|
785
|
-
const idParts = (id) => {
|
|
786
|
-
const i = id.lastIndexOf("::");
|
|
787
|
-
add(id.slice(0, i), PACKAGE);
|
|
788
|
-
add(id.slice(i + 2), DEFINITION);
|
|
789
|
-
};
|
|
790
|
-
leaves(map, (s, key) => {
|
|
791
|
-
if (s.startsWith("route:")) return add(s.slice(6), WHAT.path);
|
|
792
|
-
if (TYPES.has(key)) {
|
|
793
|
-
for (const w of s.match(WORD) ?? []) if (w.includes("::")) idParts(w);
|
|
794
|
-
else if (w.length > 1) add(w, VALUE);
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
if (s.includes("::")) {
|
|
798
|
-
for (const w of s.match(WORD)) if (w.includes("::")) idParts(w);
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
if (STRUCTURAL.has(key)) return;
|
|
802
|
-
if (key === "name" || key === "package" || key === "pkg" || key === "instantiatingPackages" || key === "cycles" || key === "to") return add(s, s.includes("/") ? PACKAGE : DEFINITION);
|
|
803
|
-
add(s, WHAT[key] ?? DEFINITION);
|
|
804
|
-
});
|
|
805
|
-
for (const b of map.bundles ?? []) if (b.count > 1) found.delete(b.label);
|
|
806
|
-
return [...found].map(([code, what]) => ({ code, what })).sort((a, b) => a.code.localeCompare(b.code));
|
|
807
|
-
}
|
|
808
|
-
function plainNames(names) {
|
|
809
|
-
const out = /* @__PURE__ */ new Map(), taken = /* @__PURE__ */ new Set();
|
|
810
|
-
for (const [code, name] of Object.entries(names).sort(([a], [b]) => a.localeCompare(b))) {
|
|
811
|
-
const base = name.replace(/::|\//g, " ").replace(/\s+/g, " ").trim();
|
|
812
|
-
if (!base) continue;
|
|
813
|
-
let plain = base;
|
|
814
|
-
for (let n = 2; taken.has(plain); n++) plain = `${base} (${n})`;
|
|
815
|
-
taken.add(plain);
|
|
816
|
-
out.set(code, plain);
|
|
817
|
-
}
|
|
818
|
-
return out;
|
|
819
|
-
}
|
|
820
|
-
function relabelSystemMap(map, names) {
|
|
821
|
-
const plain = plainNames(names);
|
|
822
|
-
const word = (w) => plain.get(w) ?? w;
|
|
823
|
-
const id = (w) => {
|
|
824
|
-
const i = w.lastIndexOf("::");
|
|
825
|
-
return `${word(w.slice(0, i))}::${word(w.slice(i + 2))}`;
|
|
826
|
-
};
|
|
827
|
-
const text3 = (s, key) => {
|
|
828
|
-
if (s.startsWith("route:")) return `route:${word(s.slice(6))}`;
|
|
829
|
-
if (plain.has(s) && !STRUCTURAL.has(key)) return plain.get(s);
|
|
830
|
-
if (s.startsWith("/")) return s;
|
|
831
|
-
return s.replace(WORD, (w) => w.includes("::") ? id(w) : STRUCTURAL.has(key) ? w : word(w));
|
|
832
|
-
};
|
|
833
|
-
const walk3 = (v, key) => {
|
|
834
|
-
if (typeof v === "string") return text3(v, key);
|
|
835
|
-
if (Array.isArray(v)) return v.map((x) => walk3(x, key));
|
|
836
|
-
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, skipped(k, x) ? x : walk3(x, k)]));
|
|
837
|
-
return v;
|
|
838
|
-
};
|
|
839
|
-
const out = walk3(map, null);
|
|
840
|
-
const pathOf = new Map(map.entries.map((e) => [e.key, e.path]));
|
|
841
|
-
out.bundles = map.bundles && map.bundles.map((b, i) => {
|
|
842
|
-
const paths = b.entries.map((k) => pathOf.get(k));
|
|
843
|
-
if (!paths.some((p) => plain.has(p))) return out.bundles[i];
|
|
844
|
-
return { ...out.bundles[i], label: b.count === 1 ? word(paths[0]) : `${word(paths[0])} and ${b.count - 1} more` };
|
|
845
|
-
});
|
|
846
|
-
return out;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// ../../web/what-happens-record.js
|
|
850
|
-
var SCHEMA = "what-happens-1";
|
|
851
|
-
var VOCABULARY2 = {
|
|
852
|
-
version: MARKS_VERSION,
|
|
853
|
-
added: ["outcome"],
|
|
854
|
-
marks: {
|
|
855
|
-
entryHead: "arrowhead where a request comes in; the rack above the tree has one per described entry, then one dashed line counting the entries no scenario describes, which opens to draw each of them struck <- entries[], ghosts[]",
|
|
856
|
-
guard: "red diamond: the declared sign-in or role check, the first branch of every entry that has one; its failure stem ends in a refusal <- tree node with from.kind=guard",
|
|
857
|
-
fork: "three prongs: a branch the code takes on the request, the caller, a value read from a table, or a call result; the arm each line takes is written where it leaves <- tree nodes",
|
|
858
|
-
station: "ring on a line: an effect on that path, its verb and table beside it; filled = it writes <- arms[].effects, entry.effects",
|
|
859
|
-
modelRing: "violet ring: the path reaches the model <- effects[resource=model]",
|
|
860
|
-
netRing: "orange ring: the path reaches the network <- effects[resource=net]",
|
|
861
|
-
outcome: "the end of a path: a cap with what the request gets back (a constructor, or a refusal with its status and code); red when a claim fails there or a check reports a witness that ends there, green when every obligation that failed there at the base is checked here and holds, hollow and dashed (unchecked) when a claim there is inconclusive or an obligation that failed at the base is no longer checked, grey otherwise <- leaves",
|
|
862
|
-
unchecked: "a hollow dashed cap and a ? tick: the engine cannot say whether the claim holds on this path (a step it could not open, a table it could not name, an answer it could not decide), or what failed here at the base is not checked here; never counted as holding <- claims[verdict=inconclusive], leaves[].gaps, diff.unresolved",
|
|
863
|
-
struck: 'a struck ghost: something checked and found absent (an entry nobody described, a write a claim says a path must not make, "no base compared" when no earlier version was compared) <- ghosts[], claims holding with not',
|
|
864
|
-
bundle: "a cable with a count: a subtree too large for this view; opening it draws that part with a breadcrumb <- derived from the tree",
|
|
865
|
-
compared: 'a line under the rack, always there: "compared with <base> \xB7 N fixed \xB7 M still failing", or "no base compared", struck <- diff',
|
|
866
|
-
hull: `a shaded box on the line: a step that is not the handler's own work, folded (a wrapper's guard or short-circuit, the platform's declared check or the transaction it runs the plan in, work beside the wrapper, a wrapper's work after the handler); it keeps what the step answers instead of running the handler, one row per arm that skips the handler, read as a side stub (the arm, its struck writes, what the request gets, ticks), then "continue" (a transaction: "runs"), and its effects; opening it draws the step's own tree; drawn inline, the same hull is a light band behind the step and its exits, with its role and where it comes from above its test, so either way it reads as not the handler's own work <- tree nodes[concern.role], entries[].steps`
|
|
867
|
-
},
|
|
868
|
-
rule: "every entry, branch, arm, effect and outcome the engine computes is drawn, a folded step's inside its box and an undescribed entry behind the rack's one line; the author's words appear only on ids the engine computed, in italic, with the code name beside them.",
|
|
869
|
-
concerns: "where each branch and effect comes from in value flow (concern.layer, effects[].layer): core = the handler's own work, a function it hands its wrapper or its body when it has none; cross = the wrapper its route shape names, shared with other entries; beside = the handler's code outside the wrapper call; platform = the route's declared authority, or the transaction the platform runs a plan in (its arms that give up or cannot confirm skip the handler's work). Roles come from which arms reach the handler, never from names."
|
|
870
|
-
};
|
|
871
|
-
var bare2 = (id) => String(id).split("::").pop();
|
|
872
|
-
var claimKey = (entry, c) => `${entry}\0${c.at}\0${c.parsed}`;
|
|
873
|
-
function walk(tree, f, path10 = []) {
|
|
874
|
-
if (!tree) return;
|
|
875
|
-
f(tree, path10);
|
|
876
|
-
if (tree.kind === "node") for (const arm of tree.arms) {
|
|
877
|
-
f(arm, path10);
|
|
878
|
-
walk(arm.next, f, [...path10, arm]);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
var leavesOf = (tree) => {
|
|
882
|
-
const out = [];
|
|
883
|
-
walk(tree, (t) => {
|
|
884
|
-
if (t.kind === "leaf") out.push(t);
|
|
885
|
-
});
|
|
886
|
-
return out;
|
|
887
|
-
};
|
|
888
|
-
function pathEffects(entry) {
|
|
889
|
-
const out = /* @__PURE__ */ new Map();
|
|
890
|
-
const go = (t, acc) => {
|
|
891
|
-
if (!t) return;
|
|
892
|
-
if (t.kind === "leaf") {
|
|
893
|
-
out.set(t.id, acc);
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
for (const a of t.arms) go(a.next, [...acc, ...a.effects]);
|
|
897
|
-
};
|
|
898
|
-
go(entry.tree, entry.effects || []);
|
|
899
|
-
return out;
|
|
900
|
-
}
|
|
901
|
-
function heldOn(tree, guards) {
|
|
902
|
-
const out = /* @__PURE__ */ new Set();
|
|
903
|
-
const go = (t, held) => {
|
|
904
|
-
if (!t) return;
|
|
905
|
-
if (t.kind === "leaf") {
|
|
906
|
-
if (held) out.add(t.id);
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
const isGuard = t.from?.kind === "call" && guards.includes(bare2(t.from.callee));
|
|
910
|
-
for (const a of t.arms) go(a.next, held || isGuard && a.arm === "True");
|
|
911
|
-
};
|
|
912
|
-
go(tree, false);
|
|
913
|
-
return out;
|
|
914
|
-
}
|
|
915
|
-
function witnessesOf(access, entries) {
|
|
916
|
-
if (!access) return [];
|
|
917
|
-
const findings = (access.report?.findings || []).filter((f) => f.status === "violated");
|
|
918
|
-
return findings.map((f, i) => {
|
|
919
|
-
const site = `${f.package}::${f.binding}`;
|
|
920
|
-
const guards = f.pathCondition?.unlessHeld || [];
|
|
921
|
-
const leaves2 = entries.flatMap((e) => {
|
|
922
|
-
const held = heldOn(e.tree, guards);
|
|
923
|
-
return leavesOf(e.tree).filter((l) => !held.has(l.id) && l.effects.some((x) => x.site === site && bare2(x.primitive) === f.operation && x.tables.includes(f.resource)));
|
|
924
|
-
}).map((l) => l.id);
|
|
925
|
-
return {
|
|
926
|
-
id: `witness:${i + 1}`,
|
|
927
|
-
check: "access",
|
|
928
|
-
resource: f.resource,
|
|
929
|
-
site,
|
|
930
|
-
operation: f.operation,
|
|
931
|
-
kind: f.kind,
|
|
932
|
-
detail: f.detail,
|
|
933
|
-
leaves: leaves2,
|
|
934
|
-
...guards.length ? { unlessHeld: guards } : {},
|
|
935
|
-
from: guards.length ? "api_access_check report.findings[status=violated], on paths where pathCondition.unlessHeld did not hold" : "api_access_check report.findings[status=violated]"
|
|
936
|
-
};
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } = {}) {
|
|
940
|
-
for (const [k, v] of Object.entries(engine)) if (v && !v.ok) throw new Error(`${k}: ${v.error}`);
|
|
941
|
-
const st = engine.structure;
|
|
942
|
-
const claims = st.claims || { scenarios: [], undescribed: (st.scenarios || []).map((s) => s.entry) };
|
|
943
|
-
const byEntry = new Map(claims.scenarios.filter((s) => s.status === "matched").map((s) => [s.entry, s]));
|
|
944
|
-
const routes = new Map((st.entries || []).map((e) => [e.entry, e]));
|
|
945
|
-
const entries = st.scenarios.map((s) => {
|
|
946
|
-
const meta = routes.get(s.entry) || {};
|
|
947
|
-
const sc = byEntry.get(s.entry);
|
|
948
|
-
const names = new Map((sc?.names || []).filter((n) => n.status === "named").map((n) => [n.id, n.words]));
|
|
949
|
-
const copy = (t) => {
|
|
950
|
-
if (!t) return null;
|
|
951
|
-
if (t.kind === "leaf") return { ...t, words: names.get(t.id) || null, claims: [], witnesses: [], status: "grey" };
|
|
952
|
-
return { ...t, words: names.get(t.id) || null, arms: t.arms.map((a) => ({ ...a, words: names.get(a.id) || null, next: copy(a.next) })) };
|
|
953
|
-
};
|
|
954
|
-
return {
|
|
955
|
-
entry: s.entry,
|
|
956
|
-
handler: s.handler,
|
|
957
|
-
label: s.entry.replace(/^route:/, ""),
|
|
958
|
-
authority: meta.authority || null,
|
|
959
|
-
transport: meta.transport || null,
|
|
960
|
-
title: sc?.title || null,
|
|
961
|
-
described: !!sc,
|
|
962
|
-
effects: s.effects,
|
|
963
|
-
tree: copy(s.tree),
|
|
964
|
-
leafCount: Number(s.leafCount),
|
|
965
|
-
truncated: s.truncated,
|
|
966
|
-
unsupported: s.unsupported || null,
|
|
967
|
-
wrapper: s.wrapper || null,
|
|
968
|
-
steps: s.steps || [],
|
|
969
|
-
from: "structure.scenarios[]"
|
|
970
|
-
};
|
|
971
|
-
});
|
|
972
|
-
const entryOf = new Map(entries.map((e) => [e.entry, e]));
|
|
973
|
-
const leafById = new Map(entries.flatMap((e) => leavesOf(e.tree).map((l) => [l.id, l])));
|
|
974
|
-
const checked = [], notes = [], refused = [], mismatches = [];
|
|
975
|
-
for (const sc of claims.scenarios) {
|
|
976
|
-
if (sc.status !== "matched") {
|
|
977
|
-
mismatches.push({ entry: sc.entry, title: sc.title || null, reason: sc.reason, from: "structure.claims.scenarios[status=mismatch]" });
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
for (const n of sc.names) if (n.status !== "named") refused.push({ entry: sc.entry, id: n.id, words: n.words, reason: n.reason, from: "structure.claims.scenarios[].names[status=refused]" });
|
|
981
|
-
for (const x of sc.expect) {
|
|
982
|
-
if (x.verdict === "not-a-claim") {
|
|
983
|
-
notes.push({ entry: sc.entry, at: x.at, text: x.claim, from: "structure.claims.scenarios[].expect[verdict=not-a-claim]" });
|
|
984
|
-
continue;
|
|
985
|
-
}
|
|
986
|
-
if (x.verdict === "refused") {
|
|
987
|
-
refused.push({ entry: sc.entry, id: x.at, words: x.claim, reason: x.reason, from: "structure.claims.scenarios[].expect[verdict=refused]" });
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
const open = new Map((x.inconclusive || []).map((o) => [o.leaf, o.reasons]));
|
|
991
|
-
const c = {
|
|
992
|
-
id: `claim:${checked.length + 1}`,
|
|
993
|
-
entry: sc.entry,
|
|
994
|
-
at: x.at,
|
|
995
|
-
claim: x.claim,
|
|
996
|
-
parsed: x.parsed,
|
|
997
|
-
property: x.property,
|
|
998
|
-
verdict: x.verdict,
|
|
999
|
-
leaves: x.leaves,
|
|
1000
|
-
failing: x.failing,
|
|
1001
|
-
inconclusive: x.inconclusive || [],
|
|
1002
|
-
from: "structure.claims.scenarios[].expect[]"
|
|
1003
|
-
};
|
|
1004
|
-
checked.push(c);
|
|
1005
|
-
for (const id of x.leaves) {
|
|
1006
|
-
const l = leafById.get(id);
|
|
1007
|
-
if (!l) continue;
|
|
1008
|
-
const verdict = x.failing.includes(id) ? "fails" : open.has(id) ? "inconclusive" : "holds";
|
|
1009
|
-
l.claims.push({ id: c.id, at: x.at, parsed: x.parsed, property: x.property, verdict, ...verdict === "inconclusive" ? { reasons: open.get(id) } : {} });
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
const witnesses = witnessesOf(engine.access, entries);
|
|
1014
|
-
const basePaths = base ? new Map(base.entries.flatMap((e) => [...pathEffects(e)])) : null;
|
|
1015
|
-
for (const w of witnesses) for (const id of w.leaves) leafById.get(id)?.witnesses.push(w.id);
|
|
1016
|
-
for (const l of leafById.values()) {
|
|
1017
|
-
if (l.claims.some((c) => c.verdict === "fails") || l.witnesses.length) l.status = "red";
|
|
1018
|
-
else if (l.claims.some((c) => c.verdict === "inconclusive")) l.status = "unchecked";
|
|
1019
|
-
}
|
|
1020
|
-
let diff = null;
|
|
1021
|
-
if (base) {
|
|
1022
|
-
const baseRed = new Map(base.entries.flatMap((e) => leavesOf(e.tree).filter((l) => l.status === "red").map((l) => [l.id, { entry: e.entry, leaf: l }])));
|
|
1023
|
-
const baseWitness = new Map((base.witnesses || []).map((w) => [w.id, w]));
|
|
1024
|
-
const fixed = [], still = [], gone = [], unresolved = [];
|
|
1025
|
-
for (const [id, { entry, leaf: bl }] of baseRed) {
|
|
1026
|
-
const here = leafById.get(id);
|
|
1027
|
-
if (!here) {
|
|
1028
|
-
gone.push(id);
|
|
1029
|
-
continue;
|
|
1030
|
-
}
|
|
1031
|
-
if (here.status === "red") {
|
|
1032
|
-
still.push(id);
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
1035
|
-
const reasons = [
|
|
1036
|
-
...bl.claims.filter((c) => c.verdict === "fails").flatMap((c) => {
|
|
1037
|
-
const now = here.claims.find((h) => claimKey(entry, h) === claimKey(entry, c));
|
|
1038
|
-
if (!now) return [`the claim ${c.parsed} at ${c.at} failed here at the base and is not checked here`];
|
|
1039
|
-
return now.verdict === "holds" ? [] : [`the claim ${c.parsed} at ${c.at} failed here at the base and is inconclusive here`];
|
|
1040
|
-
}),
|
|
1041
|
-
...bl.witnesses.length && !engine.access ? [`the access check reported a witness here at the base and did not run here`] : []
|
|
1042
|
-
];
|
|
1043
|
-
const baseClaims = bl.claims, baseWitnesses = bl.witnesses.map((w) => baseWitness.get(w)).filter(Boolean);
|
|
1044
|
-
here.base = { outcome: bl.outcome, effects: bl.effects || basePaths.get(id) || [], claims: baseClaims, witnesses: baseWitnesses };
|
|
1045
|
-
if (reasons.length) {
|
|
1046
|
-
here.status = "unchecked";
|
|
1047
|
-
unresolved.push({ id, reasons });
|
|
1048
|
-
continue;
|
|
1049
|
-
}
|
|
1050
|
-
here.status = "green";
|
|
1051
|
-
fixed.push(id);
|
|
1052
|
-
}
|
|
1053
|
-
diff = {
|
|
1054
|
-
base: baseLabel,
|
|
1055
|
-
fixed,
|
|
1056
|
-
stillRed: still,
|
|
1057
|
-
goneAtHead: gone,
|
|
1058
|
-
unresolved,
|
|
1059
|
-
from: "derived: base leaves[status=red] matched by id; each failed claim matched by entry, at and parsed claim; a witness is gone when the access check ran here and reports none on the leaf"
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
const ghosts = entries.filter((e) => !e.described).map((e) => ({ entry: e.entry, from: "structure.claims.undescribed[]" }));
|
|
1063
|
-
for (const l of leafById.values()) delete l.effects;
|
|
1064
|
-
return {
|
|
1065
|
-
schema: SCHEMA,
|
|
1066
|
-
marks: MARKS_VERSION,
|
|
1067
|
-
vocabulary: VOCABULARY2,
|
|
1068
|
-
program,
|
|
1069
|
-
entries,
|
|
1070
|
-
ghosts,
|
|
1071
|
-
claims: checked,
|
|
1072
|
-
witnesses,
|
|
1073
|
-
notes,
|
|
1074
|
-
refused,
|
|
1075
|
-
mismatches,
|
|
1076
|
-
diff,
|
|
1077
|
-
counts: {
|
|
1078
|
-
entries: entries.length,
|
|
1079
|
-
described: entries.length - ghosts.length,
|
|
1080
|
-
leaves: leafById.size,
|
|
1081
|
-
steps: entries.reduce((n, e) => n + e.steps.length, 0),
|
|
1082
|
-
wrapped: entries.filter((e) => e.wrapper && e.wrapper.wraps).length,
|
|
1083
|
-
red: [...leafById.values()].filter((l) => l.status === "red").length,
|
|
1084
|
-
green: [...leafById.values()].filter((l) => l.status === "green").length,
|
|
1085
|
-
unchecked: [...leafById.values()].filter((l) => l.status === "unchecked").length
|
|
1086
|
-
},
|
|
1087
|
-
evidence: { tools: ["api_organize:structure", ...engine.access ? ["api_access_check"] : []], derived: ["diff: base red leaves matched by id, their failed obligations by identity"] }
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
function stationLabel(e) {
|
|
1091
|
-
const verb = e.op === "mint id" ? "mint" : e.op;
|
|
1092
|
-
if (e.tables.length) return `${verb} \xB7 ${e.tables.join(", ")}`;
|
|
1093
|
-
if (e.resource === "model" || e.resource === "net") return verb;
|
|
1094
|
-
return `${verb} \xB7 ${bare2(e.primitive)}`;
|
|
1095
|
-
}
|
|
1096
|
-
function outcomeLabel(o) {
|
|
1097
|
-
if (o.status && !o.constructors.length) return o.shown;
|
|
1098
|
-
const head = o.constructors.length ? o.constructors.join(" | ") : o.value ? `the ${o.value}` : o.shown;
|
|
1099
|
-
return head;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// src/studio/project.ts
|
|
1103
|
-
import { createHash } from "crypto";
|
|
1104
|
-
import fs from "fs";
|
|
1105
|
-
import path from "path";
|
|
1106
|
-
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "out"]);
|
|
1107
|
-
function projectSources(root) {
|
|
1108
|
-
const files = [];
|
|
1109
|
-
const walk3 = (dir) => {
|
|
1110
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1111
|
-
if (entry.name.startsWith(".") || SKIPPED.has(entry.name)) continue;
|
|
1112
|
-
const full = path.join(dir, entry.name);
|
|
1113
|
-
if (entry.isDirectory()) walk3(full);
|
|
1114
|
-
else if (entry.isFile() && entry.name.endsWith(".bosatsu")) {
|
|
1115
|
-
files.push({ fileName: path.relative(root, full).split(path.sep).join("/"), source: fs.readFileSync(full, "utf8") });
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
};
|
|
1119
|
-
walk3(root);
|
|
1120
|
-
return files;
|
|
1121
|
-
}
|
|
1122
|
-
function sourcesHash(files) {
|
|
1123
|
-
const hash = createHash("sha256");
|
|
1124
|
-
for (const file of files) hash.update(`${file.fileName}\0${file.source}\0`);
|
|
1125
|
-
return hash.digest("hex");
|
|
1126
|
-
}
|
|
1127
|
-
function caseSources(root) {
|
|
1128
|
-
const dir = path.join(root, ".yichus", "cases");
|
|
1129
|
-
if (!fs.existsSync(dir)) return [];
|
|
1130
|
-
return fs.readdirSync(dir).filter((name) => name.endsWith(".bosatsu")).sort().map((name) => ({ fileName: `cases/${name}`, source: fs.readFileSync(path.join(dir, name), "utf8") }));
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
// src/studio/questions.ts
|
|
1134
|
-
import { z as z3 } from "zod";
|
|
1135
|
-
var Step = z3.looseObject({ op: z3.string(), tables: z3.array(z3.string()).default([]), resource: z3.string() });
|
|
1136
|
-
var Route = z3.looseObject({ path: z3.string(), handler: z3.string(), inferredSteps: z3.array(Step).default([]) });
|
|
1137
|
-
var Table = z3.looseObject({
|
|
1138
|
-
name: z3.string(),
|
|
1139
|
-
policy: z3.string().nullable().optional(),
|
|
1140
|
-
readers: z3.array(z3.string()).default([]),
|
|
1141
|
-
writers: z3.array(z3.string()).default([])
|
|
1142
|
-
});
|
|
1143
|
-
var Report = z3.looseObject({
|
|
1144
|
-
surface: z3.looseObject({ routes: z3.array(Route).default([]) }).default({ routes: [] }),
|
|
1145
|
-
dataModel: z3.looseObject({ tables: z3.array(Table).default([]) }).default({ tables: [] })
|
|
1146
|
-
});
|
|
1147
|
-
var Entry = z3.looseObject({ entry: z3.string(), resources: z3.array(z3.string()).default([]) });
|
|
1148
|
-
var Structure = z3.looseObject({ entries: z3.array(Entry).default([]) });
|
|
1149
|
-
var ACCESS_POLICY = {
|
|
1150
|
-
owner: "OwnerScoped",
|
|
1151
|
-
shared: "Guarded",
|
|
1152
|
-
"everyone-reads": "PublicReadRoleWrite",
|
|
1153
|
-
public: "Public"
|
|
1154
|
-
};
|
|
1155
|
-
var platform = {
|
|
1156
|
-
id: "platform",
|
|
1157
|
-
kind: "platform",
|
|
1158
|
-
subject: null,
|
|
1159
|
-
ask: "Where do you want your app to run once it is ready?",
|
|
1160
|
-
why: "Each place runs apps differently: some keep a server running, some only run the page people open. Knowing it now means your agent builds the pieces that place can run, instead of rebuilding them later.",
|
|
1161
|
-
choices: [
|
|
1162
|
-
{ id: "render", label: "Render", means: "The server, its database, the page and background jobs all run on Render." },
|
|
1163
|
-
{ id: "vercel", label: "Vercel", means: "The page people open runs on Vercel; the server and its database run on a second service, because Vercel does not keep a server running." },
|
|
1164
|
-
{ id: "netlify", label: "Netlify", means: "The page people open runs on Netlify; the server and its database run on a second service, because Netlify does not keep a server running." },
|
|
1165
|
-
{ id: "aws", label: "Amazon Web Services", means: "The server runs on App Runner or ECS with an RDS database." },
|
|
1166
|
-
{ id: "gcp", label: "Google Cloud", means: "The server runs on Cloud Run with a Cloud SQL database." },
|
|
1167
|
-
{ id: "azure", label: "Microsoft Azure", means: "The server runs on Container Apps or App Service with an Azure PostgreSQL database." },
|
|
1168
|
-
{ id: "undecided", label: "Not sure yet", means: "Your agent builds for Render, which holds everything in one place; moving later changes configuration, not the app." }
|
|
1169
|
-
],
|
|
1170
|
-
fact: "Every app runs somewhere."
|
|
1171
|
-
};
|
|
1172
|
-
var PLATFORM_SECTION = {
|
|
1173
|
-
render: "Render",
|
|
1174
|
-
vercel: "Vercel",
|
|
1175
|
-
netlify: "Netlify",
|
|
1176
|
-
aws: "AWS",
|
|
1177
|
-
gcp: "Google Cloud",
|
|
1178
|
-
azure: "Microsoft Azure",
|
|
1179
|
-
undecided: "Not decided yet"
|
|
1180
|
-
};
|
|
1181
|
-
var topology = {
|
|
1182
|
-
id: "topology",
|
|
1183
|
-
kind: "topology",
|
|
1184
|
-
subject: null,
|
|
1185
|
-
ask: "When many people use your app at the same time, should it run as one server, or as several copies of the server that share one database?",
|
|
1186
|
-
why: "Several copies keep the app up when one fails and when traffic grows, but then two requests really can run at the same moment, so every check below assumes that unless you choose one server.",
|
|
1187
|
-
choices: [
|
|
1188
|
-
{ id: "many", label: "Several copies", means: "Every check assumes requests can run at the same moment on different copies." },
|
|
1189
|
-
{ id: "one", label: "One server", means: "Checks may assume one request at a time. This holds only while exactly one copy runs; the page shows it as an assumption." }
|
|
1190
|
-
],
|
|
1191
|
-
fact: "The program serves routes."
|
|
1192
|
-
};
|
|
1193
|
-
var shown = (names, subject) => names[subject] ?? subject;
|
|
1194
|
-
function accessQuestion(table, names) {
|
|
1195
|
-
return {
|
|
1196
|
-
id: `access:${table.name}`,
|
|
1197
|
-
kind: "access",
|
|
1198
|
-
subject: table.name,
|
|
1199
|
-
ask: `Your app saves "${shown(names, table.name)}"${table.name in names ? "" : " records"}. Who should be able to see one, and who should be able to change it?`,
|
|
1200
|
-
why: "Without a rule the app has no way to know a record is private. Yichus checks every place the app reads or changes these records against the rule you choose.",
|
|
1201
|
-
choices: [
|
|
1202
|
-
{ id: "owner", label: "Only the person who created it", means: "Every read and write is tied to the signed-in person, and the check proves no one reaches another person's records." },
|
|
1203
|
-
{ id: "shared", label: "The creator and people they share it with", means: "Every read goes through a sharing rule, and the check proves nothing skips it." },
|
|
1204
|
-
{ id: "everyone-reads", label: "Everyone can see it; only certain roles can change it", means: "Anyone can read; changes need a role the app gives out." },
|
|
1205
|
-
{ id: "public", label: "Anyone, even without signing in", means: "Nothing in these records is private." }
|
|
1206
|
-
],
|
|
1207
|
-
fact: `The program stores rows in "${table.name}" (policy declared: ${table.policy ?? "none"}).`
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
function togetherQuestion(table, handlers, names) {
|
|
1211
|
-
return {
|
|
1212
|
-
id: `together:${table}`,
|
|
1213
|
-
kind: "together",
|
|
1214
|
-
subject: table,
|
|
1215
|
-
ask: `Two requests change the same record in "${shown(names, table)}" at the same moment (two people, or one person on two devices). What should happen?`,
|
|
1216
|
-
why: "When a request reads a record and then writes it back, a second request in between can be silently lost. People rarely notice until data goes missing.",
|
|
1217
|
-
choices: [
|
|
1218
|
-
{ id: "keep-both", label: "Both changes are kept", means: "Each change happens as one indivisible step, so neither can overwrite the other unseen; the check proves no change is lost." },
|
|
1219
|
-
{ id: "refuse-second", label: "The second is refused and they try again", means: "A change made to an out-of-date copy is turned away; the check proves no change is lost." }
|
|
1220
|
-
],
|
|
1221
|
-
fact: `${handlers.join(", ")} ${handlers.length === 1 ? "reads" : "read"} "${table}" and then ${handlers.length === 1 ? "writes" : "write"} it.`
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
function partialQuestion(route, tables, names) {
|
|
1225
|
-
return {
|
|
1226
|
-
id: `partial:${route.path}`,
|
|
1227
|
-
kind: "partial",
|
|
1228
|
-
subject: route.path,
|
|
1229
|
-
ask: `If the server stops halfway through "${shown(names, route.path)}" (a crash, a database error, a timeout), may part of its changes stay saved?`,
|
|
1230
|
-
why: `This action changes ${tables.map((t) => `"${shown(names, t)}"`).join(" and ")} in more than one step. A stop between steps can leave them disagreeing, for example money taken but no receipt.`,
|
|
1231
|
-
choices: [
|
|
1232
|
-
{ id: "all-or-nothing", label: "All or nothing", means: "Every change this action makes is saved together or not at all; Yichus stops it at each step and checks nothing half-done stays saved." },
|
|
1233
|
-
{ id: "partial-ok", label: "Partial is fine", means: "No check is added for this action." }
|
|
1234
|
-
],
|
|
1235
|
-
fact: `${route.handler} writes ${tables.map((t) => `"${t}"`).join(", ")} in more than one step.`
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
function outsideQuestion(route, kinds, names) {
|
|
1239
|
-
const service = kinds.includes("model") ? "an AI model" : "another service on the internet";
|
|
1240
|
-
return {
|
|
1241
|
-
id: `outside:${route.path}`,
|
|
1242
|
-
kind: "outside",
|
|
1243
|
-
subject: route.path,
|
|
1244
|
-
ask: `"${shown(names, route.path)}" calls ${service}. If that service is down or slow, what should the person using your app see?`,
|
|
1245
|
-
why: "Outside services fail often. Without a plan, a failure can leave a half-finished record or charge for work that never happened.",
|
|
1246
|
-
choices: [
|
|
1247
|
-
{ id: "fail-clean", label: "An error, with nothing saved", means: "Yichus makes the service fail and checks that nothing from this action stays saved." },
|
|
1248
|
-
{ id: "finish-later", label: "Save the request and finish it later", means: "Yichus makes the service fail and checks the saved request is tried again and finished exactly once." },
|
|
1249
|
-
{ id: "no-plan", label: "No preference", means: "No check is added for this action." }
|
|
1250
|
-
],
|
|
1251
|
-
fact: `${route.handler} reaches ${kinds.join(" and ")}.`
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
function questionsFor(report, structure, names = {}) {
|
|
1255
|
-
const questions = [];
|
|
1256
|
-
if (report.surface.routes.length) questions.push(topology);
|
|
1257
|
-
for (const table of report.dataModel.tables) questions.push(accessQuestion(table, names));
|
|
1258
|
-
for (const table of report.dataModel.tables) {
|
|
1259
|
-
const both = table.readers.filter((h) => table.writers.includes(h));
|
|
1260
|
-
if (both.length) questions.push(togetherQuestion(table.name, both, names));
|
|
1261
|
-
}
|
|
1262
|
-
for (const route of report.surface.routes) {
|
|
1263
|
-
const writes = route.inferredSteps.filter((s) => s.resource === "db" && s.op !== "read" && s.op !== "query");
|
|
1264
|
-
const tables = [...new Set(writes.flatMap((s) => s.tables))];
|
|
1265
|
-
if (writes.length >= 2) questions.push(partialQuestion(route, tables, names));
|
|
1266
|
-
}
|
|
1267
|
-
for (const route of report.surface.routes) {
|
|
1268
|
-
const entry = structure.entries.find((e) => e.entry === `route:${route.path}`);
|
|
1269
|
-
const kinds = (entry?.resources ?? []).filter((r) => r === "model" || r === "net");
|
|
1270
|
-
if (kinds.length) questions.push(outsideQuestion(route, kinds, names));
|
|
1271
|
-
}
|
|
1272
|
-
return questions;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// src/studio/requirements.ts
|
|
1276
|
-
import fs2 from "fs";
|
|
1277
|
-
import path2 from "path";
|
|
1278
|
-
import { z as z4 } from "zod";
|
|
1279
|
-
var Answer = z4.object({
|
|
1280
|
-
question: z4.string().min(1),
|
|
1281
|
-
choice: z4.string().min(1),
|
|
1282
|
-
words: z4.string(),
|
|
1283
|
-
answeredAt: z4.string()
|
|
1284
|
-
});
|
|
1285
|
-
var Scenario = z4.object({
|
|
1286
|
-
entry: z4.string().min(1),
|
|
1287
|
-
title: z4.string().optional(),
|
|
1288
|
-
names: z4.record(z4.string(), z4.string()).optional(),
|
|
1289
|
-
expect: z4.array(z4.object({ at: z4.string(), claim: z4.string() }).strict()).optional()
|
|
1290
|
-
}).strict();
|
|
1291
|
-
var Requirements = z4.object({
|
|
1292
|
-
schemaVersion: z4.literal("yichus-requirements-1"),
|
|
1293
|
-
description: z4.string().nullable(),
|
|
1294
|
-
answers: z4.array(Answer),
|
|
1295
|
-
/** Plain names for the program's records and actions, keyed by code name. */
|
|
1296
|
-
names: z4.record(z4.string(), z4.string()).optional(),
|
|
1297
|
-
scenarios: z4.array(Scenario).optional()
|
|
1298
|
-
});
|
|
1299
|
-
var studioDir = (root) => path2.join(root, ".yichus");
|
|
1300
|
-
var requirementsPath = (root) => path2.join(studioDir(root), "requirements.json");
|
|
1301
|
-
function loadRequirements(root) {
|
|
1302
|
-
const file = requirementsPath(root);
|
|
1303
|
-
if (!fs2.existsSync(file)) return { schemaVersion: "yichus-requirements-1", description: null, answers: [] };
|
|
1304
|
-
const parsed = Requirements.safeParse(JSON.parse(fs2.readFileSync(file, "utf8")));
|
|
1305
|
-
if (!parsed.success) throw new Error(`${file} is not a yichus-requirements-1 file: ${parsed.error.issues[0]?.message ?? "invalid"}`);
|
|
1306
|
-
return parsed.data;
|
|
1307
|
-
}
|
|
1308
|
-
function saveRequirements(root, requirements) {
|
|
1309
|
-
ensureStudioDir(root);
|
|
1310
|
-
fs2.writeFileSync(requirementsPath(root), `${JSON.stringify(Requirements.parse(requirements), null, 2)}
|
|
1311
|
-
`);
|
|
1312
|
-
}
|
|
1313
|
-
function withNames(requirements, names) {
|
|
1314
|
-
return { ...requirements, names: { ...requirements.names, ...names } };
|
|
1315
|
-
}
|
|
1316
|
-
function withScenarios(requirements, scenarios, drop = []) {
|
|
1317
|
-
const replaced = /* @__PURE__ */ new Set([...scenarios.map((s) => s.entry), ...drop]);
|
|
1318
|
-
return { ...requirements, scenarios: [...(requirements.scenarios ?? []).filter((s) => !replaced.has(s.entry)), ...scenarios] };
|
|
1319
|
-
}
|
|
1320
|
-
function withAnswer(requirements, answer) {
|
|
1321
|
-
return { ...requirements, answers: [...requirements.answers.filter((a) => a.question !== answer.question), answer] };
|
|
1322
|
-
}
|
|
1323
|
-
function ensureStudioDir(root) {
|
|
1324
|
-
const dir = studioDir(root);
|
|
1325
|
-
fs2.mkdirSync(dir, { recursive: true });
|
|
1326
|
-
const ignore = path2.join(dir, ".gitignore");
|
|
1327
|
-
const before = fs2.existsSync(ignore) ? fs2.readFileSync(ignore, "utf8") : "";
|
|
1328
|
-
const kept = new Set(before.split("\n").map((line) => line.trim()));
|
|
1329
|
-
const missing = ["state.json", "marks.json", "viewing.json"].filter((name) => !kept.has(name));
|
|
1330
|
-
if (missing.length) fs2.appendFileSync(ignore, `${before && !before.endsWith("\n") ? "\n" : ""}${missing.map((line) => `${line}
|
|
1331
|
-
`).join("")}`);
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
// src/studio/scenarios.ts
|
|
1335
|
-
import { z as z5 } from "zod";
|
|
1336
|
-
var Effect = z5.looseObject({ op: z5.string(), tables: z5.array(z5.string()).default([]) });
|
|
1337
|
-
var Claim = z5.looseObject({ verdict: z5.string(), parsed: z5.string(), at: z5.string() });
|
|
1338
|
-
var Leaf = z5.looseObject({ kind: z5.literal("leaf"), id: z5.string(), words: z5.string().nullable(), outcome: z5.unknown(), claims: z5.array(Claim).default([]) });
|
|
1339
|
-
var Tree = z5.lazy(() => z5.union([Leaf, z5.looseObject({
|
|
1340
|
-
kind: z5.literal("node"),
|
|
1341
|
-
id: z5.string(),
|
|
1342
|
-
test: z5.string(),
|
|
1343
|
-
words: z5.string().nullable(),
|
|
1344
|
-
arms: z5.array(z5.looseObject({ id: z5.string(), pattern: z5.string(), words: z5.string().nullable(), effects: z5.array(Effect).default([]), next: Tree }))
|
|
1345
|
-
})]));
|
|
1346
|
-
var Entry2 = z5.looseObject({
|
|
1347
|
-
entry: z5.string(),
|
|
1348
|
-
title: z5.string().nullable(),
|
|
1349
|
-
described: z5.boolean(),
|
|
1350
|
-
effects: z5.array(Effect).default([]),
|
|
1351
|
-
tree: Tree.nullable(),
|
|
1352
|
-
unsupported: z5.string().nullable()
|
|
1353
|
-
});
|
|
1354
|
-
var Checked = z5.looseObject({ entry: z5.string(), at: z5.string(), claim: z5.string(), parsed: z5.string(), verdict: z5.string() });
|
|
1355
|
-
var Refused = z5.looseObject({ entry: z5.string(), id: z5.string(), words: z5.string(), reason: z5.string() });
|
|
1356
|
-
var WhatHappens = z5.looseObject({
|
|
1357
|
-
entries: z5.array(Entry2),
|
|
1358
|
-
claims: z5.array(Checked),
|
|
1359
|
-
refused: z5.array(Refused),
|
|
1360
|
-
/** Expectations that are not claims in the grammar, so nothing checks them. */
|
|
1361
|
-
notes: z5.array(z5.looseObject({ entry: z5.string(), at: z5.string(), text: z5.string() })),
|
|
1362
|
-
mismatches: z5.array(z5.looseObject({ entry: z5.string(), reason: z5.string() }))
|
|
1363
|
-
});
|
|
1364
|
-
var effectsText = (effects) => effects.length ? `; effects: ${effects.map(stationLabel).join(", ")}` : "";
|
|
1365
|
-
function walk2(tree, visit) {
|
|
1366
|
-
if (!tree) return;
|
|
1367
|
-
if (tree.kind === "leaf") {
|
|
1368
|
-
visit(tree.id, `outcome: ${outcomeLabel(tree.outcome)}`, tree.words, true, tree.claims.length);
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
visit(tree.id, `the program checks ${tree.test}`, tree.words, false, 0);
|
|
1372
|
-
for (const arm of tree.arms) {
|
|
1373
|
-
if (arm.next.id !== arm.id) visit(arm.id, `when ${arm.pattern}${effectsText(arm.effects)}`, arm.words, false, 0);
|
|
1374
|
-
else if (arm.next.kind === "leaf") {
|
|
1375
|
-
const leaf = arm.next;
|
|
1376
|
-
visit(leaf.id, `when ${arm.pattern}${effectsText(arm.effects)}, outcome: ${outcomeLabel(leaf.outcome)}`, leaf.words ?? arm.words, true, leaf.claims.length);
|
|
1377
|
-
continue;
|
|
1378
|
-
}
|
|
1379
|
-
walk2(arm.next, visit);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
function owed(record) {
|
|
1383
|
-
return record.entries.map((e) => {
|
|
1384
|
-
const unnamed = [], uncovered = [];
|
|
1385
|
-
walk2(e.tree, (id, what, words, leaf, claims) => {
|
|
1386
|
-
if (!words) unnamed.push({ id, what });
|
|
1387
|
-
if (leaf && !claims) uncovered.push(id);
|
|
1388
|
-
});
|
|
1389
|
-
return { entry: e.entry, undescribed: !e.described, unnamed, uncovered };
|
|
1390
|
-
}).filter((o) => o.undescribed || o.unnamed.length || o.uncovered.length);
|
|
1391
|
-
}
|
|
1392
|
-
function unfollowed(record) {
|
|
1393
|
-
return record.entries.filter((e) => !e.tree).map((e) => ({
|
|
1394
|
-
entry: e.entry,
|
|
1395
|
-
why: e.unsupported ?? "the engine gave no reason"
|
|
1396
|
-
}));
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
// src/studio/director.ts
|
|
1400
|
-
var VerifyProperty = z6.looseObject({
|
|
1401
|
-
id: z6.string(),
|
|
1402
|
-
title: z6.string(),
|
|
1403
|
-
status: z6.string(),
|
|
1404
|
-
details: z6.array(z6.string()).default([]),
|
|
1405
|
-
accepted: z6.unknown().optional()
|
|
1406
|
-
});
|
|
1407
|
-
var Verify = z6.looseObject({
|
|
1408
|
-
topology: z6.string().optional(),
|
|
1409
|
-
verdict: z6.looseObject({ proven: z6.boolean(), properties: z6.array(VerifyProperty) })
|
|
1410
|
-
});
|
|
1411
|
-
var Protocol = z6.looseObject({ protocol: z6.looseObject({ status: z6.string(), name: z6.string().optional() }).optional(), error: z6.string().optional() });
|
|
1412
|
-
var caseFile = (root, questionId) => path3.join(studioDir(root), "cases", `${questionId.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "")}.json`);
|
|
1413
|
-
var answerOf = (requirements, id) => requirements.answers.find((a) => a.question === id) ?? null;
|
|
1414
|
-
function instancesFor(requirements) {
|
|
1415
|
-
const choice = answerOf(requirements, "topology")?.choice;
|
|
1416
|
-
return choice === "one" ? "1" : choice === "many" ? "many" : void 0;
|
|
1417
|
-
}
|
|
1418
|
-
async function call(engine, tool, args) {
|
|
1419
|
-
const result = await engine.call(tool, args);
|
|
1420
|
-
if (result.ok !== true) throw new Error(`${tool} failed: ${typeof result.error === "string" ? result.error : "no reason given"}`);
|
|
1421
|
-
return result;
|
|
1422
|
-
}
|
|
1423
|
-
var isAccepted = (p) => p.status === "warning" && p.accepted !== void 0;
|
|
1424
|
-
var worst = (props) => props.find((p) => p.status !== "proven" && !isAccepted(p)) ?? props.find(isAccepted);
|
|
1425
|
-
async function judge(root, engine, sources, question, answer, report, verify) {
|
|
1426
|
-
const base = { question, answer, evidence: [], check: null };
|
|
1427
|
-
if (!answer) return { ...base, status: "unanswered" };
|
|
1428
|
-
const byId = (id) => verify.find((p) => p.id === id);
|
|
1429
|
-
switch (question.kind) {
|
|
1430
|
-
case "platform":
|
|
1431
|
-
return { ...base, status: "decided" };
|
|
1432
|
-
case "topology":
|
|
1433
|
-
return {
|
|
1434
|
-
...base,
|
|
1435
|
-
status: answer.choice === "one" ? "assumed" : "decided",
|
|
1436
|
-
evidence: answer.choice === "one" ? ["Every check below assumes exactly one copy of the server runs."] : []
|
|
1437
|
-
};
|
|
1438
|
-
case "access": {
|
|
1439
|
-
const table = report.dataModel.tables.find((t) => t.name === question.subject);
|
|
1440
|
-
const want = ACCESS_POLICY[answer.choice];
|
|
1441
|
-
const declared = table?.policy ?? null;
|
|
1442
|
-
if (!want || !declared || !declared.startsWith(want)) {
|
|
1443
|
-
return {
|
|
1444
|
-
...base,
|
|
1445
|
-
status: "mismatch",
|
|
1446
|
-
check: "api_report dataModel.tables[].policy",
|
|
1447
|
-
evidence: [`"${question.subject}" is declared ${declared ?? "with no rule"}; the person chose ${want ?? answer.choice}.`]
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
1450
|
-
const prop = byId(`access:${question.subject}`) ?? byId("access-coverage");
|
|
1451
|
-
if (!prop) return { ...base, status: "inconclusive", check: "api_verify", evidence: ["api_verify reported no access property for this table."] };
|
|
1452
|
-
return {
|
|
1453
|
-
...base,
|
|
1454
|
-
check: `api_verify ${prop.id}`,
|
|
1455
|
-
evidence: prop.details,
|
|
1456
|
-
status: prop.status === "proven" ? "proven" : "broken"
|
|
1457
|
-
};
|
|
1458
|
-
}
|
|
1459
|
-
case "together": {
|
|
1460
|
-
const props = verify.filter((p) => p.id === "rmw-transactions" || p.id === "integrity");
|
|
1461
|
-
const bad = worst(props);
|
|
1462
|
-
const mentions = (p) => p.details.filter((d) => d.includes(`"${question.subject}"`));
|
|
1463
|
-
const evidence = props.flatMap((p) => mentions(p).length ? mentions(p) : p.status === "proven" ? [] : p.details);
|
|
1464
|
-
const check = `api_verify ${props.map((p) => p.id).join(" + ")}`;
|
|
1465
|
-
if (!bad) return { ...base, check, evidence, status: props.length ? "proven" : "inconclusive" };
|
|
1466
|
-
return { ...base, check, evidence, status: isAccepted(bad) ? "assumed" : "broken" };
|
|
1467
|
-
}
|
|
1468
|
-
case "partial":
|
|
1469
|
-
case "outside": {
|
|
1470
|
-
if (answer.choice === "partial-ok" || answer.choice === "no-plan") return { ...base, status: "no-check" };
|
|
1471
|
-
const file = caseFile(root, question.id);
|
|
1472
|
-
const rel = path3.relative(root, file).split(path3.sep).join("/");
|
|
1473
|
-
if (!fs3.existsSync(file)) return { ...base, casePath: rel, status: "needs-case", check: "api_protocol_check", evidence: [`No protocol case at ${rel} yet.`] };
|
|
1474
|
-
let protocolCase;
|
|
1475
|
-
try {
|
|
1476
|
-
protocolCase = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
1477
|
-
} catch (error) {
|
|
1478
|
-
return {
|
|
1479
|
-
...base,
|
|
1480
|
-
casePath: rel,
|
|
1481
|
-
check: "api_protocol_check",
|
|
1482
|
-
status: "inconclusive",
|
|
1483
|
-
evidence: [`${rel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`]
|
|
1484
|
-
};
|
|
1485
|
-
}
|
|
1486
|
-
const result = Protocol.parse(await engine.call("api_protocol_check", { sources: [...sources, ...caseSources(root)], case: protocolCase }));
|
|
1487
|
-
const status = result.protocol?.status;
|
|
1488
|
-
const check = `api_protocol_check ${rel}`;
|
|
1489
|
-
if (!status) return { ...base, casePath: rel, check, status: "inconclusive", evidence: [result.error ?? "The protocol check gave no status."] };
|
|
1490
|
-
return {
|
|
1491
|
-
...base,
|
|
1492
|
-
casePath: rel,
|
|
1493
|
-
check,
|
|
1494
|
-
evidence: [`${result.protocol?.name ?? rel}: ${status}`],
|
|
1495
|
-
status: status === "holds" ? "bounded" : status === "violated" ? "broken" : "inconclusive"
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
var QUESTION_PROPERTIES = (id) => id.startsWith("access:") || id === "access-coverage" || id === "rmw-transactions" || id === "integrity";
|
|
1501
|
-
var GUIDE = {
|
|
1502
|
-
platform: "Deploy",
|
|
1503
|
-
topology: "Multi-instance data integrity",
|
|
1504
|
-
access: "Owner-scoped data",
|
|
1505
|
-
together: "Atomic commands with Yichus/Transaction",
|
|
1506
|
-
partial: "Transaction protocols: finite cases, retries, and receipts",
|
|
1507
|
-
outside: "Transaction protocols: finite cases, retries, and receipts"
|
|
1508
|
-
};
|
|
1509
|
-
var askStep = (open) => ({
|
|
1510
|
-
stage: "ask",
|
|
1511
|
-
summary: `${open.length} question${open.length === 1 ? "" : "s"} for you about how the app should behave.`,
|
|
1512
|
-
instructions: [
|
|
1513
|
-
"Ask the person each question below, in its words, with its choices. They may also answer on the studio page.",
|
|
1514
|
-
"Speak in product terms: never quote question ids, code names, file names, tool names or the programming language to the person.",
|
|
1515
|
-
'Do not answer for them and do not pick a default: these are product decisions only they can make. If they are unsure, explain the "why" and the choices.',
|
|
1516
|
-
"Record each answer with yichus_answer (the question id, the choice id, and their own words), then call yichus_next."
|
|
1517
|
-
],
|
|
1518
|
-
ask: open.map((p) => ({ id: p.question.id, ask: p.question.ask, why: p.question.why, choices: p.question.choices }))
|
|
1519
|
-
});
|
|
1520
|
-
function nextStep(state, compileErrors) {
|
|
1521
|
-
if (!state.description) return {
|
|
1522
|
-
stage: "describe",
|
|
1523
|
-
summary: "Waiting for a description of what to build.",
|
|
1524
|
-
instructions: ["Ask the person what they want to build, in their own words. Call yichus_start with their description, unchanged."]
|
|
1525
|
-
};
|
|
1526
|
-
const where = state.properties.find((p) => p.question.kind === "platform");
|
|
1527
|
-
if (!where.answer) return askStep([where]);
|
|
1528
|
-
const deploySection = PLATFORM_SECTION[where.answer.choice];
|
|
1529
|
-
if (!state.files.length) return {
|
|
1530
|
-
stage: "write",
|
|
1531
|
-
summary: "No program yet.",
|
|
1532
|
-
instructions: [
|
|
1533
|
-
`Call yichus_reference with topic "deploy" and build what its "${deploySection}" section says: which pieces run where, the database, and how background work starts.`,
|
|
1534
|
-
'Call api_guide with section "Required workflow", then write the program as .bosatsu files in this project folder.',
|
|
1535
|
-
"For an app that stores records, call api_scaffold first and save its module as a file here. You never need to pass sources: the studio reads every .bosatsu file in the folder.",
|
|
1536
|
-
'Put every decision the app makes in pure functions, and keep the code that reads, writes and calls out free of business rules. When work takes several steps or waits on an outside service (a model, a web page, a payment), write it as one pure step program that a rule-free engine drives: call yichus_reference with topic "portable-programs" before writing it.',
|
|
1537
|
-
"Then call yichus_next."
|
|
1538
|
-
]
|
|
1539
|
-
};
|
|
1540
|
-
if (compileErrors) return {
|
|
1541
|
-
stage: "fix-compile",
|
|
1542
|
-
summary: "The program does not compile yet.",
|
|
1543
|
-
instructions: ["Fix these compile errors in the files, then call yichus_next."],
|
|
1544
|
-
evidence: compileErrors
|
|
1545
|
-
};
|
|
1546
|
-
const unnamed = /* @__PURE__ */ new Map();
|
|
1547
|
-
for (const { question } of state.properties) {
|
|
1548
|
-
if (question.subject !== null && !(question.subject in state.names)) unnamed.set(question.subject, [...unnamed.get(question.subject) ?? [], question.fact]);
|
|
1549
|
-
}
|
|
1550
|
-
for (const { code, what } of state.map ? nameableParts(state.map) : []) {
|
|
1551
|
-
if (!(code in state.names) && !unnamed.has(code)) unnamed.set(code, [`${what[0].toUpperCase()}${what.slice(1)}.`]);
|
|
1552
|
-
}
|
|
1553
|
-
if (unnamed.size) return {
|
|
1554
|
-
stage: "name",
|
|
1555
|
-
summary: "Your agent is putting the parts of your app into plain words.",
|
|
1556
|
-
instructions: [
|
|
1557
|
-
`The person sees these parts of the program in questions and in the picture of the program. Give each a short name in the person's own vocabulary, never a code name, path or table name (for example "family lists" or "starting a new list"). Name a kind of value plainly too ("text", "a number"):`,
|
|
1558
|
-
...[...unnamed].map(([subject, facts]) => `${subject}: ${facts.join(" ")}`),
|
|
1559
|
-
"Call yichus_name with all of them at once, then call yichus_next."
|
|
1560
|
-
]
|
|
1561
|
-
};
|
|
1562
|
-
const unanswered = state.properties.filter((p) => p.status === "unanswered");
|
|
1563
|
-
if (unanswered.length) return askStep(unanswered);
|
|
1564
|
-
const scenarios = state.whatHappens;
|
|
1565
|
-
const owes = scenarios ? owed(scenarios) : [];
|
|
1566
|
-
if (scenarios && (owes.length || scenarios.refused.length || scenarios.notes.length || scenarios.mismatches.length)) return {
|
|
1567
|
-
stage: "scenarios",
|
|
1568
|
-
summary: "Your agent is writing down, in your words, what should happen in each thing your app does.",
|
|
1569
|
-
instructions: [
|
|
1570
|
-
"Write down what should happen in each action below, as the person would say it, and call yichus_scenario with all of them at once. For each action give:",
|
|
1571
|
-
'title: what the person does ("You add a note").',
|
|
1572
|
-
'names: for every id listed under the action, a few words the person would use: the question the app asks itself ("are you signed in?"), each answer, and each outcome ("your note is saved").',
|
|
1573
|
-
'expect: for every outcome, what must and must not happen there, each {"at": an id, "claim": a claim}. A claim is [not] reads TABLE, writes TABLE, refused, returns CONSTRUCTOR, reaches model or reaches net; a claim at an id covers every outcome under it.',
|
|
1574
|
-
"State what the person wants, from their description and answers, never what the code happens to do. Where you cannot tell, ask the person in product terms.",
|
|
1575
|
-
...scenarios.notes.length ? ["Some expectations are not claims, so nothing checks them (the evidence lists them). Write each as a claim in the grammar above. If what the person expects cannot be said that way, tell them it will not be checked and leave it out."] : [],
|
|
1576
|
-
...scenarios.mismatches.length ? ["Some scenarios are for actions the app no longer has (the evidence lists them). If the person dropped the action, pass it in drop. If it was renamed or moved, describe the new action and drop the old one in the same call."] : [],
|
|
1577
|
-
...owes.flatMap((o) => [
|
|
1578
|
-
`${o.entry}${o.undescribed ? " (not described yet)" : ""}:`,
|
|
1579
|
-
...o.unnamed.map((u) => ` ${u.id}: ${u.what}`),
|
|
1580
|
-
...o.uncovered.length ? [` outcomes no claim covers: ${o.uncovered.join(", ")}`] : []
|
|
1581
|
-
]),
|
|
1582
|
-
"Then call yichus_next."
|
|
1583
|
-
],
|
|
1584
|
-
evidence: [
|
|
1585
|
-
...scenarios.refused.map((r) => `${r.entry}: "${r.words}" at ${r.id} was refused: ${r.reason}`),
|
|
1586
|
-
...scenarios.notes.map((n) => `${n.entry}: "${n.text}" at ${n.at} is not a claim`),
|
|
1587
|
-
...scenarios.mismatches.map((m) => `${m.entry}: ${m.reason}`)
|
|
1588
|
-
]
|
|
1589
|
-
};
|
|
1590
|
-
const mismatched = state.properties.filter((p) => p.status === "mismatch");
|
|
1591
|
-
if (mismatched.length) return {
|
|
1592
|
-
stage: "declare",
|
|
1593
|
-
summary: "The program's declared rules do not match what you chose yet.",
|
|
1594
|
-
instructions: [
|
|
1595
|
-
...mismatched.map((p) => `Make the AccessSpec rule for "${p.question.subject}" ${ACCESS_POLICY[p.answer.choice]} (api_add_rule gives the snippet; api_guide section "${p.answer.choice === "owner" ? "Owner-scoped data" : p.answer.choice === "shared" ? "Guarded data" : "Public data"}" explains it).`),
|
|
1596
|
-
"Then call yichus_next."
|
|
1597
|
-
],
|
|
1598
|
-
evidence: mismatched.flatMap((p) => p.evidence)
|
|
1599
|
-
};
|
|
1600
|
-
const broken = state.properties.filter((p) => p.status === "broken" || p.status === "inconclusive");
|
|
1601
|
-
const claims = (scenarios?.claims ?? []).filter((c) => c.verdict !== "holds");
|
|
1602
|
-
const unfollowedEntries = scenarios ? unfollowed(scenarios) : [];
|
|
1603
|
-
const unchecked = (state.whatHappensError ? 1 : 0) + unfollowedEntries.length;
|
|
1604
|
-
const open = broken.length + state.other.length + claims.length + unchecked;
|
|
1605
|
-
if (open) return {
|
|
1606
|
-
stage: "fix",
|
|
1607
|
-
summary: `${open} propert${open === 1 ? "y is" : "ies are"} not holding yet.`,
|
|
1608
|
-
instructions: [
|
|
1609
|
-
"Fix the program so each property below holds. The evidence lines are the engine's own words.",
|
|
1610
|
-
...broken.map((p) => `"${p.question.ask}" The person chose "${p.question.choices.find((c) => c.id === p.answer?.choice)?.label}". Read api_guide section "${GUIDE[p.question.kind]}".`),
|
|
1611
|
-
...state.other.map((o) => `${o.title} (${o.id}) is ${o.status}.`),
|
|
1612
|
-
...claims.map((c) => `${c.entry}: the person expects "${c.claim}" at ${c.at}, and the engine finds it ${c.verdict === "fails" ? "fails" : "cannot be decided"} there.`),
|
|
1613
|
-
...state.whatHappensError ? ["The engine could not follow each action through, so no scenario is checked. If the error below points at the program, fix the program. Otherwise tell the person their scenarios cannot be checked yet, and never tell them their choices hold."] : [],
|
|
1614
|
-
...unfollowedEntries.length ? ["The engine could not follow some actions through (the evidence says which and why), so nothing the person expects of them is checked. If the reason points at the program, such as an action with too many ways through, change the program so it can be followed. Otherwise tell the person those actions are not checked, and never tell them their choices hold."] : [],
|
|
1615
|
-
"Never weaken a rule, delete a check or a claim, or declare one server to make a check pass: change the code. If a claim misread what the person wants, ask them and correct the scenario. Then call yichus_next."
|
|
1616
|
-
],
|
|
1617
|
-
evidence: [
|
|
1618
|
-
...broken.flatMap((p) => p.evidence),
|
|
1619
|
-
...state.other.flatMap((o) => o.details),
|
|
1620
|
-
...claims.map((c) => `${c.entry} at ${c.at}: ${c.parsed}: ${c.verdict}`),
|
|
1621
|
-
...state.whatHappensError ? [state.whatHappensError] : [],
|
|
1622
|
-
...unfollowedEntries.map((u) => `${u.entry} could not be followed: ${u.why}`)
|
|
1623
|
-
]
|
|
1624
|
-
};
|
|
1625
|
-
const cases = state.properties.filter((p) => p.status === "needs-case");
|
|
1626
|
-
if (cases.length) return {
|
|
1627
|
-
stage: "write-case",
|
|
1628
|
-
summary: "Writing the failure scenarios that test your answers.",
|
|
1629
|
-
instructions: [
|
|
1630
|
-
'Call yichus_reference with topic "protocol-case": it returns the case specification and a complete worked case with its program.',
|
|
1631
|
-
"Put any Bosatsu observation predicates the case names in .yichus/cases/*.bosatsu. The studio adds those files to protocol checks only, so they never count as part of the app.",
|
|
1632
|
-
...cases.map((p) => `Write ${p.casePath}: a protocol-case-1.0 for ${p.question.subject} whose invariants state the person's choice ("${p.question.choices.find((c) => c.id === p.answer?.choice)?.label}"), with crashes${p.question.kind === "outside" ? " and failed external results" : ""} at each step.`),
|
|
1633
|
-
"State the person's rule exactly; never weaken it to match the code. Then call yichus_next."
|
|
1634
|
-
]
|
|
1635
|
-
};
|
|
1636
|
-
const assumed = state.properties.filter((p) => p.status === "assumed");
|
|
1637
|
-
return {
|
|
1638
|
-
stage: "done",
|
|
1639
|
-
summary: assumed.length ? "Everything you chose holds, under the assumptions shown." : "Everything you chose holds.",
|
|
1640
|
-
instructions: [
|
|
1641
|
-
"Tell the person every property they chose holds, and point them at the studio page for the evidence.",
|
|
1642
|
-
...assumed.length ? ["Say plainly which results depend on running exactly one server."] : [],
|
|
1643
|
-
"When they describe a new feature, change the code and call yichus_next again: new facts can raise new questions.",
|
|
1644
|
-
`When they want the app online, follow the "${deploySection}" section of yichus_reference topic "deploy"; api_deploy generates the server.`
|
|
1645
|
-
]
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
async function direct({ root, engine, requirements, withMap = true, previous = null }) {
|
|
1649
|
-
const sources = projectSources(root);
|
|
1650
|
-
const hash = sourcesHash(sources);
|
|
1651
|
-
const instances = instancesFor(requirements);
|
|
1652
|
-
const base = {
|
|
1653
|
-
schemaVersion: "yichus-studio-state-1",
|
|
1654
|
-
project: path3.basename(path3.resolve(root)),
|
|
1655
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1656
|
-
sourcesHash: hash,
|
|
1657
|
-
files: sources.map((s) => s.fileName),
|
|
1658
|
-
description: requirements.description,
|
|
1659
|
-
names: requirements.names ?? {},
|
|
1660
|
-
topology: null,
|
|
1661
|
-
proven: null,
|
|
1662
|
-
properties: [],
|
|
1663
|
-
other: [],
|
|
1664
|
-
map: null,
|
|
1665
|
-
mapError: null,
|
|
1666
|
-
layers: null,
|
|
1667
|
-
whatHappens: null,
|
|
1668
|
-
whatHappensError: null
|
|
1669
|
-
};
|
|
1670
|
-
let compileErrors = null;
|
|
1671
|
-
if (requirements.description) base.properties.push(await judge(root, engine, sources, platform, answerOf(requirements, platform.id), Report.parse({}), []));
|
|
1672
|
-
if (requirements.description && sources.length) {
|
|
1673
|
-
const verify = Verify.parse(await call(engine, "api_verify", { sources, ...instances ? { instances } : {} }));
|
|
1674
|
-
base.topology = verify.topology ?? null;
|
|
1675
|
-
base.proven = verify.verdict.proven;
|
|
1676
|
-
const compiles = verify.verdict.properties.find((p) => p.id === "compiles");
|
|
1677
|
-
if (compiles && compiles.status !== "proven") compileErrors = compiles.details;
|
|
1678
|
-
else {
|
|
1679
|
-
const report = Report.parse(await call(engine, "api_report", { sources }));
|
|
1680
|
-
const structureRaw = await call(engine, "api_organize", { sources, lens: "structure", requirements });
|
|
1681
|
-
const structure = Structure.parse(structureRaw);
|
|
1682
|
-
for (const question of questionsFor(report, structure, base.names)) {
|
|
1683
|
-
base.properties.push(await judge(root, engine, sources, question, answerOf(requirements, question.id), report, verify.verdict.properties));
|
|
1684
|
-
}
|
|
1685
|
-
base.other = verify.verdict.properties.filter((p) => !QUESTION_PROPERTIES(p.id) && p.status !== "proven" && !isAccepted(p)).map(({ id, title: title2, status, details }) => ({ id, title: title2, status, details }));
|
|
1686
|
-
try {
|
|
1687
|
-
const access = await call(engine, "api_access_check", { sources });
|
|
1688
|
-
base.whatHappens = WhatHappens.parse(buildWhatHappens({ structure: structureRaw, access }, { program: base.project }));
|
|
1689
|
-
} catch (error) {
|
|
1690
|
-
base.whatHappensError = error instanceof Error ? error.message : String(error);
|
|
1691
|
-
}
|
|
1692
|
-
if (withMap) {
|
|
1693
|
-
if (previous?.sourcesHash === hash && previous.map) {
|
|
1694
|
-
base.map = previous.map;
|
|
1695
|
-
base.layers = previous.layers ?? null;
|
|
1696
|
-
} else {
|
|
1697
|
-
try {
|
|
1698
|
-
const [abstractions, layers] = [
|
|
1699
|
-
await call(engine, "api_organize", { sources, lens: "abstractions" }),
|
|
1700
|
-
await call(engine, "api_organize", { sources, lens: "layers" })
|
|
1701
|
-
];
|
|
1702
|
-
base.map = buildSystemMap({ report, structure: structureRaw, abstractions, layers }, { program: base.project });
|
|
1703
|
-
base.layers = layers;
|
|
1704
|
-
} catch (error) {
|
|
1705
|
-
base.mapError = error instanceof Error ? error.message : String(error);
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
const state = { ...base, step: nextStep(base, compileErrors) };
|
|
1712
|
-
writeState(root, state);
|
|
1713
|
-
return state;
|
|
1714
|
-
}
|
|
1715
|
-
function writeState(root, state) {
|
|
1716
|
-
ensureStudioDir(root);
|
|
1717
|
-
fs3.writeFileSync(path3.join(studioDir(root), "state.json"), `${JSON.stringify(state, null, 2)}
|
|
1718
|
-
`);
|
|
1719
|
-
}
|
|
1720
|
-
function readState(root) {
|
|
1721
|
-
const file = path3.join(studioDir(root), "state.json");
|
|
1722
|
-
return fs3.existsSync(file) ? JSON.parse(fs3.readFileSync(file, "utf8")) : null;
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
// src/studio/mcp.ts
|
|
1726
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1727
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1728
|
-
import { z as z10 } from "zod";
|
|
1729
|
-
|
|
1730
|
-
// src/studio/marks.ts
|
|
1731
|
-
import fs4 from "fs";
|
|
1732
|
-
import path4 from "path";
|
|
1733
|
-
import { z as z8 } from "zod";
|
|
1734
|
-
|
|
1735
|
-
// src/studio/person.ts
|
|
1736
|
-
import { createHash as createHash2 } from "crypto";
|
|
1737
|
-
import { z as z7 } from "zod";
|
|
1738
|
-
|
|
1739
|
-
// ../../web/layer-groups.js
|
|
1740
|
-
var dimensions = Object.freeze({
|
|
1741
|
-
connections: "packages they connect to",
|
|
1742
|
-
types: "types they share",
|
|
1743
|
-
tables: "tables they touch",
|
|
1744
|
-
none: "no grouping"
|
|
1745
|
-
});
|
|
1746
|
-
function usesElsewhere(data, nodeOf = new Map(data.packages.map((p) => [p.name, p.name]))) {
|
|
1747
|
-
const pkg = (id) => id.slice(0, id.indexOf("::"));
|
|
1748
|
-
const uses = new Map([...new Set(nodeOf.values())].map((n) => [n, 0]));
|
|
1749
|
-
const add = (from, to, n) => {
|
|
1750
|
-
if (nodeOf.get(from) !== nodeOf.get(to)) uses.set(nodeOf.get(to), uses.get(nodeOf.get(to)) + n);
|
|
1751
|
-
};
|
|
1752
|
-
for (const e of data.crossEdges) add(pkg(e.from), pkg(e.to), 1);
|
|
1753
|
-
for (const e of data.packageEdges) add(e.from, e.to, e.typeNames.length);
|
|
1754
|
-
return uses;
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
// src/studio/person.ts
|
|
1758
|
-
var named = (names, code) => names[code] ?? null;
|
|
1759
|
-
var theirs = (words, missing) => words === null ? missing : { theirs: words };
|
|
1760
|
-
var Package = z7.looseObject({
|
|
1761
|
-
name: z7.string(),
|
|
1762
|
-
level: z7.number(),
|
|
1763
|
-
reads: z7.array(z7.string()),
|
|
1764
|
-
writes: z7.array(z7.string()),
|
|
1765
|
-
handedIn: z7.array(z7.string())
|
|
1766
|
-
});
|
|
1767
|
-
var Layers = z7.looseObject({
|
|
1768
|
-
layers: z7.looseObject({ packages: z7.array(Package), packageEdges: z7.array(z7.looseObject({})), crossEdges: z7.array(z7.looseObject({})) })
|
|
1769
|
-
});
|
|
1770
|
-
function personSections(layers, names, anchors = /* @__PURE__ */ new Map()) {
|
|
1771
|
-
const parsed = Layers.safeParse(layers);
|
|
1772
|
-
if (!parsed.success) return null;
|
|
1773
|
-
const data = parsed.data.layers;
|
|
1774
|
-
const uses = usesElsewhere(data);
|
|
1775
|
-
const levels = [...new Set(data.packages.map((p) => p.level))].sort((a, b) => b - a);
|
|
1776
|
-
return levels.map((level, i) => data.packages.filter((p) => p.level === level).map((p) => {
|
|
1777
|
-
const own = (tables) => [...new Set(tables.filter((t) => !p.handedIn.includes(t)).map((t) => named(names, t)))];
|
|
1778
|
-
const changes = own(p.writes);
|
|
1779
|
-
return { code: p.name, name: named(names, p.name), changes, looks: own(p.reads).filter((t) => !changes.includes(t)), uses: uses.get(p.name) };
|
|
1780
|
-
}).sort((a, b) => b.uses - a.uses).map(({ code, ...section }, k) => {
|
|
1781
|
-
const anchor = `section-${i}-${k}`;
|
|
1782
|
-
anchors.set(code, anchor);
|
|
1783
|
-
return { anchor, ...section };
|
|
1784
|
-
}));
|
|
1785
|
-
}
|
|
1786
|
-
var Property2 = z7.object({ negated: z7.boolean(), is: z7.enum(["reads", "writes", "refused", "returns", "reaches"]), subject: z7.string().optional() });
|
|
1787
|
-
var Claim2 = z7.looseObject({ property: Property2, verdict: z7.enum(["holds", "fails", "inconclusive"]) });
|
|
1788
|
-
var Leaf2 = z7.looseObject({
|
|
1789
|
-
kind: z7.literal("leaf"),
|
|
1790
|
-
id: z7.string(),
|
|
1791
|
-
words: z7.string().nullable(),
|
|
1792
|
-
status: z7.enum(["grey", "red", "green", "unchecked"]),
|
|
1793
|
-
outcome: z7.looseObject({ kind: z7.string() }),
|
|
1794
|
-
claims: z7.array(Claim2),
|
|
1795
|
-
witnesses: z7.array(z7.string())
|
|
1796
|
-
});
|
|
1797
|
-
var Tree2 = z7.lazy(() => z7.union([Leaf2, z7.looseObject({
|
|
1798
|
-
kind: z7.literal("node"),
|
|
1799
|
-
id: z7.string(),
|
|
1800
|
-
words: z7.string().nullable(),
|
|
1801
|
-
arms: z7.array(z7.looseObject({ id: z7.string(), words: z7.string().nullable(), next: Tree2 }))
|
|
1802
|
-
})]));
|
|
1803
|
-
var Happens = z7.looseObject({
|
|
1804
|
-
entries: z7.array(z7.looseObject({ entry: z7.string(), title: z7.string().nullable(), described: z7.boolean(), tree: Tree2.nullable() })),
|
|
1805
|
-
witnesses: z7.array(z7.looseObject({ id: z7.string(), resource: z7.string() }))
|
|
1806
|
-
});
|
|
1807
|
-
function sayClaim({ negated, is, subject }, names) {
|
|
1808
|
-
const thing = theirs(subject === void 0 ? null : named(names, subject), "something not named yet");
|
|
1809
|
-
const said = is === "refused" ? ["turned away"] : is === "reaches" ? [subject === "model" ? "asks the AI model" : "reaches the internet"] : [is === "reads" ? "looks at " : is === "writes" ? "changes " : "answers ", thing];
|
|
1810
|
-
return !negated ? said : is === "refused" ? ["not turned away"] : ["never ", ...said];
|
|
1811
|
-
}
|
|
1812
|
-
function personWhatHappens(record, names, anchors = /* @__PURE__ */ new Map()) {
|
|
1813
|
-
const parsed = Happens.safeParse(record);
|
|
1814
|
-
if (!parsed.success) return null;
|
|
1815
|
-
const reach = new Map(parsed.data.witnesses.map((w) => [w.id, named(names, w.resource)]));
|
|
1816
|
-
const VERDICT = { holds: "holds", fails: "fails", inconclusive: "cannot say" };
|
|
1817
|
-
const at = (id, anchor) => {
|
|
1818
|
-
anchors.set(id, anchor);
|
|
1819
|
-
return anchor;
|
|
1820
|
-
};
|
|
1821
|
-
const step = (t, said, place) => {
|
|
1822
|
-
if (t.kind === "leaf") return {
|
|
1823
|
-
kind: "outcome",
|
|
1824
|
-
anchor: at(t.id, place),
|
|
1825
|
-
words: t.words === said ? null : t.words,
|
|
1826
|
-
refused: t.outcome.kind === "refused",
|
|
1827
|
-
status: t.status,
|
|
1828
|
-
checks: [
|
|
1829
|
-
...t.claims.map((c) => ({ says: sayClaim(c.property, names), verdict: VERDICT[c.verdict] })),
|
|
1830
|
-
...t.witnesses.map((w) => ({ says: ["someone else's ", theirs(reach.get(w) ?? null, "records"), " can be reached here"], verdict: "fails" }))
|
|
1831
|
-
]
|
|
1832
|
-
};
|
|
1833
|
-
return { kind: "choice", anchor: at(t.id, place), words: t.words, ways: t.arms.map((a, k) => {
|
|
1834
|
-
const then = step(a.next, a.next.id === a.id ? a.words : null, `${place}-${k}`);
|
|
1835
|
-
return { anchor: a.next.id === a.id ? then.anchor : at(a.id, `${place}-${k}-way`), words: a.words, then };
|
|
1836
|
-
}) };
|
|
1837
|
-
};
|
|
1838
|
-
const described = parsed.data.entries.filter((e) => e.described);
|
|
1839
|
-
return {
|
|
1840
|
-
actions: described.map((e, i) => ({ anchor: at(e.entry, `action-${i}`), title: e.title, first: e.tree && step(e.tree, null, `action-${i}-step`) })),
|
|
1841
|
-
waiting: parsed.data.entries.length - described.length
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
|
-
function personDiagrams(state, names) {
|
|
1845
|
-
const anchors = /* @__PURE__ */ new Map();
|
|
1846
|
-
const sections = personSections(state.layers, names, anchors);
|
|
1847
|
-
const whatHappens = state.whatHappens ? personWhatHappens(state.whatHappens, names, anchors) : null;
|
|
1848
|
-
const drawing = createHash2("sha256").update(JSON.stringify([...anchors])).digest("hex").slice(0, 16);
|
|
1849
|
-
return { sections, whatHappens, anchors, drawing };
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
// src/studio/marks.ts
|
|
1853
|
-
var Mark = z8.object({ at: z8.string().min(1), note: z8.string().trim().min(1).max(280) }).strict();
|
|
1854
|
-
var Marks = z8.object({ schemaVersion: z8.literal("yichus-marks-1"), said: z8.string(), marks: z8.array(Mark) });
|
|
1855
|
-
var PointArgs = z8.object({ marks: z8.array(Mark) }).strict();
|
|
1856
|
-
var marksPath = (root) => path4.join(studioDir(root), "marks.json");
|
|
1857
|
-
function loadMarks(root) {
|
|
1858
|
-
const file = marksPath(root);
|
|
1859
|
-
if (!fs4.existsSync(file)) return null;
|
|
1860
|
-
const parsed = Marks.safeParse(JSON.parse(fs4.readFileSync(file, "utf8")));
|
|
1861
|
-
if (!parsed.success) throw new Error(`${file} is not a yichus-marks-1 file: ${z8.prettifyError(parsed.error)}`);
|
|
1862
|
-
return parsed.data;
|
|
1863
|
-
}
|
|
1864
|
-
function pointable(root) {
|
|
1865
|
-
const state = readState(root);
|
|
1866
|
-
return new Set(state ? personDiagrams(state, loadRequirements(root).names ?? {}).anchors.keys() : []);
|
|
1867
|
-
}
|
|
1868
|
-
function recordMarks(root, args, now) {
|
|
1869
|
-
const parsed = PointArgs.safeParse(args);
|
|
1870
|
-
if (!parsed.success) return { ok: false, error: `yichus_point needs marks, each with at and a note of at most 280 characters. ${z8.prettifyError(parsed.error)}` };
|
|
1871
|
-
const known = pointable(root);
|
|
1872
|
-
const unknown = parsed.data.marks.map((m) => m.at).filter((at) => !known.has(at));
|
|
1873
|
-
if (unknown.length) return { ok: false, error: `The page draws nothing at ${unknown.join(", ")}. Point at a section (a part's code name as yichus_name took it), an action once yichus_scenario described it, or an id yichus_next listed under a described action; call yichus_next for the latest ids.` };
|
|
1874
|
-
ensureStudioDir(root);
|
|
1875
|
-
fs4.writeFileSync(marksPath(root), `${JSON.stringify(Marks.parse({ schemaVersion: "yichus-marks-1", said: now.toISOString(), marks: parsed.data.marks }), null, 2)}
|
|
1876
|
-
`);
|
|
1877
|
-
return { ok: true };
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
// src/studio/page.ts
|
|
1881
|
-
var PAGE_STATUS = {
|
|
1882
|
-
unanswered: ["waiting for your answer", "quiet"],
|
|
1883
|
-
decided: ["decided", "good"],
|
|
1884
|
-
assumed: ["holds, under an assumption", "warn"],
|
|
1885
|
-
proven: ["proven", "good"],
|
|
1886
|
-
bounded: ["holds in every scenario checked", "good"],
|
|
1887
|
-
broken: ["broken", "bad"],
|
|
1888
|
-
mismatch: ["the code says otherwise", "bad"],
|
|
1889
|
-
"needs-case": ["scenario being written", "quiet"],
|
|
1890
|
-
inconclusive: ["not decided yet", "warn"],
|
|
1891
|
-
"no-check": ["no check needed", "quiet"],
|
|
1892
|
-
pending: ["answered, waiting for your agent to check", "quiet"]
|
|
3
|
+
// package.json
|
|
4
|
+
var engines = {
|
|
5
|
+
node: ">=20"
|
|
1893
6
|
};
|
|
1894
|
-
var PAGE_HEADINGS = {
|
|
1895
|
-
asked: "What you asked for",
|
|
1896
|
-
now: "What is happening now",
|
|
1897
|
-
questions: "Questions for you",
|
|
1898
|
-
props: "What your program must do",
|
|
1899
|
-
happens: "What happens when",
|
|
1900
|
-
sections: "How your app is built",
|
|
1901
|
-
map: "The shape of your program"
|
|
1902
|
-
};
|
|
1903
|
-
var STUDIO_PAGE = `<!doctype html>
|
|
1904
|
-
<html lang="en">
|
|
1905
|
-
<head>
|
|
1906
|
-
<meta charset="utf-8">
|
|
1907
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1908
|
-
<title>Yichus studio</title>
|
|
1909
|
-
<link rel="stylesheet" href="system-map.css">
|
|
1910
|
-
<style>
|
|
1911
|
-
:root { --ink:#0f172a; --muted:#475569; --faint:#94a3b8; --rule:#e2e8f0; --paper:#ffffff; --page:#f6f8fb;
|
|
1912
|
-
--good:#047857; --goodSoft:#ecfdf5; --bad:#b91c1c; --badSoft:#fef2f2; --warn:#92400e; --warnSoft:#fffbeb; --accent:#22675e;
|
|
1913
|
-
--agent:#4f46e5; --agentSoft:rgba(79,70,229,0.16); }
|
|
1914
|
-
* { box-sizing:border-box; }
|
|
1915
|
-
body { margin:0; background:var(--page); color:var(--ink); font:16px/1.55 Inter, system-ui, sans-serif; }
|
|
1916
|
-
main { max-width:1100px; margin:0 auto; padding:24px 16px 64px; }
|
|
1917
|
-
header p { margin:4px 0 0; color:var(--muted); }
|
|
1918
|
-
h1 { font-size:1.6rem; margin:0; font-weight:600; }
|
|
1919
|
-
h2 { font-size:1.15rem; margin:32px 0 8px; }
|
|
1920
|
-
section > p.lede { color:var(--muted); margin:0 0 12px; max-width:70ch; }
|
|
1921
|
-
.frame { background:var(--paper); border:1px solid var(--rule); border-radius:10px; padding:16px; position:relative; }
|
|
1922
|
-
.frame > .tag { position:absolute; top:-10px; left:12px; background:var(--page); padding:0 6px; font-size:0.72rem; letter-spacing:0.08em; text-transform:uppercase; color:var(--accent); font-weight:700; }
|
|
1923
|
-
.yours { border-left:3px solid var(--accent); padding:4px 12px; margin:0; font-style:italic; }
|
|
1924
|
-
.label { font-size:0.75rem; color:var(--muted); text-transform:uppercase; letter-spacing:0.06em; margin:0 0 4px; }
|
|
1925
|
-
.now { font-weight:600; }
|
|
1926
|
-
.question { border-top:1px solid var(--rule); padding:14px 0; }
|
|
1927
|
-
.question:first-of-type { border-top:none; padding-top:4px; }
|
|
1928
|
-
.question .ask { font-weight:600; margin:0 0 4px; }
|
|
1929
|
-
.question .why { color:var(--muted); margin:0 0 8px; font-size:0.94rem; }
|
|
1930
|
-
.choice { display:flex; gap:8px; align-items:flex-start; margin:6px 0; }
|
|
1931
|
-
.choice span small { display:block; color:var(--muted); }
|
|
1932
|
-
textarea { width:100%; min-height:3.2em; font:inherit; border:1px solid var(--rule); border-radius:8px; padding:8px; margin:6px 0; }
|
|
1933
|
-
button { font:inherit; font-weight:600; border:none; border-radius:8px; padding:8px 14px; background:var(--accent); color:#fff; cursor:pointer; }
|
|
1934
|
-
button:disabled { opacity:0.5; cursor:default; }
|
|
1935
|
-
.props { list-style:none; margin:0; padding:0; }
|
|
1936
|
-
.props li { border-top:1px solid var(--rule); padding:12px 0; display:grid; grid-template-columns:1fr auto; gap:4px 16px; }
|
|
1937
|
-
.props li:first-child { border-top:none; }
|
|
1938
|
-
.chip { font-size:0.8rem; font-weight:700; border-radius:999px; padding:2px 10px; white-space:nowrap; align-self:start; justify-self:start; }
|
|
1939
|
-
.chip.good { color:var(--good); background:var(--goodSoft); }
|
|
1940
|
-
.chip.bad { color:var(--bad); background:var(--badSoft); }
|
|
1941
|
-
.chip.warn { color:var(--warn); background:var(--warnSoft); }
|
|
1942
|
-
.chip.quiet { color:var(--muted); background:var(--rule); }
|
|
1943
|
-
.props .choice-said { color:var(--muted); font-size:0.94rem; grid-column:1 / -1; }
|
|
1944
|
-
details { grid-column:1 / -1; font-size:0.88rem; }
|
|
1945
|
-
details pre { white-space:pre-wrap; word-break:break-word; background:var(--page); border-radius:8px; padding:8px; margin:6px 0 0; font-size:0.82rem; }
|
|
1946
|
-
.empty { color:var(--muted); }
|
|
1947
|
-
.studio-action { border-top:1px solid var(--rule); padding:12px 0; }
|
|
1948
|
-
.studio-action:first-child { border-top:none; padding-top:0; }
|
|
1949
|
-
.studio-action h3 { font-size:1rem; margin:0 0 8px; font-weight:600; }
|
|
1950
|
-
.studio-ask, .studio-way { margin:0; }
|
|
1951
|
-
.studio-ways { list-style:none; margin:4px 0 0 9px; padding:0 0 0 14px; border-left:2px solid var(--rule); }
|
|
1952
|
-
.studio-ways > li { position:relative; padding:4px 0; }
|
|
1953
|
-
.studio-ways > li::before { content:''; position:absolute; left:-14px; top:15px; width:10px; border-top:2px solid var(--rule); }
|
|
1954
|
-
.studio-way, .studio-on { color:var(--muted); }
|
|
1955
|
-
.studio-on { margin:4px 0 0; }
|
|
1956
|
-
.studio-fold summary { cursor:pointer; color:var(--muted); }
|
|
1957
|
-
.studio-folded { margin:4px 0 0; padding-left:10px; border-left:2px dashed var(--rule); }
|
|
1958
|
-
.studio-outcome { display:grid; grid-template-columns:20px minmax(0, 1fr); align-items:center; gap:2px 8px; margin:2px 0 0; }
|
|
1959
|
-
.studio-said, .studio-ask, .studio-way, .studio-checks li { overflow-wrap:anywhere; }
|
|
1960
|
-
.studio-end { font-weight:600; }
|
|
1961
|
-
.studio-outcome[data-status="red"] .studio-end { color:var(--bad); }
|
|
1962
|
-
.studio-outcome[data-status="green"] .studio-end { color:var(--good); }
|
|
1963
|
-
.studio-checks { list-style:none; margin:0; padding:0; grid-column:2; display:flex; flex-wrap:wrap; gap:2px 12px; font-size:0.9rem; color:var(--muted); }
|
|
1964
|
-
.studio-checks li { min-width:0; }
|
|
1965
|
-
.studio-checks [data-verdict="fails"] { color:var(--bad); font-weight:600; }
|
|
1966
|
-
.studio-checks [data-verdict="cannot say"] { color:var(--warn); }
|
|
1967
|
-
.studio-unnamed, .studio-waiting { color:var(--faint); }
|
|
1968
|
-
.studio-stack { display:block; max-width:100%; height:auto; }
|
|
1969
|
-
.studio-band { fill:var(--page); }
|
|
1970
|
-
.studio-band[data-shade="1"] { fill:#eef2f7; }
|
|
1971
|
-
.studio-section rect { fill:var(--paper); stroke:var(--faint); }
|
|
1972
|
-
.studio-canvas { fill:var(--paper); }
|
|
1973
|
-
.studio-section-name { fill:var(--ink); }
|
|
1974
|
-
.studio-section[data-named="false"] .studio-section-name { fill:var(--faint); }
|
|
1975
|
-
.studio-section-does { fill:var(--muted); }
|
|
1976
|
-
.agent-layer { position:absolute; left:0; top:0; width:0; height:0; z-index:20; pointer-events:none; }
|
|
1977
|
-
.agent-ring { position:absolute; border-radius:10px; box-shadow:0 0 0 2px var(--agent), 0 0 0 8px var(--agentSoft); }
|
|
1978
|
-
.agent-bubble { position:absolute; pointer-events:auto; background:var(--paper); border:1px solid var(--agent); border-radius:4px 14px 14px 14px;
|
|
1979
|
-
padding:8px 30px 10px 12px; box-shadow:0 10px 28px rgba(15,23,42,0.18); font-size:0.94rem; }
|
|
1980
|
-
.agent-tail { position:absolute; top:-7px; width:12px; height:12px; background:var(--paper); border-left:1px solid var(--agent); border-top:1px solid var(--agent); transform:rotate(45deg); }
|
|
1981
|
-
.agent-bubble[data-side="right"] { border-radius:14px 14px 14px 14px; }
|
|
1982
|
-
.agent-bubble[data-side="right"] .agent-tail { transform:rotate(-45deg); }
|
|
1983
|
-
.agent-who { margin:0 0 2px; font-size:0.72rem; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:var(--agent); }
|
|
1984
|
-
.agent-who span { color:var(--muted); font-weight:600; }
|
|
1985
|
-
.agent-says { margin:0; overflow-wrap:anywhere; }
|
|
1986
|
-
.agent-close { position:absolute; top:4px; right:4px; background:none; color:var(--muted); padding:2px 8px; font-size:1rem; line-height:1; }
|
|
1987
|
-
.agent-in { animation:agent-in 280ms ease-out both; }
|
|
1988
|
-
@keyframes agent-in { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } }
|
|
1989
|
-
@media (prefers-reduced-motion: reduce) { .agent-in { animation:none; } }
|
|
1990
|
-
@media (max-width:640px) { .props li { grid-template-columns:1fr; } }
|
|
1991
|
-
</style>
|
|
1992
|
-
</head>
|
|
1993
|
-
<body>
|
|
1994
|
-
<main>
|
|
1995
|
-
<header>
|
|
1996
|
-
<h1>Yichus studio<span id="project"></span></h1>
|
|
1997
|
-
<p id="updated">Waiting for your agent.</p>
|
|
1998
|
-
<p>Ask your agent what anything here means.</p>
|
|
1999
|
-
</header>
|
|
2000
|
-
<section aria-labelledby="h-asked" data-section="asked">
|
|
2001
|
-
<h2 id="h-asked">${PAGE_HEADINGS.asked}</h2>
|
|
2002
|
-
<p class="label">Your words</p>
|
|
2003
|
-
<p class="yours" id="description">Nothing yet. Tell your agent what you want to build.</p>
|
|
2004
|
-
</section>
|
|
2005
|
-
<section aria-labelledby="h-now" data-section="now">
|
|
2006
|
-
<h2 id="h-now">${PAGE_HEADINGS.now}</h2>
|
|
2007
|
-
<div class="frame"><span class="tag">Yichus output</span>
|
|
2008
|
-
<p class="now" id="summary">No checks have run yet.</p>
|
|
2009
|
-
</div>
|
|
2010
|
-
</section>
|
|
2011
|
-
<section aria-labelledby="h-questions" id="questions-section" data-section="questions" hidden>
|
|
2012
|
-
<h2 id="h-questions">${PAGE_HEADINGS.questions}</h2>
|
|
2013
|
-
<p class="lede">Your agent will ask you these too. Answer here or in the chat, whichever is easier.</p>
|
|
2014
|
-
<div class="frame"><span class="tag">Yichus output</span><div id="questions"></div></div>
|
|
2015
|
-
</section>
|
|
2016
|
-
<section aria-labelledby="h-props" data-section="props">
|
|
2017
|
-
<h2 id="h-props">${PAGE_HEADINGS.props}</h2>
|
|
2018
|
-
<div class="frame"><span class="tag">Yichus output</span><ul class="props" id="props"><li class="empty">No properties chosen yet.</li></ul></div>
|
|
2019
|
-
</section>
|
|
2020
|
-
<section aria-labelledby="h-happens" data-section="happens">
|
|
2021
|
-
<h2 id="h-happens">${PAGE_HEADINGS.happens}</h2>
|
|
2022
|
-
<p class="lede">Each thing your app does, every way it can go, and what you get at the end, in the words you and your agent chose.</p>
|
|
2023
|
-
<p class="label">Drawn by Yichus. Words in italics are your agent's, not checked</p>
|
|
2024
|
-
<div class="frame"><div id="happens"><p class="empty">This appears once the program compiles.</p></div></div>
|
|
2025
|
-
</section>
|
|
2026
|
-
<section aria-labelledby="h-sections" data-section="sections">
|
|
2027
|
-
<h2 id="h-sections">${PAGE_HEADINGS.sections}</h2>
|
|
2028
|
-
<p class="lede">The parts of your app, stacked: each layer is built only from the layers below it.</p>
|
|
2029
|
-
<p class="label">Drawn by Yichus. Words in italics are your agent's, not checked</p>
|
|
2030
|
-
<div class="frame"><div id="sections"><p class="empty">This appears once the program compiles.</p></div></div>
|
|
2031
|
-
</section>
|
|
2032
|
-
<section aria-labelledby="h-map" data-section="map">
|
|
2033
|
-
<h2 id="h-map">${PAGE_HEADINGS.map}</h2>
|
|
2034
|
-
<div class="frame"><div class="system-map" id="map"><p class="empty">The map appears once the program compiles.</p></div></div>
|
|
2035
|
-
</section>
|
|
2036
|
-
</main>
|
|
2037
|
-
<script type="module">
|
|
2038
|
-
import { mount } from './system-map.js';
|
|
2039
|
-
import { mountSections, mountWhatHappens, reportViewing, showMarks } from './studio-views.js';
|
|
2040
|
-
|
|
2041
|
-
const STATUS = ${JSON.stringify(PAGE_STATUS)};
|
|
2042
|
-
const el = (tag, props = {}, ...kids) => { const n = Object.assign(document.createElement(tag), props); n.append(...kids); return n; };
|
|
2043
|
-
let mapHash = null, drawn = '';
|
|
2044
7
|
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
const
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
card.append(el('label', { className: 'choice' }, input, el('span', {}, c.label, el('small', { textContent: c.means }))));
|
|
2053
|
-
}
|
|
2054
|
-
const words = el('textarea', { placeholder: 'In your own words (optional)', value: saved?.words ?? '' });
|
|
2055
|
-
const status = el('span', { className: 'empty', textContent: saved ? ' Saved. Your agent will check it next.' : '' });
|
|
2056
|
-
const save = el('button', { type: 'button', textContent: 'Save answer' });
|
|
2057
|
-
save.addEventListener('click', async () => {
|
|
2058
|
-
const picked = card.querySelector('input[name="' + CSS.escape(name) + '"]:checked');
|
|
2059
|
-
if (!picked) { status.textContent = ' Pick one of the choices first.'; return; }
|
|
2060
|
-
save.disabled = true;
|
|
2061
|
-
const label = q.choices.find(c => c.id === picked.value).label;
|
|
2062
|
-
const response = await fetch('/answer', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
2063
|
-
body: JSON.stringify({ question: q.id, choice: picked.value, words: words.value.trim() || label }) });
|
|
2064
|
-
const result = await response.json();
|
|
2065
|
-
status.textContent = result.ok ? ' Saved. Your agent will check it next.' : ' ' + result.error;
|
|
2066
|
-
save.disabled = false;
|
|
2067
|
-
});
|
|
2068
|
-
card.append(el('p', { className: 'label', textContent: 'Your words' }), words, save, status);
|
|
2069
|
-
return card;
|
|
8
|
+
// src/node-version.ts
|
|
9
|
+
function nodeVersionProblem(running, required) {
|
|
10
|
+
const floor = /^>=(\d+)$/.exec(required);
|
|
11
|
+
if (!floor) throw new Error(`engines.node must read >=N, not ${required}`);
|
|
12
|
+
const needed = Number(floor[1]);
|
|
13
|
+
if (Number(running.split(".")[0]) >= needed) return void 0;
|
|
14
|
+
return `yichus needs Node ${needed} or newer; this is Node ${running}. Install Node ${needed} or newer and run the command again.`;
|
|
2070
15
|
}
|
|
2071
16
|
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
const label = p.question.choices.find(c => c.id === answer.choice)?.label ?? answer.choice;
|
|
2077
|
-
const li = el('li', {}, el('span', { textContent: p.question.ask }), el('span', { className: 'chip ' + tone, textContent: words }));
|
|
2078
|
-
li.dataset.property = p.question.id;
|
|
2079
|
-
li.append(el('span', { className: 'choice-said', textContent: 'You chose: ' + label + (answer.words && answer.words !== label ? ' ("' + answer.words + '")' : '') }));
|
|
2080
|
-
return li;
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
async function refresh() {
|
|
2084
|
-
const { state, requirements } = await (await fetch('/state')).json();
|
|
2085
|
-
const savedOf = id => requirements.answers.find(a => a.question === id);
|
|
2086
|
-
if (requirements.description) document.getElementById('description').textContent = requirements.description;
|
|
2087
|
-
if (!state) return;
|
|
2088
|
-
document.getElementById('project').textContent = ': ' + state.project;
|
|
2089
|
-
document.getElementById('updated').textContent = 'Last checked ' + new Date(state.updatedAt).toLocaleTimeString() + '.';
|
|
2090
|
-
document.getElementById('summary').textContent = state.summary;
|
|
2091
|
-
const open = state.properties.filter(p => p.status === 'unanswered' && !savedOf(p.question.id));
|
|
2092
|
-
document.getElementById('questions-section').hidden = !open.length;
|
|
2093
|
-
// Keep a half-written answer: only rebuild cards when the questions change.
|
|
2094
|
-
const qs = document.getElementById('questions');
|
|
2095
|
-
const ids = open.map(p => p.question.id).join(' ');
|
|
2096
|
-
if (qs.dataset.ids !== ids) { qs.dataset.ids = ids; qs.replaceChildren(...open.map(p => questionCard(p.question, savedOf(p.question.id)))); }
|
|
2097
|
-
const chosen = state.properties.filter(p => p.answer || savedOf(p.question.id));
|
|
2098
|
-
document.getElementById('props').replaceChildren(...(chosen.length ? chosen.map(p => propertyRow(p, savedOf(p.question.id)))
|
|
2099
|
-
: [el('li', { className: 'empty', textContent: 'No properties chosen yet.' })]));
|
|
2100
|
-
// Redrawn only when the words or the program change.
|
|
2101
|
-
const views = JSON.stringify([state.whatHappens, state.sections]);
|
|
2102
|
-
if (views !== drawn) {
|
|
2103
|
-
drawn = views;
|
|
2104
|
-
if (state.whatHappens) mountWhatHappens(document.getElementById('happens'), state.whatHappens);
|
|
2105
|
-
if (state.sections) mountSections(document.getElementById('sections'), state.sections);
|
|
2106
|
-
}
|
|
2107
|
-
const map = document.getElementById('map');
|
|
2108
|
-
if (state.map && state.sourcesHash !== mapHash) { mapHash = state.sourcesHash; mount(map, state.map); }
|
|
2109
|
-
else if (!state.map && state.mapError) map.replaceChildren(el('p', { className: 'empty', textContent: 'The map could not be drawn: ' + state.mapError }));
|
|
2110
|
-
// Last, so each note is placed over the pictures as they are now drawn.
|
|
2111
|
-
showMarks(state.marks);
|
|
2112
|
-
viewing(state.drawing);
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
// What the person has in view goes to the studio, so their agent can follow
|
|
2116
|
-
// along when they say "this one".
|
|
2117
|
-
const viewing = reportViewing(body => fetch('/viewing', { method: 'POST', headers: { 'content-type': 'application/json' }, body }));
|
|
2118
|
-
|
|
2119
|
-
new EventSource('/events').addEventListener('change', () => refresh());
|
|
2120
|
-
refresh();
|
|
2121
|
-
</script>
|
|
2122
|
-
</body>
|
|
2123
|
-
</html>
|
|
2124
|
-
`;
|
|
2125
|
-
|
|
2126
|
-
// src/studio/viewing.ts
|
|
2127
|
-
import fs5 from "fs";
|
|
2128
|
-
import path5 from "path";
|
|
2129
|
-
import { z as z9 } from "zod";
|
|
2130
|
-
var PAGE_SECTIONS = ["asked", "now", "questions", "props", "happens", "sections", "map"];
|
|
2131
|
-
var Id = z9.string().min(1).max(200);
|
|
2132
|
-
var Ids = z9.array(Id).max(1e3);
|
|
2133
|
-
var Section = z9.enum(PAGE_SECTIONS);
|
|
2134
|
-
var Touched = z9.discriminatedUnion("kind", [
|
|
2135
|
-
z9.object({ kind: z9.literal("section"), id: Section }).strict(),
|
|
2136
|
-
z9.object({ kind: z9.enum(["part", "question", "property"]), id: Id }).strict()
|
|
2137
|
-
]);
|
|
2138
|
-
var Report2 = z9.object({
|
|
2139
|
-
/** The page's tab is the one in front. */
|
|
2140
|
-
shown: z9.boolean(),
|
|
2141
|
-
sections: z9.array(Section).max(PAGE_SECTIONS.length),
|
|
2142
|
-
/** Anchors of the drawn parts on screen (person.ts). */
|
|
2143
|
-
parts: Ids,
|
|
2144
|
-
questions: Ids,
|
|
2145
|
-
/** Question ids of the properties shown. */
|
|
2146
|
-
properties: Ids,
|
|
2147
|
-
/** Anchors of the folds the person opened. */
|
|
2148
|
-
open: Ids,
|
|
2149
|
-
touched: Touched.nullable(),
|
|
2150
|
-
/** The drawing the anchors belong to (person.ts), or null before one is drawn. */
|
|
2151
|
-
drawing: z9.string().max(64).nullable()
|
|
2152
|
-
}).strict();
|
|
2153
|
-
var Viewing = Report2.extend({ schemaVersion: z9.literal("yichus-viewing-1"), at: z9.string() }).strict();
|
|
2154
|
-
var viewingPath = (root) => path5.join(studioDir(root), "viewing.json");
|
|
2155
|
-
function loadViewing(root) {
|
|
2156
|
-
const file = viewingPath(root);
|
|
2157
|
-
if (!fs5.existsSync(file)) return null;
|
|
2158
|
-
const parsed = Viewing.safeParse(JSON.parse(fs5.readFileSync(file, "utf8")));
|
|
2159
|
-
if (!parsed.success) throw new Error(`${file} is not a yichus-viewing-1 file: ${z9.prettifyError(parsed.error)}`);
|
|
2160
|
-
return parsed.data;
|
|
2161
|
-
}
|
|
2162
|
-
function recordViewing(root, report, now) {
|
|
2163
|
-
const parsed = Report2.safeParse(report);
|
|
2164
|
-
if (!parsed.success) return { ok: false, error: `The page's report is not one this studio reads. ${z9.prettifyError(parsed.error)}` };
|
|
2165
|
-
ensureStudioDir(root);
|
|
2166
|
-
const file = viewingPath(root), written = `${file}.${process.pid}.tmp`;
|
|
2167
|
-
fs5.writeFileSync(written, `${JSON.stringify(Viewing.parse({ schemaVersion: "yichus-viewing-1", at: now.toISOString(), ...parsed.data }), null, 2)}
|
|
2168
|
-
`);
|
|
2169
|
-
fs5.renameSync(written, file);
|
|
2170
|
-
return { ok: true };
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
// src/studio/page-view.ts
|
|
2174
|
-
var quoted = (words, missing) => words === null ? missing : `"${words}"`;
|
|
2175
|
-
var say = (phrase) => phrase.map((p) => typeof p === "string" ? p : `"${p.theirs}"`).join("");
|
|
2176
|
-
var TICK = { holds: "\u2713", fails: "\u2717", "cannot say": "?" };
|
|
2177
|
-
var ENDING = {
|
|
2178
|
-
grey: "no expectation is false here",
|
|
2179
|
-
red: "something expected is false here",
|
|
2180
|
-
green: "an ending that was broken is fixed",
|
|
2181
|
-
unchecked: "the check cannot say"
|
|
2182
|
-
};
|
|
2183
|
-
function ago(from, now) {
|
|
2184
|
-
const seconds = Math.max(0, Math.round((now.getTime() - Date.parse(from)) / 1e3));
|
|
2185
|
-
const [n, unit] = seconds < 120 ? [seconds, "second"] : seconds < 7200 ? [Math.round(seconds / 60), "minute"] : [Math.round(seconds / 3600), "hour"];
|
|
2186
|
-
return `${n} ${unit}${n === 1 ? "" : "s"} ago`;
|
|
2187
|
-
}
|
|
2188
|
-
function pageText(root, pageUrl, now) {
|
|
2189
|
-
const state = readState(root);
|
|
2190
|
-
const { description, answers, names = {} } = loadRequirements(root);
|
|
2191
|
-
const marks = loadMarks(root);
|
|
2192
|
-
const { sections, whatHappens, anchors, drawing } = state ? personDiagrams(state, names) : { sections: null, whatHappens: null, anchors: /* @__PURE__ */ new Map(), drawing: null };
|
|
2193
|
-
const codeAt = new Map([...anchors].map(([code, anchor]) => [anchor, code]));
|
|
2194
|
-
const said = /* @__PURE__ */ new Map();
|
|
2195
|
-
const part = (anchor, words) => {
|
|
2196
|
-
said.set(codeAt.get(anchor), words);
|
|
2197
|
-
return `[${codeAt.get(anchor)}]`;
|
|
2198
|
-
};
|
|
2199
|
-
const out = [`The studio page${pageUrl ? ` (${pageUrl})` : ""}, as the person sees it now. Words in quotes are yours or the person's; the rest is computed. [ids] are what yichus_point takes.`];
|
|
2200
|
-
const heading = (key) => out.push("", `## ${PAGE_HEADINGS[key]}`);
|
|
2201
|
-
out.push("", "## What the person has in view");
|
|
2202
|
-
const inView = out.length;
|
|
2203
|
-
heading("asked");
|
|
2204
|
-
out.push(description ? `"${description}"` : "Nothing yet.");
|
|
2205
|
-
heading("now");
|
|
2206
|
-
out.push(state ? state.step.summary : "No checks have run yet.");
|
|
2207
|
-
const savedOf = (id) => answers.find((a) => a.question === id);
|
|
2208
|
-
const open = (state?.properties ?? []).filter((p) => p.status === "unanswered" && !savedOf(p.question.id));
|
|
2209
|
-
if (open.length) {
|
|
2210
|
-
heading("questions");
|
|
2211
|
-
for (const { question: q } of open) out.push(`question ${q.id}: ${q.ask}`, ...q.choices.map((c) => ` - ${c.label}: ${c.means}`));
|
|
2212
|
-
}
|
|
2213
|
-
heading("props");
|
|
2214
|
-
const chosen = (state?.properties ?? []).filter((p) => p.answer || savedOf(p.question.id));
|
|
2215
|
-
if (!chosen.length) out.push("No properties chosen yet.");
|
|
2216
|
-
for (const p of chosen) {
|
|
2217
|
-
const saved = savedOf(p.question.id);
|
|
2218
|
-
const stale = saved && (!p.answer || p.answer.answeredAt !== saved.answeredAt);
|
|
2219
|
-
const answer = stale ? saved : p.answer;
|
|
2220
|
-
const label2 = p.question.choices.find((c) => c.id === answer.choice)?.label ?? answer.choice;
|
|
2221
|
-
out.push(`question ${p.question.id}: ${p.question.ask} Status: ${PAGE_STATUS[stale ? "pending" : p.status][0]}. They chose ${label2}${answer.words && answer.words !== label2 ? ` ("${answer.words}")` : ""}.`);
|
|
2222
|
-
}
|
|
2223
|
-
heading("happens");
|
|
2224
|
-
const step = (s, pad) => {
|
|
2225
|
-
if (s.kind === "outcome") {
|
|
2226
|
-
const end = `${s.refused ? "turned away" : "answered"}${s.words ? ` "${s.words}"` : ""}`;
|
|
2227
|
-
return [`${pad}${part(s.anchor, end)} ${end}: ${ENDING[s.status]}`, ...s.checks.map((c) => `${pad} ${TICK[c.verdict]} ${say(c.says)}`)];
|
|
2228
|
-
}
|
|
2229
|
-
const asks = quoted(s.words, "a check not described yet");
|
|
2230
|
-
return [`${pad}${part(s.anchor, asks)} checks ${asks}`, ...s.ways.flatMap((w) => {
|
|
2231
|
-
const words = quoted(w.words, "a way not described yet");
|
|
2232
|
-
return [`${pad} way ${words}${w.anchor === w.then.anchor ? "" : ` ${part(w.anchor, words)}`}:`, ...step(w.then, `${pad} `)];
|
|
2233
|
-
})];
|
|
2234
|
-
};
|
|
2235
|
-
if (!whatHappens) out.push(state?.whatHappensError ? `It could not be drawn: ${state.whatHappensError}` : "This appears once the program compiles.");
|
|
2236
|
-
else {
|
|
2237
|
-
if (!whatHappens.actions.length && !whatHappens.waiting) out.push("The app has no actions yet.");
|
|
2238
|
-
for (const a of whatHappens.actions) {
|
|
2239
|
-
const title2 = quoted(a.title, "an action not described yet");
|
|
2240
|
-
out.push(`${part(a.anchor, title2)} ${title2}`, ...a.first ? step(a.first, " ") : [" This action could not be followed."]);
|
|
2241
|
-
}
|
|
2242
|
-
if (whatHappens.waiting) out.push(`${whatHappens.waiting} more ${whatHappens.waiting === 1 ? "action is" : "actions are"} not described yet.`);
|
|
2243
|
-
}
|
|
2244
|
-
heading("sections");
|
|
2245
|
-
const records = (verb, names2) => names2.length ? [`${verb} ${names2.map((n) => quoted(n, "a kind of record not named yet")).join(", ")}`] : [];
|
|
2246
|
-
const does = (s) => [...records("changes", s.changes), ...records("looks at", s.looks)].join(" \xB7 ") || "keeps no records itself";
|
|
2247
|
-
if (!sections) out.push("This appears once the program compiles.");
|
|
2248
|
-
else sections.forEach((layer, i) => {
|
|
2249
|
-
out.push(`layer ${i + 1} of ${sections.length}${i === 0 ? ", the top" : ""}:`);
|
|
2250
|
-
for (const s of layer) {
|
|
2251
|
-
const name = quoted(s.name, "a part not named yet");
|
|
2252
|
-
out.push(` ${part(s.anchor, name)} ${name}: ${does(s)}`);
|
|
2253
|
-
}
|
|
2254
|
-
});
|
|
2255
|
-
heading("map");
|
|
2256
|
-
if (state?.map) {
|
|
2257
|
-
const parts = nameableParts(state.map);
|
|
2258
|
-
const unnamed = parts.filter((p) => !(p.code in names)).length;
|
|
2259
|
-
out.push(`The system map of the program, drawn with the person's names for ${parts.length - unnamed} of its ${parts.length} parts${unnamed === 1 ? "; the other one shows its code name" : unnamed ? `; the other ${unnamed} show their code names` : ""}. api_abstraction_map gives the map itself.`);
|
|
2260
|
-
} else out.push(state?.mapError ? `The map could not be drawn: ${state.mapError}` : "The map appears once the program compiles.");
|
|
2261
|
-
out.push("", "## Your notes on the page");
|
|
2262
|
-
const report = loadViewing(root);
|
|
2263
|
-
const viewing = report && { ...report, current: report.drawing === drawing };
|
|
2264
|
-
const visible = new Set(!viewing?.current ? [] : viewing.parts.flatMap((a) => codeAt.has(a) ? [codeAt.get(a)] : []));
|
|
2265
|
-
const notes = marks?.marks.filter((m) => anchors.has(m.at)) ?? [];
|
|
2266
|
-
if (!notes.length) out.push("None. yichus_point puts a note over a part of the page.");
|
|
2267
|
-
else out.push(`Said ${ago(marks.said, now)}:`, ...notes.map((m) => ` [${m.at}] "${m.note}"${visible.has(m.at) ? " (on screen)" : ""}`));
|
|
2268
|
-
out.splice(inView, 0, ...inViewLines(viewing, codeAt, said, now));
|
|
2269
|
-
return `${out.join("\n")}
|
|
2270
|
-
`;
|
|
2271
|
-
}
|
|
2272
|
-
function inViewLines(viewing, codeAt, said, now) {
|
|
2273
|
-
if (!viewing) return ["The page has not said what is on screen: it may not be open. Give the person the link yichus_start returned."];
|
|
2274
|
-
const parts = (anchors) => !viewing.current ? [] : anchors.flatMap((a) => codeAt.has(a) ? [`[${codeAt.get(a)}] ${said.get(codeAt.get(a))}`] : []);
|
|
2275
|
-
const list = (what, items) => items.length ? [`${what}:`, ...items.map((i) => ` ${i}`)] : [];
|
|
2276
|
-
const { touched } = viewing;
|
|
2277
|
-
const clicked = !touched ? [] : touched.kind === "section" ? [`Last clicked: in ${PAGE_HEADINGS[touched.id]}.`] : touched.kind === "question" ? [`Last clicked: question ${touched.id}.`] : touched.kind === "property" ? [`Last clicked: the property of question ${touched.id}.`] : parts([touched.id]).map((p) => `Last clicked: ${p}`);
|
|
2278
|
-
return [
|
|
2279
|
-
`The page said this ${ago(viewing.at, now)}. ${viewing.shown ? "Its tab is in front." : "Its tab is behind another one."}`,
|
|
2280
|
-
`On screen: ${viewing.sections.map((s) => PAGE_HEADINGS[s]).join(", ") || "nothing of the page"}.`,
|
|
2281
|
-
...viewing.current ? [] : ["The pictures have changed since, so which of their parts are in view is not known until the page redraws and says again."],
|
|
2282
|
-
...list("Parts on screen", parts(viewing.parts)),
|
|
2283
|
-
...list("Questions on screen", viewing.questions.map((q) => `question ${q}`)),
|
|
2284
|
-
...list("Properties on screen", viewing.properties.map((q) => `question ${q}`)),
|
|
2285
|
-
...list("Folds the person opened", parts(viewing.open)),
|
|
2286
|
-
...clicked
|
|
2287
|
-
];
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
// src/studio/reference.ts
|
|
2291
|
-
import fs6 from "fs";
|
|
2292
|
-
import path6 from "path";
|
|
2293
|
-
import { fileURLToPath } from "url";
|
|
2294
|
-
|
|
2295
|
-
// src/studio/reference.json
|
|
2296
|
-
var reference_default = {
|
|
2297
|
-
"protocol-case": [
|
|
2298
|
-
{ name: "protocol-specification.txt", from: "docs/protocol-verification.md" },
|
|
2299
|
-
{ name: "withdrawal-case.json", from: "eval/corpus/webmcp-traps/iteration-003/programs/Q15/twin/case.json" },
|
|
2300
|
-
{ name: "withdrawal.bosatsu", from: "eval/corpus/webmcp-traps/iteration-003/programs/Q15/twin/ledger.bosatsu" }
|
|
2301
|
-
],
|
|
2302
|
-
"portable-programs": [
|
|
2303
|
-
{ name: "portable-programs.md", from: "docs/studio/portable-programs.md" },
|
|
2304
|
-
{ name: "refund-policy.bosatsu", from: "demos/portable/refund-policy.bosatsu" },
|
|
2305
|
-
{ name: "refund-desk.bosatsu", from: "demos/portable/refund-desk.bosatsu" },
|
|
2306
|
-
{ name: "refund-test.bosatsu", from: "demos/portable/refund-test.bosatsu" },
|
|
2307
|
-
{ name: "refund-desk-one-round.bosatsu", from: "demos/portable/limits/refund-desk-one-round.bosatsu" }
|
|
2308
|
-
],
|
|
2309
|
-
deploy: [
|
|
2310
|
-
{ name: "deploy-platforms.md", from: "docs/studio/deploy-platforms.md" }
|
|
2311
|
-
],
|
|
2312
|
-
guiding: [
|
|
2313
|
-
{ name: "the-page.md", from: "docs/studio/the-page.md" }
|
|
2314
|
-
],
|
|
2315
|
-
porting: [
|
|
2316
|
-
{ name: "porting.md", from: "docs/studio/porting.md" },
|
|
2317
|
-
{ name: "worker-authority.md", from: "docs/runtime/worker-authority.md" }
|
|
2318
|
-
]
|
|
2319
|
-
};
|
|
2320
|
-
|
|
2321
|
-
// src/studio/reference.ts
|
|
2322
|
-
var TOPICS = Object.keys(reference_default);
|
|
2323
|
-
var BUILT_REFERENCE = fileURLToPath(new URL("./studio-reference/", import.meta.url));
|
|
2324
|
-
var REPO_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
2325
|
-
var builtCopy = (file) => path6.join(BUILT_REFERENCE, file.name);
|
|
2326
|
-
function readReference(topic, locate) {
|
|
2327
|
-
if (!TOPICS.includes(topic)) return null;
|
|
2328
|
-
return reference_default[topic].map((file) => `===== ${file.name} =====
|
|
2329
|
-
${fs6.readFileSync(locate(file), "utf8")}`).join("\n\n");
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
// src/studio/text.ts
|
|
2333
|
-
var STATUS_WORDS = {
|
|
2334
|
-
unanswered: "not answered yet",
|
|
2335
|
-
decided: "decided",
|
|
2336
|
-
assumed: "holds under an assumption",
|
|
2337
|
-
proven: "proven",
|
|
2338
|
-
bounded: "holds in every scenario checked",
|
|
2339
|
-
broken: "broken",
|
|
2340
|
-
mismatch: "declared differently from the answer",
|
|
2341
|
-
"needs-case": "needs a failure scenario",
|
|
2342
|
-
inconclusive: "not decided by the check",
|
|
2343
|
-
"no-check": "no check needed"
|
|
2344
|
-
};
|
|
2345
|
-
function stepText(state, pageUrl) {
|
|
2346
|
-
const { step } = state;
|
|
2347
|
-
const lines = [`Stage: ${step.stage}. ${step.summary}`, "", "Do this:", ...step.instructions.map((s, i) => `${i + 1}. ${s}`)];
|
|
2348
|
-
for (const q of step.ask ?? []) {
|
|
2349
|
-
lines.push("", `Question ${q.id}: ${q.ask}`, ` Why it matters: ${q.why}`);
|
|
2350
|
-
for (const c of q.choices) lines.push(` - ${c.id}: ${c.label}. ${c.means}`);
|
|
2351
|
-
}
|
|
2352
|
-
if (step.evidence?.length) lines.push("", "Evidence (engine output):", ...step.evidence.map((e) => ` ${e}`));
|
|
2353
|
-
const answered = state.properties.filter((p) => p.status !== "unanswered");
|
|
2354
|
-
if (answered.length) {
|
|
2355
|
-
lines.push("", "Properties:");
|
|
2356
|
-
for (const p of answered) {
|
|
2357
|
-
const choice = p.question.choices.find((c) => c.id === p.answer?.choice)?.label ?? p.answer?.choice;
|
|
2358
|
-
lines.push(` ${p.question.id} (${choice}): ${STATUS_WORDS[p.status]}`);
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
if (pageUrl) lines.push("", `Studio page: ${pageUrl}`);
|
|
2362
|
-
return `${lines.join("\n")}
|
|
2363
|
-
`;
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
// src/studio/mcp.ts
|
|
2367
|
-
var INSTRUCTIONS = `Yichus studio: you are building software for a person who will not read the code. Yichus tells you what to do next and proves the properties the person chooses.
|
|
2368
|
-
|
|
2369
|
-
The loop:
|
|
2370
|
-
1. Ask the person what they want built and call yichus_start with their words. If it replaces an app they already have, read yichus_reference topic "porting" first: reading the old app is yours, and the studio sees only the new one.
|
|
2371
|
-
2. After every change to the code, call yichus_next and do exactly what it says. It reads the .bosatsu files in the project folder itself; you never paste sources.
|
|
2372
|
-
3. When yichus_next lists questions, put each one to the person with its choices and record their answer with yichus_answer. Never answer for them or pick a default: who may see data, what happens when two people act at once, and what happens when a server or an outside service fails are their product decisions.
|
|
2373
|
-
4. Never weaken a rule, delete a check, or declare a single server to make a check pass. Change the code.
|
|
2374
|
-
5. Give the person the studio page link from yichus_start. It shows the shape of their program, what happens in each thing it does, and whether each property they chose holds. The person reads no documentation: everything they need to know about the steps, the questions, the statuses and the pictures comes from you, a little at a time as it becomes relevant. Read yichus_reference topic "guiding" before you give them the link.
|
|
2375
|
-
6. Speak to the person in product terms. They never need the programming language, code names, file names, tool names or question ids; when yichus_next asks you to name parts of the program or describe what happens, use the words they would use.
|
|
2376
|
-
7. Build for the place the person chose to run the app (yichus_reference topic "deploy"). Keep every business decision in pure code: work that takes several steps or waits on outside services is one pure step program that a rule-free engine drives (yichus_reference topic "portable-programs").
|
|
2377
|
-
8. While you talk with the person, point at what you mean on the studio page with yichus_point: a section of their app, an action, or a step in one, with a sentence or two as you would say it in the chat. The page shows each note over the pictures as yours, not checked, so never use a note to say something holds; the checks say that. Point again as the conversation moves on, and clear the marks when they no longer help.
|
|
2378
|
-
9. You drive the conversation, and the person reads the studio page while you talk. Call yichus_page to see what the page shows them, in words: what they asked for, the questions, each property with its status, what happens in each action, the sections of their app, your notes, and what they have on screen and last clicked. Call it whenever they refer to something on the page ("this one", "the red one", "what does that mean"), and after they answer on the page.
|
|
2379
|
-
|
|
2380
|
-
Every api_* tool is available too. Leave out sources and the studio supplies the project's files. Pages on yich.us, such as calculators and the playground, come through a separate connection (@yichus/mcp); building needs only this one.`;
|
|
2381
|
-
var START = {
|
|
2382
|
-
name: "yichus_start",
|
|
2383
|
-
description: "Record what the person wants built, in their own words, and get the studio page link to give them. Call this first.",
|
|
2384
|
-
inputSchema: { type: "object", properties: { description: { type: "string", description: "The person's description of the software, unchanged." } }, required: ["description"] }
|
|
2385
|
-
};
|
|
2386
|
-
var NEXT = {
|
|
2387
|
-
name: "yichus_next",
|
|
2388
|
-
description: "Read the program from the project folder, run the checks, and return the one next step: questions to ask the person, code to fix, a declaration to add, or done. Call after every change.",
|
|
2389
|
-
inputSchema: { type: "object", properties: {} }
|
|
2390
|
-
};
|
|
2391
|
-
var ANSWER = {
|
|
2392
|
-
name: "yichus_answer",
|
|
2393
|
-
description: "Record the person's answer to a question yichus_next listed: the question id, the id of the choice they picked, and their own words.",
|
|
2394
|
-
inputSchema: { type: "object", properties: {
|
|
2395
|
-
question: { type: "string", description: "The question id from yichus_next." },
|
|
2396
|
-
choice: { type: "string", description: "The id of the choice the person picked." },
|
|
2397
|
-
words: { type: "string", description: "What the person said, in their words." }
|
|
2398
|
-
}, required: ["question", "choice", "words"] }
|
|
2399
|
-
};
|
|
2400
|
-
var NAME = {
|
|
2401
|
-
name: "yichus_name",
|
|
2402
|
-
description: "Give the parts of the program yichus_next listed (kinds of record, actions, pieces of the program) the short plain names the person would use. The person sees these names instead of the code names, in questions and in the picture.",
|
|
2403
|
-
inputSchema: { type: "object", properties: {
|
|
2404
|
-
names: {
|
|
2405
|
-
type: "object",
|
|
2406
|
-
additionalProperties: { type: "string" },
|
|
2407
|
-
description: `Each code name yichus_next listed, such as a table name or a route path, with a few words in the person's vocabulary, such as "family lists" or "starting a new list".`
|
|
2408
|
-
}
|
|
2409
|
-
}, required: ["names"] }
|
|
2410
|
-
};
|
|
2411
|
-
var SCENARIO = {
|
|
2412
|
-
name: "yichus_scenario",
|
|
2413
|
-
description: "Record what should happen in actions yichus_next listed, in the person's words: a title, names for the ids the engine computed, and claims the engine checks on each outcome. A scenario replaces any earlier one for the same action. drop removes the scenario of an action the app no longer has.",
|
|
2414
|
-
inputSchema: { type: "object", properties: {
|
|
2415
|
-
scenarios: { type: "array", items: { type: "object", properties: {
|
|
2416
|
-
entry: { type: "string", description: "The action, as yichus_next listed it (route:/path)." },
|
|
2417
|
-
title: { type: "string", description: "What the person does, in their words." },
|
|
2418
|
-
names: { type: "object", additionalProperties: { type: "string" }, description: "Each id yichus_next listed under the action, with the person's words for it." },
|
|
2419
|
-
expect: {
|
|
2420
|
-
type: "array",
|
|
2421
|
-
items: { type: "object", properties: { at: { type: "string" }, claim: { type: "string" } }, required: ["at", "claim"] },
|
|
2422
|
-
description: "What must and must not happen: [not] reads TABLE, writes TABLE, refused, returns CONSTRUCTOR, reaches model or reaches net, at an id."
|
|
2423
|
-
}
|
|
2424
|
-
}, required: ["entry", "title", "names", "expect"] } },
|
|
2425
|
-
drop: { type: "array", items: { type: "string" }, description: "Actions yichus_next reports the app no longer has, whose scenarios to remove." }
|
|
2426
|
-
} }
|
|
2427
|
-
};
|
|
2428
|
-
var POINT = {
|
|
2429
|
-
name: "yichus_point",
|
|
2430
|
-
description: "Point at parts of the studio page while you talk with the person: each mark puts your short note over the picture, next to a section of their app, an action, or a step in one, marked as your words and not checked. A call replaces the marks before it; an empty list clears them.",
|
|
2431
|
-
inputSchema: { type: "object", properties: {
|
|
2432
|
-
marks: { type: "array", items: { type: "object", properties: {
|
|
2433
|
-
at: { type: "string", description: "What to point at: a section by its code name as yichus_name took it, an action yichus_scenario described (route:/path), or an id yichus_next listed under a described action." },
|
|
2434
|
-
note: { type: "string", description: "What you are saying about it, in the person's words, at most 280 characters." }
|
|
2435
|
-
}, required: ["at", "note"] } }
|
|
2436
|
-
}, required: ["marks"] }
|
|
2437
|
-
};
|
|
2438
|
-
var PAGE = {
|
|
2439
|
-
name: "yichus_page",
|
|
2440
|
-
description: "See the studio page as the person sees it now, in words, with the id yichus_point takes for each part: the questions, each property and its status, what happens in each action, the sections of their app and your notes. Also what the person has on screen, the folds they opened and what they last clicked. Call it when the person refers to something on the page.",
|
|
2441
|
-
inputSchema: { type: "object", properties: {} }
|
|
2442
|
-
};
|
|
2443
|
-
var REFERENCE_TOOL = {
|
|
2444
|
-
name: "yichus_reference",
|
|
2445
|
-
description: "Reference material yichus_next asks for, such as the protocol case specification with a complete worked case.",
|
|
2446
|
-
inputSchema: { type: "object", properties: { topic: { type: "string", enum: TOPICS, description: "The topic yichus_next named." } }, required: ["topic"] }
|
|
2447
|
-
};
|
|
2448
|
-
var StartArgs = z10.object({ description: z10.string().min(1) }).strict();
|
|
2449
|
-
var AnswerArgs = z10.object({ question: z10.string().min(1), choice: z10.string().min(1), words: z10.string() }).strict();
|
|
2450
|
-
var ScenarioInput = z10.object({
|
|
2451
|
-
entry: z10.string().min(1),
|
|
2452
|
-
title: z10.string().trim().min(1),
|
|
2453
|
-
names: z10.record(z10.string().min(1), z10.string().trim().min(1)),
|
|
2454
|
-
expect: z10.array(z10.object({ at: z10.string().min(1), claim: z10.string().trim().min(1) }).strict())
|
|
2455
|
-
}).strict();
|
|
2456
|
-
var ScenarioArgs = z10.object({ scenarios: z10.array(ScenarioInput).default([]), drop: z10.array(z10.string().min(1)).default([]) }).refine((a) => a.scenarios.length + a.drop.length > 0, "give at least one scenario or one action to drop");
|
|
2457
|
-
var NameArgs = z10.object({ names: z10.record(z10.string().min(1), z10.string().trim().min(1)).refine((names) => Object.keys(names).length > 0) });
|
|
2458
|
-
var text = (body, structured, isError = false) => ({ content: [{ type: "text", text: body }], ...structured ? { structuredContent: structured } : {}, isError });
|
|
2459
|
-
function studioTool(tool) {
|
|
2460
|
-
const schema = tool.inputSchema;
|
|
2461
|
-
if (!schema.properties || !("sources" in schema.properties)) return tool;
|
|
2462
|
-
return {
|
|
2463
|
-
...tool,
|
|
2464
|
-
description: [tool.description, "In the studio, leave out sources to check every .bosatsu file in the project folder."].filter(Boolean).join(" "),
|
|
2465
|
-
inputSchema: { ...schema, required: (schema.required ?? []).filter((name) => name !== "sources") }
|
|
2466
|
-
};
|
|
2467
|
-
}
|
|
2468
|
-
function recordStart(root, args) {
|
|
2469
|
-
const parsed = StartArgs.safeParse(args);
|
|
2470
|
-
if (!parsed.success) return { ok: false, error: `yichus_start needs the person's description, and nothing else. ${z10.prettifyError(parsed.error)}` };
|
|
2471
|
-
saveRequirements(root, { ...loadRequirements(root), description: parsed.data.description });
|
|
2472
|
-
return { ok: true };
|
|
2473
|
-
}
|
|
2474
|
-
function recordAnswer(root, args, now) {
|
|
2475
|
-
const parsed = AnswerArgs.safeParse(args);
|
|
2476
|
-
if (!parsed.success) return { ok: false, error: `yichus_answer needs question, choice and words, and nothing else. ${z10.prettifyError(parsed.error)}` };
|
|
2477
|
-
const state = readState(root);
|
|
2478
|
-
const property = state?.properties.find((p) => p.question.id === parsed.data.question);
|
|
2479
|
-
if (!property) return { ok: false, error: `No question ${parsed.data.question} in the latest yichus_next; call yichus_next and use its ids.` };
|
|
2480
|
-
if (!property.question.choices.some((c) => c.id === parsed.data.choice)) {
|
|
2481
|
-
return { ok: false, error: `${parsed.data.choice} is not a choice of ${parsed.data.question}; the choices are ${property.question.choices.map((c) => c.id).join(", ")}.` };
|
|
2482
|
-
}
|
|
2483
|
-
saveRequirements(root, withAnswer(loadRequirements(root), { ...parsed.data, answeredAt: now.toISOString() }));
|
|
2484
|
-
return { ok: true };
|
|
2485
|
-
}
|
|
2486
|
-
function recordNames(root, args) {
|
|
2487
|
-
const parsed = NameArgs.safeParse(args);
|
|
2488
|
-
if (!parsed.success) return { ok: false, error: "yichus_name needs names: each code name yichus_next listed, with its plain name." };
|
|
2489
|
-
const state = readState(root);
|
|
2490
|
-
const parts = /* @__PURE__ */ new Set([...state?.properties.flatMap((p) => p.question.subject ?? []) ?? [], ...state?.map ? nameableParts(state.map).map((p) => p.code) : []]);
|
|
2491
|
-
const unknown = Object.keys(parsed.data.names).filter((code) => !parts.has(code));
|
|
2492
|
-
if (unknown.length) {
|
|
2493
|
-
const listed = [...parts].slice(0, 20).join(", ") + (parts.size > 20 ? `, and ${parts.size - 20} more` : "");
|
|
2494
|
-
return { ok: false, error: `No part named ${unknown.join(", ")} in the latest yichus_next; the parts are ${listed || "none yet"}.` };
|
|
2495
|
-
}
|
|
2496
|
-
saveRequirements(root, withNames(loadRequirements(root), parsed.data.names));
|
|
2497
|
-
return { ok: true };
|
|
2498
|
-
}
|
|
2499
|
-
function recordScenarios(root, args) {
|
|
2500
|
-
const parsed = ScenarioArgs.safeParse(args);
|
|
2501
|
-
if (!parsed.success) return { ok: false, error: `yichus_scenario needs scenarios, each with entry, title, names and expect, or drop. ${z10.prettifyError(parsed.error)}` };
|
|
2502
|
-
const happens = readState(root)?.whatHappens;
|
|
2503
|
-
const entries = new Set(happens?.entries.map((e) => e.entry) ?? []);
|
|
2504
|
-
const unknown = parsed.data.scenarios.map((s) => s.entry).filter((e) => !entries.has(e));
|
|
2505
|
-
if (unknown.length) return { ok: false, error: `No action ${unknown.join(", ")} in the latest yichus_next; the actions are ${[...entries].join(", ") || "none yet"}.` };
|
|
2506
|
-
const gone = new Set(happens?.mismatches.map((m) => m.entry));
|
|
2507
|
-
const kept = parsed.data.drop.filter((e) => !gone.has(e));
|
|
2508
|
-
if (kept.length) return { ok: false, error: `The latest yichus_next does not report ${kept.join(", ")} as gone, so its scenario stays. Only an action the app no longer has can be dropped; replace the scenario of one it still has.` };
|
|
2509
|
-
saveRequirements(root, withScenarios(loadRequirements(root), parsed.data.scenarios, parsed.data.drop));
|
|
2510
|
-
return { ok: true };
|
|
2511
|
-
}
|
|
2512
|
-
function createStudioMcpServer({ root, engine, catalog, pageUrl, now = () => /* @__PURE__ */ new Date(), locate = builtCopy }) {
|
|
2513
|
-
const server = new Server({ name: "yichus-studio", version: "0.3.0" }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
|
|
2514
|
-
const active = /* @__PURE__ */ new Set();
|
|
2515
|
-
const track = (task) => {
|
|
2516
|
-
active.add(task);
|
|
2517
|
-
void task.finally(() => active.delete(task)).catch(() => {
|
|
2518
|
-
});
|
|
2519
|
-
return task;
|
|
2520
|
-
};
|
|
2521
|
-
let tools = null;
|
|
2522
|
-
const catalogTools2 = async () => tools ??= (await catalog()).map(studioTool);
|
|
2523
|
-
let last = readState(root);
|
|
2524
|
-
server.setRequestHandler(ListToolsRequestSchema, () => track((async () => ({ tools: [START, NEXT, NAME, ANSWER, SCENARIO, POINT, PAGE, REFERENCE_TOOL, ...await catalogTools2()] }))()));
|
|
2525
|
-
server.setRequestHandler(CallToolRequestSchema, (request) => track(callTool(request.params)));
|
|
2526
|
-
async function callTool(params) {
|
|
2527
|
-
const { name, arguments: args = {} } = params;
|
|
2528
|
-
if (name === "yichus_start") {
|
|
2529
|
-
const result2 = recordStart(root, args);
|
|
2530
|
-
if (!result2.ok) return text(result2.error, void 0, true);
|
|
2531
|
-
const page = pageUrl ? `Read yichus_reference topic "guiding", then give the person this link and ask them to keep it open: ${pageUrl}` : "No studio page is running.";
|
|
2532
|
-
return text(`${page}
|
|
2533
|
-
Next: call yichus_next.`, { pageUrl });
|
|
2534
|
-
}
|
|
2535
|
-
if (name === "yichus_next") {
|
|
2536
|
-
last = await direct({ root, engine, requirements: loadRequirements(root), previous: last });
|
|
2537
|
-
const { map: _map, layers: _layers, whatHappens: _whatHappens, ...rest } = last;
|
|
2538
|
-
return text(stepText(last, pageUrl), rest);
|
|
2539
|
-
}
|
|
2540
|
-
if (name === "yichus_answer") {
|
|
2541
|
-
const result2 = recordAnswer(root, args, now());
|
|
2542
|
-
return result2.ok ? text("Recorded. Ask any remaining questions, then call yichus_next.") : text(result2.error, void 0, true);
|
|
2543
|
-
}
|
|
2544
|
-
if (name === "yichus_name") {
|
|
2545
|
-
const result2 = recordNames(root, args);
|
|
2546
|
-
return result2.ok ? text("Recorded. Call yichus_next.") : text(result2.error, void 0, true);
|
|
2547
|
-
}
|
|
2548
|
-
if (name === "yichus_scenario") {
|
|
2549
|
-
const result2 = recordScenarios(root, args);
|
|
2550
|
-
return result2.ok ? text("Recorded. Call yichus_next: the engine checks every name and claim.") : text(result2.error, void 0, true);
|
|
2551
|
-
}
|
|
2552
|
-
if (name === "yichus_point") {
|
|
2553
|
-
const result2 = recordMarks(root, args, now());
|
|
2554
|
-
return result2.ok ? text("The page shows your notes now. Go on talking with the person.") : text(result2.error, void 0, true);
|
|
2555
|
-
}
|
|
2556
|
-
if (name === "yichus_page") {
|
|
2557
|
-
if (Object.keys(args).length) return text("yichus_page takes no arguments.", void 0, true);
|
|
2558
|
-
return text(pageText(root, pageUrl, now()));
|
|
2559
|
-
}
|
|
2560
|
-
if (name === "yichus_reference") {
|
|
2561
|
-
const said = readReference(String(args.topic), locate);
|
|
2562
|
-
return said ? text(said) : text(`No reference topic ${String(args.topic)}; the topics are ${TOPICS.join(", ")}.`, void 0, true);
|
|
2563
|
-
}
|
|
2564
|
-
const tool = (await catalogTools2()).find((t) => t.name === name);
|
|
2565
|
-
if (!tool) return text(`Unknown tool ${name}.`, void 0, true);
|
|
2566
|
-
const filled = { ...args };
|
|
2567
|
-
const props = tool.inputSchema.properties ?? {};
|
|
2568
|
-
if ("sources" in props && filled.sources === void 0) {
|
|
2569
|
-
filled.sources = name === "api_protocol_check" ? [...projectSources(root), ...caseSources(root)] : projectSources(root);
|
|
2570
|
-
}
|
|
2571
|
-
const topology2 = loadRequirements(root).answers.find((a) => a.question === "topology")?.choice;
|
|
2572
|
-
if ("instances" in props && filled.instances === void 0 && topology2) filled.instances = topology2 === "one" ? "1" : "many";
|
|
2573
|
-
const result = await engine.call(name, filled);
|
|
2574
|
-
if (name === "api_guide" && filled.section === void 0 && Array.isArray(result.sections)) {
|
|
2575
|
-
const titles = {
|
|
2576
|
-
ok: true,
|
|
2577
|
-
tool: "api_guide",
|
|
2578
|
-
titles: result.sections.map((s) => s.title),
|
|
2579
|
-
note: "The whole guide is long. Call api_guide with section set to one of these titles, or to part of one."
|
|
2580
|
-
};
|
|
2581
|
-
return text(JSON.stringify(titles), titles);
|
|
2582
|
-
}
|
|
2583
|
-
return text(JSON.stringify(result), result, result.ok !== true);
|
|
2584
|
-
}
|
|
2585
|
-
return {
|
|
2586
|
-
server,
|
|
2587
|
-
async idle() {
|
|
2588
|
-
while (active.size) await Promise.allSettled(active);
|
|
2589
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
2590
|
-
}
|
|
2591
|
-
};
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
// src/studio/command.ts
|
|
2595
|
-
var STUDIO_COMMANDS = /* @__PURE__ */ new Set(["start", "next", "name", "answer", "scenario", "point", "page", "reference", "studio"]);
|
|
2596
|
-
var STUDIO_HELP = `
|
|
2597
|
-
Studio (guided building; all take --project DIR, default the current folder):
|
|
2598
|
-
yichus studio [--port N] MCP server on stdio plus the studio page
|
|
2599
|
-
yichus studio --page [--port N] only the studio page, for the commands below
|
|
2600
|
-
yichus start "DESCRIPTION" record what the person wants built
|
|
2601
|
-
yichus next [--json] check the program and print the next step
|
|
2602
|
-
yichus name SUBJECT "NAME" ... plain names the person sees for parts of the program
|
|
2603
|
-
yichus answer QUESTION CHOICE [--words TEXT]
|
|
2604
|
-
yichus scenario FILE.json what should happen in actions: {"scenarios": [...], "drop": [...]}
|
|
2605
|
-
yichus point FILE.json point at parts of the page: {"marks": [{"at": ..., "note": ...}]}
|
|
2606
|
-
yichus page the page as the person sees it now, in words
|
|
2607
|
-
yichus reference TOPIC reference material a step asks for
|
|
2608
|
-
`;
|
|
2609
|
-
async function runStudioCommand(argv2, io) {
|
|
2610
|
-
try {
|
|
2611
|
-
const { values, positionals } = parseArgs({ args: argv2, allowPositionals: true, strict: true, options: {
|
|
2612
|
-
project: { type: "string" },
|
|
2613
|
-
json: { type: "boolean" },
|
|
2614
|
-
words: { type: "string" },
|
|
2615
|
-
page: { type: "boolean" },
|
|
2616
|
-
port: { type: "string" }
|
|
2617
|
-
} });
|
|
2618
|
-
const [command, ...rest] = positionals;
|
|
2619
|
-
const root = path7.resolve(values.project ?? ".");
|
|
2620
|
-
const expect = (n, usage) => {
|
|
2621
|
-
if (rest.length !== n) throw new Error(`Usage: ${usage}`);
|
|
2622
|
-
};
|
|
2623
|
-
if (command === "start") {
|
|
2624
|
-
expect(1, 'yichus start "DESCRIPTION"');
|
|
2625
|
-
const result = recordStart(root, { description: rest[0] });
|
|
2626
|
-
if (!result.ok) throw new Error(result.error);
|
|
2627
|
-
io.stderr("Recorded the description. Next: yichus next\n");
|
|
2628
|
-
return 0;
|
|
2629
|
-
}
|
|
2630
|
-
if (command === "name") {
|
|
2631
|
-
if (!rest.length || rest.length % 2) throw new Error('Usage: yichus name SUBJECT "NAME" [SUBJECT "NAME" ...]');
|
|
2632
|
-
const names = Object.fromEntries(rest.flatMap((s, i) => i % 2 ? [] : [[s, rest[i + 1]]]));
|
|
2633
|
-
const result = recordNames(root, { names });
|
|
2634
|
-
if (!result.ok) throw new Error(result.error);
|
|
2635
|
-
io.stderr(`Named ${Object.entries(names).map(([subject, name]) => `${subject} "${name}"`).join(", ")}. Next: yichus next
|
|
2636
|
-
`);
|
|
2637
|
-
return 0;
|
|
2638
|
-
}
|
|
2639
|
-
if (command === "answer") {
|
|
2640
|
-
expect(2, "yichus answer QUESTION CHOICE [--words TEXT]");
|
|
2641
|
-
const [question, choice] = rest;
|
|
2642
|
-
const result = recordAnswer(root, { question, choice, words: values.words ?? "" }, io.now());
|
|
2643
|
-
if (!result.ok) throw new Error(result.error);
|
|
2644
|
-
io.stderr(`Recorded ${choice} for ${question}. Next: yichus next
|
|
2645
|
-
`);
|
|
2646
|
-
return 0;
|
|
2647
|
-
}
|
|
2648
|
-
if (command === "scenario") {
|
|
2649
|
-
expect(1, "yichus scenario FILE.json");
|
|
2650
|
-
const result = recordScenarios(root, JSON.parse(fs7.readFileSync(path7.resolve(rest[0]), "utf8")));
|
|
2651
|
-
if (!result.ok) throw new Error(result.error);
|
|
2652
|
-
io.stderr("Recorded the scenarios. Next: yichus next\n");
|
|
2653
|
-
return 0;
|
|
2654
|
-
}
|
|
2655
|
-
if (command === "point") {
|
|
2656
|
-
expect(1, "yichus point FILE.json");
|
|
2657
|
-
const result = recordMarks(root, JSON.parse(fs7.readFileSync(path7.resolve(rest[0]), "utf8")), io.now());
|
|
2658
|
-
if (!result.ok) throw new Error(result.error);
|
|
2659
|
-
io.stderr("The page shows the notes now.\n");
|
|
2660
|
-
return 0;
|
|
2661
|
-
}
|
|
2662
|
-
if (command === "page") {
|
|
2663
|
-
expect(0, "yichus page");
|
|
2664
|
-
io.stdout(pageText(root, null, io.now()));
|
|
2665
|
-
return 0;
|
|
2666
|
-
}
|
|
2667
|
-
if (command === "reference") {
|
|
2668
|
-
expect(1, "yichus reference TOPIC");
|
|
2669
|
-
const said = readReference(rest[0], io.locate);
|
|
2670
|
-
if (!said) throw new Error(`No reference topic ${rest[0]}`);
|
|
2671
|
-
io.stdout(`${said}
|
|
2672
|
-
`);
|
|
2673
|
-
return 0;
|
|
2674
|
-
}
|
|
2675
|
-
if (command === "next") {
|
|
2676
|
-
expect(0, "yichus next [--json]");
|
|
2677
|
-
const state = await direct({ root, engine: await io.engine(), requirements: loadRequirements(root), previous: readState(root) });
|
|
2678
|
-
io.stdout(values.json ? `${JSON.stringify(state, null, 2)}
|
|
2679
|
-
` : stepText(state, null));
|
|
2680
|
-
return 0;
|
|
2681
|
-
}
|
|
2682
|
-
expect(0, "yichus studio [--page] [--port N]");
|
|
2683
|
-
const port = values.port === void 0 ? null : Number(values.port);
|
|
2684
|
-
if (port !== null && (!Number.isInteger(port) || port < 0 || port > 65535)) throw new Error("--port must be a port number");
|
|
2685
|
-
await io.serve({ root, port, pageOnly: values.page ?? false });
|
|
2686
|
-
return 0;
|
|
2687
|
-
} catch (error) {
|
|
2688
|
-
io.stderr(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
2689
|
-
`);
|
|
2690
|
-
return 2;
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
// src/run.ts
|
|
2695
|
-
var HELP = `yichus \u2014 the Yichus WebMCP analysis engine, locally
|
|
2696
|
-
|
|
2697
|
-
yichus tools
|
|
2698
|
-
yichus call TOOL [--args JSON_FILE|-] [--source FILE ...]
|
|
2699
|
-
yichus verify FILE ... [--instances 1|many] [--block-on-warning ID ...]
|
|
2700
|
-
yichus lean FILE ... --lean-wasm-root DIRECTORY [--timeout-ms N]
|
|
2701
|
-
${STUDIO_HELP}
|
|
2702
|
-
Options:
|
|
2703
|
-
--source FILE Add a Bosatsu source (repeatable; call only). Files
|
|
2704
|
-
sharing a basename are named by their folders.
|
|
2705
|
-
--args FILE|- Tool arguments as a JSON object; - reads stdin.
|
|
2706
|
-
--block-on-warning ID A property id whose warning blocks the verdict, as a
|
|
2707
|
-
violation would (repeatable; all names every property;
|
|
2708
|
-
verify only). An id the verdict lacks exits 2.
|
|
2709
|
-
--output FILE Write the result to this file instead of stdout.
|
|
2710
|
-
--select /POINTER Extract a JSON field; strings are written verbatim.
|
|
2711
|
-
--expect /PTR=JSON Fail unless a result field exactly equals JSON (repeatable).
|
|
2712
|
-
--bundle FILE Explicit Scala.js engine override.
|
|
2713
|
-
--lean-wasm-root DIR Pinned local Lean Wasm installation (lean only).
|
|
2714
|
-
--timeout-ms N Total proof budget, at most 300000 (lean only).
|
|
2715
|
-
--help, -h Show this help without loading the engine.
|
|
2716
|
-
|
|
2717
|
-
tools and call expose Yichus/Mcp::catalog without a second analysis layer.
|
|
2718
|
-
verify runs api_verify and requires verdict.proven to be exactly true.
|
|
2719
|
-
Its JSON opens with proven, the verdict; ok only says the check ran.
|
|
2720
|
-
It always prints a plain summary of the verdict to stderr: each property's
|
|
2721
|
-
status, the first failure's evidence (parse errors carry file:line:col),
|
|
2722
|
-
and where the full JSON verdict went.
|
|
2723
|
-
lean runs the pinned Wasm kernel and requires all discovered obligations to be verified.
|
|
2724
|
-
It prints on stderr whether each law was proven and, if not, why: Lean
|
|
2725
|
-
rejected it (with Lean's words), the law is outside what Lean Wasm admits,
|
|
2726
|
-
the --timeout-ms budget ran out, or no runtime is installed (with the
|
|
2727
|
-
install command).
|
|
2728
|
-
call requires ok:true; use --expect to gate on a check's actual verdict.
|
|
2729
|
-
When the tool fails, stderr gives its error as plain text, what to do
|
|
2730
|
-
instead when the engine names the failure's kind (errorKinds), and where
|
|
2731
|
-
the JSON result went. An unmet --expect is one JSON line on stderr.
|
|
2732
|
-
With --output it says on stderr where the result went; api_abstraction_map
|
|
2733
|
-
also says what it is mapping before it starts, and afterwards its layer,
|
|
2734
|
-
reuse and copy counts in words, each with the JSON field that lists them.
|
|
2735
|
-
No browser is needed. Page editing, visible feedback and app sessions use
|
|
2736
|
-
the separate @yichus/mcp browser relay.
|
|
2737
|
-
|
|
2738
|
-
Exit status: 0 = completed and expectations met; 1 = tool/check rejected;
|
|
2739
|
-
2 = invalid invocation (including a --block-on-warning id the verdict lacks), malformed engine result, or execution/IO failure
|
|
2740
|
-
(for lean: no runtime, the budget ran out, or the runtime failed).
|
|
2741
|
-
JSON diagnostics remain available for rejected checks: the result JSON,
|
|
2742
|
-
or, when --select withheld it, a JSON line on stderr. --select emits no
|
|
2743
|
-
artifact on rejection. A named output is removed before execution so a
|
|
2744
|
-
failed check cannot leave yesterday's artifact behind.
|
|
2745
|
-
`;
|
|
2746
|
-
function pointerParts(pointer) {
|
|
2747
|
-
if (pointer === "") return [];
|
|
2748
|
-
if (!pointer.startsWith("/") || /~(?:[^01]|$)/u.test(pointer)) {
|
|
2749
|
-
throw new Error(`Invalid JSON Pointer: ${pointer}`);
|
|
2750
|
-
}
|
|
2751
|
-
return pointer.slice(1).split("/").map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
2752
|
-
}
|
|
2753
|
-
function readPointer(value, pointer) {
|
|
2754
|
-
for (const part of pointerParts(pointer)) {
|
|
2755
|
-
if (value === null || typeof value !== "object" || !Object.hasOwn(value, part)) return void 0;
|
|
2756
|
-
value = value[part];
|
|
2757
|
-
}
|
|
2758
|
-
return value;
|
|
2759
|
-
}
|
|
2760
|
-
function objectValue(value, label2) {
|
|
2761
|
-
const parsed = z11.record(z11.string(), z11.unknown()).safeParse(value);
|
|
2762
|
-
if (!parsed.success) throw new Error(`${label2} must be a JSON object`);
|
|
2763
|
-
return parsed.data;
|
|
2764
|
-
}
|
|
2765
|
-
var objectJson = (raw, label2) => objectValue(JSON.parse(raw), label2);
|
|
2766
|
-
var FLAGS = {
|
|
2767
|
-
source: { type: "string", multiple: true },
|
|
2768
|
-
args: { type: "string" },
|
|
2769
|
-
instances: { type: "string" },
|
|
2770
|
-
"block-on-warning": { type: "string", multiple: true },
|
|
2771
|
-
output: { type: "string" },
|
|
2772
|
-
bundle: { type: "string" },
|
|
2773
|
-
"lean-wasm-root": { type: "string" },
|
|
2774
|
-
"timeout-ms": { type: "string" },
|
|
2775
|
-
select: { type: "string" },
|
|
2776
|
-
expect: { type: "string", multiple: true }
|
|
2777
|
-
};
|
|
2778
|
-
var TimeoutMs = z11.coerce.number().int().min(1).max(3e5);
|
|
2779
|
-
function parse(argv2) {
|
|
2780
|
-
const { values, positionals, tokens } = parseArgs2({ args: argv2, options: FLAGS, allowPositionals: true, strict: true, tokens: true });
|
|
2781
|
-
const named2 = tokens.flatMap((token) => token.kind === "option" ? [token.name] : []);
|
|
2782
|
-
const repeated = named2.find((name, i) => !("multiple" in FLAGS[name]) && named2.indexOf(name) !== i);
|
|
2783
|
-
if (repeated) throw new Error(`Repeated --${repeated}`);
|
|
2784
|
-
const [command, ...rest] = positionals;
|
|
2785
|
-
if (command !== "tools" && command !== "call" && command !== "verify" && command !== "lean") {
|
|
2786
|
-
throw new Error("Expected tools, call TOOL, verify FILE, or lean FILE; use --help for usage.");
|
|
2787
|
-
}
|
|
2788
|
-
const opts = { command, sources: [], expects: [], blockOnWarnings: values["block-on-warning"] ?? [] };
|
|
2789
|
-
if (command === "call") {
|
|
2790
|
-
opts.tool = rest.shift();
|
|
2791
|
-
if (!opts.tool) throw new Error("call requires a tool name");
|
|
2792
|
-
}
|
|
2793
|
-
if ((command === "tools" || command === "call") && rest.length) throw new Error(`Unexpected argument: ${rest[0]}`);
|
|
2794
|
-
opts.sources = [...rest, ...values.source ?? []];
|
|
2795
|
-
opts.args = values.args;
|
|
2796
|
-
opts.instances = values.instances;
|
|
2797
|
-
opts.output = values.output;
|
|
2798
|
-
opts.bundle = values.bundle;
|
|
2799
|
-
opts.leanWasmRoot = values["lean-wasm-root"];
|
|
2800
|
-
if (values["timeout-ms"] !== void 0) {
|
|
2801
|
-
const timeout = TimeoutMs.safeParse(values["timeout-ms"]);
|
|
2802
|
-
if (!timeout.success) throw new Error("--timeout-ms must be an integer from 1 to 300000");
|
|
2803
|
-
opts.timeoutMs = timeout.data;
|
|
2804
|
-
}
|
|
2805
|
-
if (values.select !== void 0) {
|
|
2806
|
-
pointerParts(values.select);
|
|
2807
|
-
opts.select = values.select;
|
|
2808
|
-
}
|
|
2809
|
-
for (const value of values.expect ?? []) {
|
|
2810
|
-
const equal = value.indexOf("=");
|
|
2811
|
-
if (equal < 0) throw new Error("--expect requires /POINTER=JSON");
|
|
2812
|
-
const pointer = value.slice(0, equal);
|
|
2813
|
-
pointerParts(pointer);
|
|
2814
|
-
opts.expects.push({ pointer, value: JSON.parse(value.slice(equal + 1)) });
|
|
2815
|
-
}
|
|
2816
|
-
if (command === "tools" && (opts.sources.length || opts.args || opts.instances)) {
|
|
2817
|
-
throw new Error("tools does not accept sources or tool arguments");
|
|
2818
|
-
}
|
|
2819
|
-
if (command === "verify") {
|
|
2820
|
-
if (!opts.sources.length) throw new Error("verify requires at least one Bosatsu file");
|
|
2821
|
-
if (opts.args) throw new Error("verify accepts --instances; use call for arbitrary arguments");
|
|
2822
|
-
if (values.source !== void 0) throw new Error("verify takes positional files; --source is for call");
|
|
2823
|
-
if (opts.instances && opts.instances !== "1" && opts.instances !== "many") {
|
|
2824
|
-
throw new Error("--instances must be 1 or many");
|
|
2825
|
-
}
|
|
2826
|
-
opts.expects.push({ pointer: "/verdict/proven", value: true });
|
|
2827
|
-
} else if (command === "lean") {
|
|
2828
|
-
if (!opts.sources.length) throw new Error("lean requires at least one Bosatsu file");
|
|
2829
|
-
if (!opts.leanWasmRoot) throw new Error("lean requires --lean-wasm-root");
|
|
2830
|
-
if (opts.args || opts.instances || values.source !== void 0)
|
|
2831
|
-
throw new Error("lean takes positional files and a Wasm root");
|
|
2832
|
-
opts.expects.push({ pointer: "/allVerified", value: true });
|
|
2833
|
-
} else if (opts.instances) {
|
|
2834
|
-
throw new Error("--instances is for verify; pass catalog arguments through --args for call");
|
|
2835
|
-
}
|
|
2836
|
-
if (command !== "verify" && opts.blockOnWarnings.length)
|
|
2837
|
-
throw new Error("--block-on-warning is for verify; pass block_on_warnings through --args for call");
|
|
2838
|
-
if (command !== "lean" && (opts.leanWasmRoot || opts.timeoutMs !== void 0))
|
|
2839
|
-
throw new Error("--lean-wasm-root and --timeout-ms are for lean");
|
|
2840
|
-
return opts;
|
|
2841
|
-
}
|
|
2842
|
-
function closestSibling(file) {
|
|
2843
|
-
const dir = path8.dirname(file);
|
|
2844
|
-
const want = path8.basename(file);
|
|
2845
|
-
let names;
|
|
2846
|
-
try {
|
|
2847
|
-
names = fs8.readdirSync(dir);
|
|
2848
|
-
} catch {
|
|
2849
|
-
return void 0;
|
|
2850
|
-
}
|
|
2851
|
-
const distance = (a, b) => {
|
|
2852
|
-
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
2853
|
-
for (let i = 1; i <= a.length; i++) {
|
|
2854
|
-
let diagonal = row[0];
|
|
2855
|
-
row[0] = i;
|
|
2856
|
-
for (let j = 1; j <= b.length; j++) {
|
|
2857
|
-
const above = row[j];
|
|
2858
|
-
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
2859
|
-
diagonal = above;
|
|
2860
|
-
}
|
|
2861
|
-
}
|
|
2862
|
-
return row[b.length];
|
|
2863
|
-
};
|
|
2864
|
-
const scored = names.map((name) => ({ name, d: distance(want, name) })).filter(({ d }) => d <= 2).sort((x, y) => x.d - y.d);
|
|
2865
|
-
return scored.length ? path8.join(dir, scored[0].name) : void 0;
|
|
2866
|
-
}
|
|
2867
|
-
function readInput(file) {
|
|
2868
|
-
try {
|
|
2869
|
-
return fs8.readFileSync(file, "utf8");
|
|
2870
|
-
} catch (error) {
|
|
2871
|
-
const code = error?.code;
|
|
2872
|
-
if (code === "ENOENT") {
|
|
2873
|
-
const near = closestSibling(file);
|
|
2874
|
-
throw new Error(`File not found: ${file}${near ? `. Did you mean ${near}?` : ""}`);
|
|
2875
|
-
}
|
|
2876
|
-
if (code === "EISDIR") throw new Error(`${file} is a directory; name a file instead.`);
|
|
2877
|
-
if (code === "EACCES" || code === "EPERM") throw new Error(`Permission denied reading ${file}.`);
|
|
2878
|
-
throw error;
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
function sourceNames(paths) {
|
|
2882
|
-
const parts = paths.map((p) => path8.resolve(p).split(path8.sep).filter((s) => s !== ""));
|
|
2883
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2884
|
-
parts.forEach((segments, i) => {
|
|
2885
|
-
const full = segments.join("/");
|
|
2886
|
-
if (seen.has(full)) throw new Error(`${paths[i]} names the same file as an earlier --source; name each source once`);
|
|
2887
|
-
seen.add(full);
|
|
2888
|
-
});
|
|
2889
|
-
const tail = (segments, n) => segments.slice(-n).join("/");
|
|
2890
|
-
return parts.map((segments, i) => {
|
|
2891
|
-
let n = 1;
|
|
2892
|
-
while (parts.some((other, j) => j !== i && tail(other, n) === tail(segments, n))) n += 1;
|
|
2893
|
-
return tail(segments, n);
|
|
2894
|
-
});
|
|
2895
|
-
}
|
|
2896
|
-
async function argumentsFor(opts, io) {
|
|
2897
|
-
const args = opts.args ? objectJson(opts.args === "-" ? await io.readStdin() : readInput(opts.args), "Arguments") : {};
|
|
2898
|
-
if (opts.sources.length) {
|
|
2899
|
-
if (Object.hasOwn(args, "sources")) throw new Error("Use either --source files or args.sources, not both");
|
|
2900
|
-
const names = sourceNames(opts.sources);
|
|
2901
|
-
args.sources = opts.sources.map((sourcePath, i) => ({ fileName: names[i], source: readInput(sourcePath) }));
|
|
2902
|
-
}
|
|
2903
|
-
if (opts.instances) args.instances = opts.instances;
|
|
2904
|
-
if (opts.blockOnWarnings.length) args.block_on_warnings = [...new Set(opts.blockOnWarnings)];
|
|
2905
|
-
return args;
|
|
2906
|
-
}
|
|
2907
|
-
function clearOutput(opts) {
|
|
2908
|
-
if (!opts.output) return;
|
|
2909
|
-
const output = path8.resolve(opts.output);
|
|
2910
|
-
const outputStat = fs8.existsSync(output) ? fs8.statSync(output) : void 0;
|
|
2911
|
-
for (const input of [...opts.sources, opts.args, opts.bundle].filter((p) => !!p && p !== "-")) {
|
|
2912
|
-
const inputStat = fs8.existsSync(input) ? fs8.statSync(input) : void 0;
|
|
2913
|
-
if (path8.resolve(input) === output || outputStat && inputStat && inputStat.dev === outputStat.dev && inputStat.ino === outputStat.ino) {
|
|
2914
|
-
throw new Error("--output must not overwrite an input file");
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
fs8.rmSync(output, { force: true });
|
|
2918
|
-
}
|
|
2919
|
-
function rejection(result, unmet) {
|
|
2920
|
-
if (result.ok) return `${unmet} --expect ${unmet === 1 ? "expectation was" : "expectations were"} not met`;
|
|
2921
|
-
const said = EngineError.safeParse(result);
|
|
2922
|
-
return said.success ? said.data.error : "The tool failed without saying why; see its result";
|
|
2923
|
-
}
|
|
2924
|
-
async function runCli(argv2, io) {
|
|
2925
|
-
try {
|
|
2926
|
-
if (argv2.length === 0 || argv2.includes("--help") || argv2.includes("-h")) {
|
|
2927
|
-
io.stdout(HELP);
|
|
2928
|
-
return 0;
|
|
2929
|
-
}
|
|
2930
|
-
const opts = parse(argv2);
|
|
2931
|
-
clearOutput(opts);
|
|
2932
|
-
const args = await argumentsFor(opts, io);
|
|
2933
|
-
let result;
|
|
2934
|
-
const timeoutMs = opts.timeoutMs ?? 18e4;
|
|
2935
|
-
if (opts.command === "lean") {
|
|
2936
|
-
if (!io.checkLeanWasm) throw new Error("Lean Wasm backend is unavailable");
|
|
2937
|
-
io.stderr(`Checking the laws in ${opts.sources.join(", ")} with Lean Wasm at ${opts.leanWasmRoot} (budget ${timeoutMs} ms)...
|
|
2938
|
-
`);
|
|
2939
|
-
result = objectValue(await io.checkLeanWasm(
|
|
2940
|
-
opts.bundle,
|
|
2941
|
-
opts.leanWasmRoot,
|
|
2942
|
-
args.sources,
|
|
2943
|
-
timeoutMs
|
|
2944
|
-
), "Lean result");
|
|
2945
|
-
} else {
|
|
2946
|
-
if (opts.command === "call") {
|
|
2947
|
-
const files = Array.isArray(args.sources) ? args.sources.length : void 0;
|
|
2948
|
-
const progress = callProgress(opts.tool, files);
|
|
2949
|
-
if (progress) io.stderr(progress);
|
|
2950
|
-
}
|
|
2951
|
-
const engine = await io.loadEngine(opts.bundle);
|
|
2952
|
-
const raw = opts.command === "tools" ? engine.listTools() : engine.callTool(opts.command === "verify" ? "api_verify" : opts.tool, JSON.stringify(args));
|
|
2953
|
-
result = objectJson(raw, "Engine result");
|
|
2954
|
-
}
|
|
2955
|
-
if (typeof result.ok !== "boolean") throw new Error("Engine result must explicitly contain boolean ok");
|
|
2956
|
-
const unmet = opts.expects.filter(({ pointer, value }) => !isDeepStrictEqual(readPointer(result, pointer), value));
|
|
2957
|
-
const passed = result.ok && unmet.length === 0;
|
|
2958
|
-
if (opts.select === void 0 || passed) {
|
|
2959
|
-
const selected = opts.select === void 0 ? result : readPointer(result, opts.select);
|
|
2960
|
-
if (selected === void 0) throw new Error(`Result field missing: ${opts.select}`);
|
|
2961
|
-
const text3 = opts.select !== void 0 && typeof selected === "string" ? selected : `${JSON.stringify(selected, null, 2)}
|
|
2962
|
-
`;
|
|
2963
|
-
if (opts.output) fs8.writeFileSync(opts.output, text3);
|
|
2964
|
-
else io.stdout(text3);
|
|
2965
|
-
}
|
|
2966
|
-
const artifact = opts.select !== void 0 ? "the unselected result; run without --select to see it" : opts.output ?? "the JSON on standard output";
|
|
2967
|
-
let exit = passed ? 0 : 1;
|
|
2968
|
-
if (opts.command === "verify") {
|
|
2969
|
-
io.stderr(verifySummary(result, passed, artifact));
|
|
2970
|
-
if (!result.ok && result.errorKind === "invalid-arguments") exit = 2;
|
|
2971
|
-
} else if (opts.command === "lean") {
|
|
2972
|
-
const reading = leanSummary(result, artifact, timeoutMs, opts.sources);
|
|
2973
|
-
io.stderr(reading.text);
|
|
2974
|
-
exit = reading.exit;
|
|
2975
|
-
} else if (!result.ok) {
|
|
2976
|
-
io.stderr(callFailure(opts.tool ?? "tools", result, artifact));
|
|
2977
|
-
} else if (passed && opts.select === void 0 && opts.command === "call") {
|
|
2978
|
-
const said = callSummary(opts.tool, result, opts.output);
|
|
2979
|
-
if (said) io.stderr(said);
|
|
2980
|
-
}
|
|
2981
|
-
const generic = opts.command === "call" || opts.command === "tools";
|
|
2982
|
-
if (!passed && (opts.select !== void 0 || generic && result.ok)) {
|
|
2983
|
-
io.stderr(`${JSON.stringify({ ok: false, error: rejection(result, unmet.length), unmet, result: opts.select === void 0 ? void 0 : result })}
|
|
2984
|
-
`);
|
|
2985
|
-
}
|
|
2986
|
-
return exit;
|
|
2987
|
-
} catch (error) {
|
|
2988
|
-
io.stderr(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
2989
|
-
`);
|
|
2990
|
-
return 2;
|
|
2991
|
-
}
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
// src/studio/serve.ts
|
|
2995
|
-
import { once } from "events";
|
|
2996
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2997
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2998
|
-
import { EngineWorkerClient, catalogTools } from "@yichus/api-mcp";
|
|
2999
|
-
|
|
3000
|
-
// src/studio/page-server.ts
|
|
3001
|
-
import fs9 from "fs";
|
|
3002
|
-
import http from "http";
|
|
3003
|
-
import path9 from "path";
|
|
3004
|
-
|
|
3005
|
-
// src/studio/page-assets.json
|
|
3006
|
-
var page_assets_default = ["system-map.js", "label-fit.js", "system-map-record.js", "system-map.css", "dom.js", "layer-groups.js", "layer-diagram.js", "source-link.js", "what-happens.js", "what-happens-record.js", "studio-views.js"];
|
|
3007
|
-
|
|
3008
|
-
// src/studio/page-server.ts
|
|
3009
|
-
var PAGE_ASSETS = page_assets_default;
|
|
3010
|
-
function personView(state, names, marks = null) {
|
|
3011
|
-
if (!state) return null;
|
|
3012
|
-
const { sections, whatHappens, anchors, drawing } = personDiagrams(state, names);
|
|
3013
|
-
return {
|
|
3014
|
-
project: state.project,
|
|
3015
|
-
updatedAt: state.updatedAt,
|
|
3016
|
-
sourcesHash: state.sourcesHash,
|
|
3017
|
-
summary: state.step.summary,
|
|
3018
|
-
properties: state.properties.map((p) => ({
|
|
3019
|
-
question: { id: p.question.id, ask: p.question.ask, why: p.question.why, choices: p.question.choices },
|
|
3020
|
-
answer: p.answer,
|
|
3021
|
-
status: p.status
|
|
3022
|
-
})),
|
|
3023
|
-
map: state.map ? relabelSystemMap(state.map, names) : null,
|
|
3024
|
-
mapError: state.mapError,
|
|
3025
|
-
sections,
|
|
3026
|
-
whatHappens,
|
|
3027
|
-
// Sent back with what the person has in view, so a report about an
|
|
3028
|
-
// older drawing is not read against this one.
|
|
3029
|
-
drawing,
|
|
3030
|
-
// A mark on a part the page no longer draws is left out.
|
|
3031
|
-
marks: marks && { said: marks.said, marks: marks.marks.flatMap((m) => anchors.has(m.at) ? [{ anchor: anchors.get(m.at), note: m.note }] : []) }
|
|
3032
|
-
};
|
|
3033
|
-
}
|
|
3034
|
-
function stateBody(root) {
|
|
3035
|
-
const { description, answers, names } = loadRequirements(root);
|
|
3036
|
-
return { state: personView(readState(root), names ?? {}, loadMarks(root)), requirements: { description, answers } };
|
|
3037
|
-
}
|
|
3038
|
-
var STUDIO_PORTS = [4317, 4318, 4319, 4320, 4321, 0];
|
|
3039
|
-
async function startPageServer(root, assetsDir, port = null, now = () => /* @__PURE__ */ new Date()) {
|
|
3040
|
-
ensureStudioDir(root);
|
|
3041
|
-
const clients = /* @__PURE__ */ new Set();
|
|
3042
|
-
const watched = ["state.json", "requirements.json", "marks.json"].map((name) => path9.join(studioDir(root), name));
|
|
3043
|
-
const changed = (now2, before) => {
|
|
3044
|
-
if (now2.mtimeMs !== before.mtimeMs || now2.size !== before.size) for (const res of clients) res.write("event: change\ndata: {}\n\n");
|
|
3045
|
-
};
|
|
3046
|
-
for (const file of watched) fs9.watchFile(file, { interval: 250 }, changed);
|
|
3047
|
-
const hosts = /* @__PURE__ */ new Set();
|
|
3048
|
-
const takes = /* @__PURE__ */ new Map([
|
|
3049
|
-
["/answer", (args) => recordAnswer(root, args, now())],
|
|
3050
|
-
["/viewing", (args) => recordViewing(root, args, now())]
|
|
3051
|
-
]);
|
|
3052
|
-
const server = http.createServer((req, res) => {
|
|
3053
|
-
const send = (status, type, body) => {
|
|
3054
|
-
res.writeHead(status, { "content-type": type, "cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
3055
|
-
res.end(body);
|
|
3056
|
-
};
|
|
3057
|
-
const host = req.headers.host ?? "";
|
|
3058
|
-
if (!hosts.has(host) || req.headers.origin && req.headers.origin !== `http://${host}`) {
|
|
3059
|
-
send(403, "text/plain", "This studio only answers its own page.");
|
|
3060
|
-
return;
|
|
3061
|
-
}
|
|
3062
|
-
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
3063
|
-
if (req.method === "GET" && url.pathname === "/") return send(200, "text/html; charset=utf-8", STUDIO_PAGE);
|
|
3064
|
-
if (req.method === "GET" && url.pathname === "/state") {
|
|
3065
|
-
return send(200, "application/json", JSON.stringify(stateBody(root)));
|
|
3066
|
-
}
|
|
3067
|
-
if (req.method === "GET" && url.pathname === "/events") {
|
|
3068
|
-
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
|
|
3069
|
-
res.write(": connected\n\n");
|
|
3070
|
-
clients.add(res);
|
|
3071
|
-
req.on("close", () => clients.delete(res));
|
|
3072
|
-
return;
|
|
3073
|
-
}
|
|
3074
|
-
const asset = PAGE_ASSETS.find((a) => url.pathname === `/${a}`);
|
|
3075
|
-
if (req.method === "GET" && asset) {
|
|
3076
|
-
return send(200, asset.endsWith(".css") ? "text/css" : "text/javascript", fs9.readFileSync(path9.join(assetsDir, asset), "utf8"));
|
|
3077
|
-
}
|
|
3078
|
-
const take = req.method === "POST" ? takes.get(url.pathname) : void 0;
|
|
3079
|
-
if (take) {
|
|
3080
|
-
if (req.headers["content-type"] !== "application/json") return send(415, "text/plain", "Send JSON.");
|
|
3081
|
-
let body = "";
|
|
3082
|
-
req.setEncoding("utf8");
|
|
3083
|
-
req.on("data", (chunk) => {
|
|
3084
|
-
body += chunk;
|
|
3085
|
-
if (body.length > 65536) req.destroy();
|
|
3086
|
-
});
|
|
3087
|
-
req.on("end", () => {
|
|
3088
|
-
let args;
|
|
3089
|
-
try {
|
|
3090
|
-
args = JSON.parse(body);
|
|
3091
|
-
} catch {
|
|
3092
|
-
return send(400, "application/json", JSON.stringify({ ok: false, error: "The request is not JSON." }));
|
|
3093
|
-
}
|
|
3094
|
-
const result = take(args);
|
|
3095
|
-
send(result.ok ? 200 : 400, "application/json", JSON.stringify(result));
|
|
3096
|
-
});
|
|
3097
|
-
return;
|
|
3098
|
-
}
|
|
3099
|
-
send(404, "text/plain", "Not found.");
|
|
3100
|
-
});
|
|
3101
|
-
for (const candidate of port === null ? STUDIO_PORTS : [port]) {
|
|
3102
|
-
const error = await new Promise((resolve) => {
|
|
3103
|
-
server.once("error", resolve);
|
|
3104
|
-
server.listen(candidate, "127.0.0.1", () => {
|
|
3105
|
-
server.off("error", resolve);
|
|
3106
|
-
resolve(null);
|
|
3107
|
-
});
|
|
3108
|
-
});
|
|
3109
|
-
if (!error) break;
|
|
3110
|
-
if (error.code !== "EADDRINUSE" || candidate === port) throw error;
|
|
3111
|
-
}
|
|
3112
|
-
const bound = server.address().port;
|
|
3113
|
-
hosts.add(`127.0.0.1:${bound}`).add(`localhost:${bound}`);
|
|
3114
|
-
return {
|
|
3115
|
-
url: `http://127.0.0.1:${bound}/`,
|
|
3116
|
-
async close() {
|
|
3117
|
-
for (const file of watched) fs9.unwatchFile(file, changed);
|
|
3118
|
-
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
3119
|
-
server.closeAllConnections();
|
|
3120
|
-
await closed;
|
|
3121
|
-
}
|
|
3122
|
-
};
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
// src/studio/serve.ts
|
|
3126
|
-
var objectResult = (raw, tool) => {
|
|
3127
|
-
const value = JSON.parse(raw);
|
|
3128
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${tool} returned no JSON object`);
|
|
3129
|
-
return value;
|
|
3130
|
-
};
|
|
3131
|
-
var inProcessEngine = (engine) => ({
|
|
3132
|
-
async call(tool, args) {
|
|
3133
|
-
return objectResult(engine.callTool(tool, JSON.stringify(args)), tool);
|
|
3134
|
-
}
|
|
3135
|
-
});
|
|
3136
|
-
function workerEngine(bundle) {
|
|
3137
|
-
const worker = new EngineWorkerClient(bundle, 9e5);
|
|
3138
|
-
let id = 0;
|
|
3139
|
-
return {
|
|
3140
|
-
async call(tool, args) {
|
|
3141
|
-
return objectResult(await worker.callTool(tool, JSON.stringify(args), `studio-${++id}`), tool);
|
|
3142
|
-
},
|
|
3143
|
-
async catalog() {
|
|
3144
|
-
return catalogTools(await worker.listTools(`studio-${++id}`));
|
|
3145
|
-
},
|
|
3146
|
-
close: () => worker.close()
|
|
3147
|
-
};
|
|
3148
|
-
}
|
|
3149
|
-
var BUILT_ASSETS = fileURLToPath2(new URL("./studio-page/", import.meta.url));
|
|
3150
|
-
async function serveStudio(options) {
|
|
3151
|
-
const page = await startPageServer(options.root, options.assetsDir ?? BUILT_ASSETS, options.port);
|
|
3152
|
-
options.stderr(`Yichus studio page: ${page.url}
|
|
17
|
+
// src/cli.ts
|
|
18
|
+
var problem = nodeVersionProblem(process.versions.node, engines.node);
|
|
19
|
+
if (problem) {
|
|
20
|
+
process.stderr.write(`${problem}
|
|
3153
21
|
`);
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
return;
|
|
3158
|
-
}
|
|
3159
|
-
const engine = workerEngine(options.bundle);
|
|
3160
|
-
const { server, idle } = createStudioMcpServer({ root: options.root, engine, pageUrl: page.url, catalog: engine.catalog });
|
|
3161
|
-
const stdin = options.stdin ?? process.stdin;
|
|
3162
|
-
const closed = once(stdin, "end");
|
|
3163
|
-
try {
|
|
3164
|
-
await server.connect(new StdioServerTransport(stdin, options.stdout ?? process.stdout));
|
|
3165
|
-
await closed;
|
|
3166
|
-
await idle();
|
|
3167
|
-
} finally {
|
|
3168
|
-
await server.close();
|
|
3169
|
-
await engine.close();
|
|
3170
|
-
await page.close();
|
|
3171
|
-
}
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
} else {
|
|
24
|
+
await import("./main.js");
|
|
3172
25
|
}
|
|
3173
|
-
|
|
3174
|
-
// src/cli.ts
|
|
3175
|
-
var bundleOrThrow = (bundle) => {
|
|
3176
|
-
const resolved = bundle ?? findApiMcpBundle();
|
|
3177
|
-
if (!resolved) throw new Error("Yichus engine bundle missing. Reinstall @yichus/api-mcp or pass --bundle.");
|
|
3178
|
-
return resolved;
|
|
3179
|
-
};
|
|
3180
|
-
var argv = process.argv.slice(2);
|
|
3181
|
-
process.exitCode = STUDIO_COMMANDS.has(argv[0] ?? "") ? await runStudioCommand(argv, {
|
|
3182
|
-
engine: async () => inProcessEngine(await loadApiEngine(bundleOrThrow())),
|
|
3183
|
-
serve: (options) => serveStudio({ ...options, bundle: bundleOrThrow(), stderr: (text3) => process.stderr.write(text3) }),
|
|
3184
|
-
stdout: (text3) => process.stdout.write(text3),
|
|
3185
|
-
stderr: (text3) => process.stderr.write(text3),
|
|
3186
|
-
now: () => /* @__PURE__ */ new Date(),
|
|
3187
|
-
locate: builtCopy
|
|
3188
|
-
}) : await runCli(argv, {
|
|
3189
|
-
async loadEngine(bundle) {
|
|
3190
|
-
return loadApiEngine(bundleOrThrow(bundle));
|
|
3191
|
-
},
|
|
3192
|
-
async checkLeanWasm(bundle, root, sources, timeoutMs) {
|
|
3193
|
-
return checkLeanWasmSources(bundleOrThrow(bundle), root, sources, timeoutMs);
|
|
3194
|
-
},
|
|
3195
|
-
// A stream, not readFileSync(0): a pipe the parent has not written yet
|
|
3196
|
-
// fails a synchronous read with EAGAIN on macOS.
|
|
3197
|
-
readStdin: () => text2(process.stdin),
|
|
3198
|
-
stdout: (text3) => process.stdout.write(text3),
|
|
3199
|
-
stderr: (text3) => process.stderr.write(text3)
|
|
3200
|
-
});
|
|
3201
26
|
//# sourceMappingURL=cli.js.map
|