yichus 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,10 +5,10 @@ import { text as text2 } from "stream/consumers";
5
5
  import { checkLeanWasmSources, findApiMcpBundle, loadApiEngine } from "@yichus/api-mcp";
6
6
 
7
7
  // src/run.ts
8
- import fs5 from "fs";
9
- import path6 from "path";
8
+ import fs8 from "fs";
9
+ import path8 from "path";
10
10
  import { isDeepStrictEqual, parseArgs as parseArgs2 } from "util";
11
- import { z as z7 } from "zod";
11
+ import { z as z11 } from "zod";
12
12
 
13
13
  // src/engine-results.ts
14
14
  import { z } from "zod";
@@ -270,16 +270,17 @@ function callFailure(tool, result, artifact) {
270
270
  }
271
271
 
272
272
  // src/studio/command.ts
273
- import path5 from "path";
273
+ import fs7 from "fs";
274
+ import path7 from "path";
274
275
  import { parseArgs } from "util";
275
276
 
276
277
  // src/studio/director.ts
277
278
  import fs3 from "fs";
278
279
  import path3 from "path";
279
- import { z as z5 } from "zod";
280
+ import { z as z6 } from "zod";
280
281
 
281
282
  // ../../web/system-map-record.js
282
- var MARKS_VERSION = "yichus-marks-2";
283
+ var MARKS_VERSION = "yichus-marks-3";
283
284
  var VOCABULARY = {
284
285
  version: MARKS_VERSION,
285
286
  fixedPlaces: {
@@ -290,6 +291,7 @@ var VOCABULARY = {
290
291
  replyBars: "one bar per entry at the same x, past the hull band <- entries[]",
291
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]",
292
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[]",
293
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[]"
294
296
  },
295
297
  marks: {
@@ -298,7 +300,8 @@ var VOCABULARY = {
298
300
  role: "red word beside the lock: a role beyond sign-in <- entries[].roles",
299
301
  limit: "gauge beside the path: a declared rate limit <- entries[].limits",
300
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",
301
- modelRing: "violet ring: the entry reaches the model; the count under the model says how many entries do <- entries[].model",
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",
302
305
  netRing: "orange ring: the entry reaches the network <- entries[].net",
303
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",
304
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[]",
@@ -306,15 +309,28 @@ var VOCABULARY = {
306
309
  token: "small grey word under a path: the value the caller fixes for the wrapper <- entries[].policy",
307
310
  afterDot: "dot in the after band: a call the entry makes after its wrapper returns <- entries[].beside.calls",
308
311
  builder: "plus-disc under a station with the call that builds the handle there <- uses[how=local].builtBy",
309
- unknown: "a ? in a circle on the rail: the engine traced the handle only on some paths <- uses[how=mixed]",
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]',
310
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[]",
311
- bundle: "one cable with a count: entries with the same wrapper and identical marks; its title names every route; opening it draws each one <- bundles[]",
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[]',
312
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",
313
- step: "loop arc with a type on the quiet line: functions of the package take that type and return it <- steps[]",
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[]',
314
317
  loop: "loop arc with package names: packages whose types lead back to themselves <- phases.cycles",
315
318
  composes: "hull round a chain: a definition takes the type the chain hands in at each step and applies it <- library.applied[].fedBy",
316
319
  port: "plug strip on the right: a record of functions; each adapter a plug, hollow when nothing plugs it in <- library.ports[]",
317
- generic: "the letter \u2200 with its variables over a part that never looks inside what it carries <- library.generic[]"
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[]'
318
334
  },
319
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."
320
336
  };
@@ -330,6 +346,110 @@ var uniqBy = (xs, key) => {
330
346
  const seen = /* @__PURE__ */ new Set();
331
347
  return xs.filter((x) => !seen.has(key(x)) && seen.add(key(x)));
332
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
+ }
333
453
  var outsideResourcesOf = (steps) => uniq((steps || []).map((e) => e.resource).filter((r) => r === "model" || r === "net"));
334
454
  function buildSystemMap(engine, { program }) {
335
455
  for (const [k, v] of Object.entries(engine)) if (!v.ok) throw new Error(`${k}: ${v.error}`);
@@ -385,6 +505,7 @@ function buildSystemMap(engine, { program }) {
385
505
  limits,
386
506
  limitComputed: ee.limits.written === "computed",
387
507
  reachKinds: ee.reaches,
508
+ db: ee.resources.includes("db"),
388
509
  model: ee.resources.includes("model"),
389
510
  net: ee.resources.includes("net"),
390
511
  writes: reach.some((e) => isWrite(e.op)),
@@ -539,6 +660,7 @@ function buildSystemMap(engine, { program }) {
539
660
  library,
540
661
  keyed,
541
662
  steps,
663
+ programs: programsOf(st, layers),
542
664
  bundles: routes.length > 12 ? bundlesOf(entries, uses, keyed) : null,
543
665
  phases: st.phases,
544
666
  evidence: {
@@ -623,6 +745,10 @@ var STRUCTURAL = /* @__PURE__ */ new Set([
623
745
  "keyKinds",
624
746
  "reachKinds",
625
747
  "resources",
748
+ "replies",
749
+ "also",
750
+ "kinds",
751
+ "within",
626
752
  "vars",
627
753
  "id"
628
754
  ]);
@@ -642,14 +768,14 @@ var VALUE = "a kind of value";
642
768
  var TYPES = /* @__PURE__ */ new Set(["type", "types", "signature"]);
643
769
  var WORD = /[A-Za-z_][\w/]*::\w+|[A-Za-z_]\w*/g;
644
770
  var leaves = (record, visit) => {
645
- const walk = (v, key) => {
771
+ const walk3 = (v, key) => {
646
772
  if (typeof v === "string") visit(v, key);
647
- else if (Array.isArray(v)) v.forEach((x) => walk(x, key));
773
+ else if (Array.isArray(v)) v.forEach((x) => walk3(x, key));
648
774
  else if (v && typeof v === "object") {
649
- for (const [k, x] of Object.entries(v)) if (!skipped(k, x)) walk(x, k);
775
+ for (const [k, x] of Object.entries(v)) if (!skipped(k, x)) walk3(x, k);
650
776
  }
651
777
  };
652
- walk(record, null);
778
+ walk3(record, null);
653
779
  };
654
780
  function nameableParts(map) {
655
781
  const found = /* @__PURE__ */ new Map();
@@ -704,13 +830,13 @@ function relabelSystemMap(map, names) {
704
830
  if (s.startsWith("/")) return s;
705
831
  return s.replace(WORD, (w) => w.includes("::") ? id(w) : STRUCTURAL.has(key) ? w : word(w));
706
832
  };
707
- const walk = (v, key) => {
833
+ const walk3 = (v, key) => {
708
834
  if (typeof v === "string") return text3(v, key);
709
- if (Array.isArray(v)) return v.map((x) => walk(x, key));
710
- if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, skipped(k, x) ? x : walk(x, k)]));
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)]));
711
837
  return v;
712
838
  };
713
- const out = walk(map, null);
839
+ const out = walk3(map, null);
714
840
  const pathOf = new Map(map.entries.map((e) => [e.key, e.path]));
715
841
  out.bundles = map.bundles && map.bundles.map((b, i) => {
716
842
  const paths = b.entries.map((k) => pathOf.get(k));
@@ -720,6 +846,259 @@ function relabelSystemMap(map, names) {
720
846
  return out;
721
847
  }
722
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
+
723
1102
  // src/studio/project.ts
724
1103
  import { createHash } from "crypto";
725
1104
  import fs from "fs";
@@ -727,17 +1106,17 @@ import path from "path";
727
1106
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "out"]);
728
1107
  function projectSources(root) {
729
1108
  const files = [];
730
- const walk = (dir) => {
1109
+ const walk3 = (dir) => {
731
1110
  for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
732
1111
  if (entry.name.startsWith(".") || SKIPPED.has(entry.name)) continue;
733
1112
  const full = path.join(dir, entry.name);
734
- if (entry.isDirectory()) walk(full);
1113
+ if (entry.isDirectory()) walk3(full);
735
1114
  else if (entry.isFile() && entry.name.endsWith(".bosatsu")) {
736
1115
  files.push({ fileName: path.relative(root, full).split(path.sep).join("/"), source: fs.readFileSync(full, "utf8") });
737
1116
  }
738
1117
  }
739
1118
  };
740
- walk(root);
1119
+ walk3(root);
741
1120
  return files;
742
1121
  }
743
1122
  function sourcesHash(files) {
@@ -773,6 +1152,32 @@ var ACCESS_POLICY = {
773
1152
  "everyone-reads": "PublicReadRoleWrite",
774
1153
  public: "Public"
775
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
+ };
776
1181
  var topology = {
777
1182
  id: "topology",
778
1183
  kind: "topology",
@@ -908,6 +1313,10 @@ function saveRequirements(root, requirements) {
908
1313
  function withNames(requirements, names) {
909
1314
  return { ...requirements, names: { ...requirements.names, ...names } };
910
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
+ }
911
1320
  function withAnswer(requirements, answer) {
912
1321
  return { ...requirements, answers: [...requirements.answers.filter((a) => a.question !== answer.question), answer] };
913
1322
  }
@@ -915,22 +1324,91 @@ function ensureStudioDir(root) {
915
1324
  const dir = studioDir(root);
916
1325
  fs2.mkdirSync(dir, { recursive: true });
917
1326
  const ignore = path2.join(dir, ".gitignore");
918
- if (!fs2.existsSync(ignore)) fs2.writeFileSync(ignore, "state.json\n");
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("")}`);
919
1332
  }
920
1333
 
921
- // src/studio/director.ts
922
- var VerifyProperty = z5.looseObject({
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"),
923
1341
  id: z5.string(),
924
- title: z5.string(),
925
- status: z5.string(),
926
- details: z5.array(z5.string()).default([]),
927
- accepted: z5.unknown().optional()
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()
928
1406
  });
929
- var Verify = z5.looseObject({
930
- topology: z5.string().optional(),
931
- verdict: z5.looseObject({ proven: z5.boolean(), properties: z5.array(VerifyProperty) })
1407
+ var Verify = z6.looseObject({
1408
+ topology: z6.string().optional(),
1409
+ verdict: z6.looseObject({ proven: z6.boolean(), properties: z6.array(VerifyProperty) })
932
1410
  });
933
- var Protocol = z5.looseObject({ protocol: z5.looseObject({ status: z5.string(), name: z5.string().optional() }).optional(), error: z5.string().optional() });
1411
+ var Protocol = z6.looseObject({ protocol: z6.looseObject({ status: z6.string(), name: z6.string().optional() }).optional(), error: z6.string().optional() });
934
1412
  var caseFile = (root, questionId) => path3.join(studioDir(root), "cases", `${questionId.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "")}.json`);
935
1413
  var answerOf = (requirements, id) => requirements.answers.find((a) => a.question === id) ?? null;
936
1414
  function instancesFor(requirements) {
@@ -949,6 +1427,8 @@ async function judge(root, engine, sources, question, answer, report, verify) {
949
1427
  if (!answer) return { ...base, status: "unanswered" };
950
1428
  const byId = (id) => verify.find((p) => p.id === id);
951
1429
  switch (question.kind) {
1430
+ case "platform":
1431
+ return { ...base, status: "decided" };
952
1432
  case "topology":
953
1433
  return {
954
1434
  ...base,
@@ -1019,24 +1499,41 @@ async function judge(root, engine, sources, question, answer, report, verify) {
1019
1499
  }
1020
1500
  var QUESTION_PROPERTIES = (id) => id.startsWith("access:") || id === "access-coverage" || id === "rmw-transactions" || id === "integrity";
1021
1501
  var GUIDE = {
1502
+ platform: "Deploy",
1022
1503
  topology: "Multi-instance data integrity",
1023
1504
  access: "Owner-scoped data",
1024
1505
  together: "Atomic commands with Yichus/Transaction",
1025
1506
  partial: "Transaction protocols: finite cases, retries, and receipts",
1026
1507
  outside: "Transaction protocols: finite cases, retries, and receipts"
1027
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
+ });
1028
1520
  function nextStep(state, compileErrors) {
1029
1521
  if (!state.description) return {
1030
1522
  stage: "describe",
1031
1523
  summary: "Waiting for a description of what to build.",
1032
1524
  instructions: ["Ask the person what they want to build, in their own words. Call yichus_start with their description, unchanged."]
1033
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];
1034
1529
  if (!state.files.length) return {
1035
1530
  stage: "write",
1036
1531
  summary: "No program yet.",
1037
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.`,
1038
1534
  'Call api_guide with section "Required workflow", then write the program as .bosatsu files in this project folder.',
1039
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.',
1040
1537
  "Then call yichus_next."
1041
1538
  ]
1042
1539
  };
@@ -1063,16 +1560,32 @@ function nextStep(state, compileErrors) {
1063
1560
  ]
1064
1561
  };
1065
1562
  const unanswered = state.properties.filter((p) => p.status === "unanswered");
1066
- if (unanswered.length) return {
1067
- stage: "ask",
1068
- summary: `${unanswered.length} question${unanswered.length === 1 ? "" : "s"} for you about how the app should behave.`,
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.",
1069
1569
  instructions: [
1070
- "Ask the person each question below, in its words, with its choices. They may also answer on the studio page.",
1071
- "Speak in product terms: never quote question ids, code names, file names, tool names or the programming language to the person.",
1072
- '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.',
1073
- "Record each answer with yichus_answer (the question id, the choice id, and their own words), then call yichus_next."
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."
1074
1583
  ],
1075
- ask: unanswered.map((p) => ({ id: p.question.id, ask: p.question.ask, why: p.question.why, choices: p.question.choices }))
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
+ ]
1076
1589
  };
1077
1590
  const mismatched = state.properties.filter((p) => p.status === "mismatch");
1078
1591
  if (mismatched.length) return {
@@ -1085,16 +1598,29 @@ function nextStep(state, compileErrors) {
1085
1598
  evidence: mismatched.flatMap((p) => p.evidence)
1086
1599
  };
1087
1600
  const broken = state.properties.filter((p) => p.status === "broken" || p.status === "inconclusive");
1088
- if (broken.length || state.other.length) return {
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 {
1089
1606
  stage: "fix",
1090
- summary: `${broken.length + state.other.length} propert${broken.length + state.other.length === 1 ? "y is" : "ies are"} not holding yet.`,
1607
+ summary: `${open} propert${open === 1 ? "y is" : "ies are"} not holding yet.`,
1091
1608
  instructions: [
1092
1609
  "Fix the program so each property below holds. The evidence lines are the engine's own words.",
1093
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]}".`),
1094
1611
  ...state.other.map((o) => `${o.title} (${o.id}) is ${o.status}.`),
1095
- "Never weaken a rule, delete a check, or declare one server to make a check pass: change the code. Then call yichus_next."
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."
1096
1616
  ],
1097
- evidence: [...broken.flatMap((p) => p.evidence), ...state.other.flatMap((o) => o.details)]
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
+ ]
1098
1624
  };
1099
1625
  const cases = state.properties.filter((p) => p.status === "needs-case");
1100
1626
  if (cases.length) return {
@@ -1114,7 +1640,8 @@ function nextStep(state, compileErrors) {
1114
1640
  instructions: [
1115
1641
  "Tell the person every property they chose holds, and point them at the studio page for the evidence.",
1116
1642
  ...assumed.length ? ["Say plainly which results depend on running exactly one server."] : [],
1117
- "When they describe a new feature, change the code and call yichus_next again: new facts can raise new questions."
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.`
1118
1645
  ]
1119
1646
  };
1120
1647
  }
@@ -1135,9 +1662,13 @@ async function direct({ root, engine, requirements, withMap = true, previous = n
1135
1662
  properties: [],
1136
1663
  other: [],
1137
1664
  map: null,
1138
- mapError: null
1665
+ mapError: null,
1666
+ layers: null,
1667
+ whatHappens: null,
1668
+ whatHappensError: null
1139
1669
  };
1140
1670
  let compileErrors = null;
1671
+ if (requirements.description) base.properties.push(await judge(root, engine, sources, platform, answerOf(requirements, platform.id), Report.parse({}), []));
1141
1672
  if (requirements.description && sources.length) {
1142
1673
  const verify = Verify.parse(await call(engine, "api_verify", { sources, ...instances ? { instances } : {} }));
1143
1674
  base.topology = verify.topology ?? null;
@@ -1146,21 +1677,30 @@ async function direct({ root, engine, requirements, withMap = true, previous = n
1146
1677
  if (compiles && compiles.status !== "proven") compileErrors = compiles.details;
1147
1678
  else {
1148
1679
  const report = Report.parse(await call(engine, "api_report", { sources }));
1149
- const structureRaw = await call(engine, "api_organize", { sources, lens: "structure" });
1680
+ const structureRaw = await call(engine, "api_organize", { sources, lens: "structure", requirements });
1150
1681
  const structure = Structure.parse(structureRaw);
1151
1682
  for (const question of questionsFor(report, structure, base.names)) {
1152
1683
  base.properties.push(await judge(root, engine, sources, question, answerOf(requirements, question.id), report, verify.verdict.properties));
1153
1684
  }
1154
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
+ }
1155
1692
  if (withMap) {
1156
- if (previous?.sourcesHash === hash && previous.map) base.map = previous.map;
1157
- else {
1693
+ if (previous?.sourcesHash === hash && previous.map) {
1694
+ base.map = previous.map;
1695
+ base.layers = previous.layers ?? null;
1696
+ } else {
1158
1697
  try {
1159
1698
  const [abstractions, layers] = [
1160
1699
  await call(engine, "api_organize", { sources, lens: "abstractions" }),
1161
1700
  await call(engine, "api_organize", { sources, lens: "layers" })
1162
1701
  ];
1163
1702
  base.map = buildSystemMap({ report, structure: structureRaw, abstractions, layers }, { program: base.project });
1703
+ base.layers = layers;
1164
1704
  } catch (error) {
1165
1705
  base.mapError = error instanceof Error ? error.message : String(error);
1166
1706
  }
@@ -1185,11 +1725,571 @@ function readState(root) {
1185
1725
  // src/studio/mcp.ts
1186
1726
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1187
1727
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
1188
- import { z as z6 } from "zod";
1728
+ import { z as z10 } from "zod";
1189
1729
 
1190
- // src/studio/reference.ts
1730
+ // src/studio/marks.ts
1191
1731
  import fs4 from "fs";
1192
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"]
1893
+ };
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
+
2045
+ function questionCard(q, saved) {
2046
+ const card = el('div', { className: 'question' });
2047
+ card.dataset.question = q.id;
2048
+ card.append(el('p', { className: 'ask', textContent: q.ask }), el('p', { className: 'why', textContent: q.why }));
2049
+ const name = 'q-' + q.id;
2050
+ for (const c of q.choices) {
2051
+ const input = el('input', { type: 'radio', name, value: c.id, checked: saved?.choice === c.id });
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;
2070
+ }
2071
+
2072
+ function propertyRow(p, saved) {
2073
+ const stale = saved && (!p.answer || p.answer.answeredAt !== saved.answeredAt);
2074
+ const [words, tone] = STATUS[stale ? 'pending' : p.status];
2075
+ const answer = stale ? saved : p.answer;
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";
1193
2293
  import { fileURLToPath } from "url";
1194
2294
 
1195
2295
  // src/studio/reference.json
@@ -1198,6 +2298,23 @@ var reference_default = {
1198
2298
  { name: "protocol-specification.txt", from: "docs/protocol-verification.md" },
1199
2299
  { name: "withdrawal-case.json", from: "eval/corpus/webmcp-traps/iteration-003/programs/Q15/twin/case.json" },
1200
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" }
1201
2318
  ]
1202
2319
  };
1203
2320
 
@@ -1205,11 +2322,11 @@ var reference_default = {
1205
2322
  var TOPICS = Object.keys(reference_default);
1206
2323
  var BUILT_REFERENCE = fileURLToPath(new URL("./studio-reference/", import.meta.url));
1207
2324
  var REPO_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
1208
- var builtCopy = (file) => path4.join(BUILT_REFERENCE, file.name);
2325
+ var builtCopy = (file) => path6.join(BUILT_REFERENCE, file.name);
1209
2326
  function readReference(topic, locate) {
1210
2327
  if (!TOPICS.includes(topic)) return null;
1211
2328
  return reference_default[topic].map((file) => `===== ${file.name} =====
1212
- ${fs4.readFileSync(locate(file), "utf8")}`).join("\n\n");
2329
+ ${fs6.readFileSync(locate(file), "utf8")}`).join("\n\n");
1213
2330
  }
1214
2331
 
1215
2332
  // src/studio/text.ts
@@ -1250,14 +2367,17 @@ function stepText(state, pageUrl) {
1250
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.
1251
2368
 
1252
2369
  The loop:
1253
- 1. Ask the person what they want built and call yichus_start with their words.
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.
1254
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.
1255
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.
1256
2373
  4. Never weaken a rule, delete a check, or declare a single server to make a check pass. Change the code.
1257
- 5. Give the person the studio page link from yichus_start. It shows the shape of their program and whether each property they chose holds.
1258
- 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, use the words they would use.
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.
1259
2379
 
1260
- Every api_* tool is available too. Leave out sources and the studio supplies the project's files.`;
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.`;
1261
2381
  var START = {
1262
2382
  name: "yichus_start",
1263
2383
  description: "Record what the person wants built, in their own words, and get the studio page link to give them. Call this first.",
@@ -1288,14 +2408,53 @@ var NAME = {
1288
2408
  }
1289
2409
  }, required: ["names"] }
1290
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
+ };
1291
2443
  var REFERENCE_TOOL = {
1292
2444
  name: "yichus_reference",
1293
2445
  description: "Reference material yichus_next asks for, such as the protocol case specification with a complete worked case.",
1294
2446
  inputSchema: { type: "object", properties: { topic: { type: "string", enum: TOPICS, description: "The topic yichus_next named." } }, required: ["topic"] }
1295
2447
  };
1296
- var StartArgs = z6.object({ description: z6.string().min(1) });
1297
- var AnswerArgs = z6.object({ question: z6.string().min(1), choice: z6.string().min(1), words: z6.string() });
1298
- var NameArgs = z6.object({ names: z6.record(z6.string().min(1), z6.string().trim().min(1)).refine((names) => Object.keys(names).length > 0) });
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) });
1299
2458
  var text = (body, structured, isError = false) => ({ content: [{ type: "text", text: body }], ...structured ? { structuredContent: structured } : {}, isError });
1300
2459
  function studioTool(tool) {
1301
2460
  const schema = tool.inputSchema;
@@ -1306,9 +2465,15 @@ function studioTool(tool) {
1306
2465
  inputSchema: { ...schema, required: (schema.required ?? []).filter((name) => name !== "sources") }
1307
2466
  };
1308
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
+ }
1309
2474
  function recordAnswer(root, args, now) {
1310
2475
  const parsed = AnswerArgs.safeParse(args);
1311
- if (!parsed.success) return { ok: false, error: "yichus_answer needs question, choice and words." };
2476
+ if (!parsed.success) return { ok: false, error: `yichus_answer needs question, choice and words, and nothing else. ${z10.prettifyError(parsed.error)}` };
1312
2477
  const state = readState(root);
1313
2478
  const property = state?.properties.find((p) => p.question.id === parsed.data.question);
1314
2479
  if (!property) return { ok: false, error: `No question ${parsed.data.question} in the latest yichus_next; call yichus_next and use its ids.` };
@@ -1331,8 +2496,21 @@ function recordNames(root, args) {
1331
2496
  saveRequirements(root, withNames(loadRequirements(root), parsed.data.names));
1332
2497
  return { ok: true };
1333
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
+ }
1334
2512
  function createStudioMcpServer({ root, engine, catalog, pageUrl, now = () => /* @__PURE__ */ new Date(), locate = builtCopy }) {
1335
- const server = new Server({ name: "yichus-studio", version: "0.2.0" }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
2513
+ const server = new Server({ name: "yichus-studio", version: "0.3.0" }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
1336
2514
  const active = /* @__PURE__ */ new Set();
1337
2515
  const track = (task) => {
1338
2516
  active.add(task);
@@ -1343,21 +2521,20 @@ function createStudioMcpServer({ root, engine, catalog, pageUrl, now = () => /*
1343
2521
  let tools = null;
1344
2522
  const catalogTools2 = async () => tools ??= (await catalog()).map(studioTool);
1345
2523
  let last = readState(root);
1346
- server.setRequestHandler(ListToolsRequestSchema, () => track((async () => ({ tools: [START, NEXT, NAME, ANSWER, REFERENCE_TOOL, ...await catalogTools2()] }))()));
2524
+ server.setRequestHandler(ListToolsRequestSchema, () => track((async () => ({ tools: [START, NEXT, NAME, ANSWER, SCENARIO, POINT, PAGE, REFERENCE_TOOL, ...await catalogTools2()] }))()));
1347
2525
  server.setRequestHandler(CallToolRequestSchema, (request) => track(callTool(request.params)));
1348
2526
  async function callTool(params) {
1349
2527
  const { name, arguments: args = {} } = params;
1350
2528
  if (name === "yichus_start") {
1351
- const parsed = StartArgs.safeParse(args);
1352
- if (!parsed.success) return text("yichus_start needs the person's description.", void 0, true);
1353
- saveRequirements(root, { ...loadRequirements(root), description: parsed.data.description });
1354
- const page = pageUrl ? `Give the person this link and ask them to keep it open: ${pageUrl}` : "No studio page is running.";
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.";
1355
2532
  return text(`${page}
1356
2533
  Next: call yichus_next.`, { pageUrl });
1357
2534
  }
1358
2535
  if (name === "yichus_next") {
1359
2536
  last = await direct({ root, engine, requirements: loadRequirements(root), previous: last });
1360
- const { map: _map, ...rest } = last;
2537
+ const { map: _map, layers: _layers, whatHappens: _whatHappens, ...rest } = last;
1361
2538
  return text(stepText(last, pageUrl), rest);
1362
2539
  }
1363
2540
  if (name === "yichus_answer") {
@@ -1368,6 +2545,18 @@ Next: call yichus_next.`, { pageUrl });
1368
2545
  const result2 = recordNames(root, args);
1369
2546
  return result2.ok ? text("Recorded. Call yichus_next.") : text(result2.error, void 0, true);
1370
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
+ }
1371
2560
  if (name === "yichus_reference") {
1372
2561
  const said = readReference(String(args.topic), locate);
1373
2562
  return said ? text(said) : text(`No reference topic ${String(args.topic)}; the topics are ${TOPICS.join(", ")}.`, void 0, true);
@@ -1403,7 +2592,7 @@ Next: call yichus_next.`, { pageUrl });
1403
2592
  }
1404
2593
 
1405
2594
  // src/studio/command.ts
1406
- var STUDIO_COMMANDS = /* @__PURE__ */ new Set(["start", "next", "name", "answer", "reference", "studio"]);
2595
+ var STUDIO_COMMANDS = /* @__PURE__ */ new Set(["start", "next", "name", "answer", "scenario", "point", "page", "reference", "studio"]);
1407
2596
  var STUDIO_HELP = `
1408
2597
  Studio (guided building; all take --project DIR, default the current folder):
1409
2598
  yichus studio [--port N] MCP server on stdio plus the studio page
@@ -1412,6 +2601,9 @@ Studio (guided building; all take --project DIR, default the current folder):
1412
2601
  yichus next [--json] check the program and print the next step
1413
2602
  yichus name SUBJECT "NAME" ... plain names the person sees for parts of the program
1414
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
1415
2607
  yichus reference TOPIC reference material a step asks for
1416
2608
  `;
1417
2609
  async function runStudioCommand(argv2, io) {
@@ -1424,13 +2616,14 @@ async function runStudioCommand(argv2, io) {
1424
2616
  port: { type: "string" }
1425
2617
  } });
1426
2618
  const [command, ...rest] = positionals;
1427
- const root = path5.resolve(values.project ?? ".");
2619
+ const root = path7.resolve(values.project ?? ".");
1428
2620
  const expect = (n, usage) => {
1429
2621
  if (rest.length !== n) throw new Error(`Usage: ${usage}`);
1430
2622
  };
1431
2623
  if (command === "start") {
1432
2624
  expect(1, 'yichus start "DESCRIPTION"');
1433
- saveRequirements(root, { ...loadRequirements(root), description: rest[0] });
2625
+ const result = recordStart(root, { description: rest[0] });
2626
+ if (!result.ok) throw new Error(result.error);
1434
2627
  io.stderr("Recorded the description. Next: yichus next\n");
1435
2628
  return 0;
1436
2629
  }
@@ -1452,6 +2645,25 @@ async function runStudioCommand(argv2, io) {
1452
2645
  `);
1453
2646
  return 0;
1454
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
+ }
1455
2667
  if (command === "reference") {
1456
2668
  expect(1, "yichus reference TOPIC");
1457
2669
  const said = readReference(rest[0], io.locate);
@@ -1546,7 +2758,7 @@ function readPointer(value, pointer) {
1546
2758
  return value;
1547
2759
  }
1548
2760
  function objectValue(value, label2) {
1549
- const parsed = z7.record(z7.string(), z7.unknown()).safeParse(value);
2761
+ const parsed = z11.record(z11.string(), z11.unknown()).safeParse(value);
1550
2762
  if (!parsed.success) throw new Error(`${label2} must be a JSON object`);
1551
2763
  return parsed.data;
1552
2764
  }
@@ -1563,11 +2775,11 @@ var FLAGS = {
1563
2775
  select: { type: "string" },
1564
2776
  expect: { type: "string", multiple: true }
1565
2777
  };
1566
- var TimeoutMs = z7.coerce.number().int().min(1).max(3e5);
2778
+ var TimeoutMs = z11.coerce.number().int().min(1).max(3e5);
1567
2779
  function parse(argv2) {
1568
2780
  const { values, positionals, tokens } = parseArgs2({ args: argv2, options: FLAGS, allowPositionals: true, strict: true, tokens: true });
1569
- const named = tokens.flatMap((token) => token.kind === "option" ? [token.name] : []);
1570
- const repeated = named.find((name, i) => !("multiple" in FLAGS[name]) && named.indexOf(name) !== i);
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);
1571
2783
  if (repeated) throw new Error(`Repeated --${repeated}`);
1572
2784
  const [command, ...rest] = positionals;
1573
2785
  if (command !== "tools" && command !== "call" && command !== "verify" && command !== "lean") {
@@ -1628,11 +2840,11 @@ function parse(argv2) {
1628
2840
  return opts;
1629
2841
  }
1630
2842
  function closestSibling(file) {
1631
- const dir = path6.dirname(file);
1632
- const want = path6.basename(file);
2843
+ const dir = path8.dirname(file);
2844
+ const want = path8.basename(file);
1633
2845
  let names;
1634
2846
  try {
1635
- names = fs5.readdirSync(dir);
2847
+ names = fs8.readdirSync(dir);
1636
2848
  } catch {
1637
2849
  return void 0;
1638
2850
  }
@@ -1650,11 +2862,11 @@ function closestSibling(file) {
1650
2862
  return row[b.length];
1651
2863
  };
1652
2864
  const scored = names.map((name) => ({ name, d: distance(want, name) })).filter(({ d }) => d <= 2).sort((x, y) => x.d - y.d);
1653
- return scored.length ? path6.join(dir, scored[0].name) : void 0;
2865
+ return scored.length ? path8.join(dir, scored[0].name) : void 0;
1654
2866
  }
1655
2867
  function readInput(file) {
1656
2868
  try {
1657
- return fs5.readFileSync(file, "utf8");
2869
+ return fs8.readFileSync(file, "utf8");
1658
2870
  } catch (error) {
1659
2871
  const code = error?.code;
1660
2872
  if (code === "ENOENT") {
@@ -1667,7 +2879,7 @@ function readInput(file) {
1667
2879
  }
1668
2880
  }
1669
2881
  function sourceNames(paths) {
1670
- const parts = paths.map((p) => path6.resolve(p).split(path6.sep).filter((s) => s !== ""));
2882
+ const parts = paths.map((p) => path8.resolve(p).split(path8.sep).filter((s) => s !== ""));
1671
2883
  const seen = /* @__PURE__ */ new Set();
1672
2884
  parts.forEach((segments, i) => {
1673
2885
  const full = segments.join("/");
@@ -1694,15 +2906,15 @@ async function argumentsFor(opts, io) {
1694
2906
  }
1695
2907
  function clearOutput(opts) {
1696
2908
  if (!opts.output) return;
1697
- const output = path6.resolve(opts.output);
1698
- const outputStat = fs5.existsSync(output) ? fs5.statSync(output) : void 0;
2909
+ const output = path8.resolve(opts.output);
2910
+ const outputStat = fs8.existsSync(output) ? fs8.statSync(output) : void 0;
1699
2911
  for (const input of [...opts.sources, opts.args, opts.bundle].filter((p) => !!p && p !== "-")) {
1700
- const inputStat = fs5.existsSync(input) ? fs5.statSync(input) : void 0;
1701
- if (path6.resolve(input) === output || outputStat && inputStat && inputStat.dev === outputStat.dev && inputStat.ino === outputStat.ino) {
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) {
1702
2914
  throw new Error("--output must not overwrite an input file");
1703
2915
  }
1704
2916
  }
1705
- fs5.rmSync(output, { force: true });
2917
+ fs8.rmSync(output, { force: true });
1706
2918
  }
1707
2919
  function rejection(result, unmet) {
1708
2920
  if (result.ok) return `${unmet} --expect ${unmet === 1 ? "expectation was" : "expectations were"} not met`;
@@ -1748,7 +2960,7 @@ async function runCli(argv2, io) {
1748
2960
  if (selected === void 0) throw new Error(`Result field missing: ${opts.select}`);
1749
2961
  const text3 = opts.select !== void 0 && typeof selected === "string" ? selected : `${JSON.stringify(selected, null, 2)}
1750
2962
  `;
1751
- if (opts.output) fs5.writeFileSync(opts.output, text3);
2963
+ if (opts.output) fs8.writeFileSync(opts.output, text3);
1752
2964
  else io.stdout(text3);
1753
2965
  }
1754
2966
  const artifact = opts.select !== void 0 ? "the unselected result; run without --select to see it" : opts.output ?? "the JSON on standard output";
@@ -1786,173 +2998,18 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
1786
2998
  import { EngineWorkerClient, catalogTools } from "@yichus/api-mcp";
1787
2999
 
1788
3000
  // src/studio/page-server.ts
1789
- import fs6 from "fs";
3001
+ import fs9 from "fs";
1790
3002
  import http from "http";
1791
- import path7 from "path";
1792
-
1793
- // src/studio/page.ts
1794
- var STUDIO_PAGE = `<!doctype html>
1795
- <html lang="en">
1796
- <head>
1797
- <meta charset="utf-8">
1798
- <meta name="viewport" content="width=device-width, initial-scale=1">
1799
- <title>Yichus studio</title>
1800
- <link rel="stylesheet" href="system-map.css">
1801
- <style>
1802
- :root { --ink:#0f172a; --muted:#475569; --faint:#94a3b8; --rule:#e2e8f0; --paper:#ffffff; --page:#f6f8fb;
1803
- --good:#047857; --goodSoft:#ecfdf5; --bad:#b91c1c; --badSoft:#fef2f2; --warn:#92400e; --warnSoft:#fffbeb; --accent:#22675e; }
1804
- * { box-sizing:border-box; }
1805
- body { margin:0; background:var(--page); color:var(--ink); font:16px/1.55 Inter, system-ui, sans-serif; }
1806
- main { max-width:1100px; margin:0 auto; padding:24px 16px 64px; }
1807
- header p { margin:4px 0 0; color:var(--muted); }
1808
- h1 { font-size:1.6rem; margin:0; font-weight:600; }
1809
- h2 { font-size:1.15rem; margin:32px 0 8px; }
1810
- section > p.lede { color:var(--muted); margin:0 0 12px; max-width:70ch; }
1811
- a.guide { color:var(--accent); font-weight:600; }
1812
- .frame { background:var(--paper); border:1px solid var(--rule); border-radius:10px; padding:16px; position:relative; }
1813
- .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; }
1814
- .yours { border-left:3px solid var(--accent); padding:4px 12px; margin:0; font-style:italic; }
1815
- .label { font-size:0.75rem; color:var(--muted); text-transform:uppercase; letter-spacing:0.06em; margin:0 0 4px; }
1816
- .now { font-weight:600; }
1817
- .question { border-top:1px solid var(--rule); padding:14px 0; }
1818
- .question:first-of-type { border-top:none; padding-top:4px; }
1819
- .question .ask { font-weight:600; margin:0 0 4px; }
1820
- .question .why { color:var(--muted); margin:0 0 8px; font-size:0.94rem; }
1821
- .choice { display:flex; gap:8px; align-items:flex-start; margin:6px 0; }
1822
- .choice span small { display:block; color:var(--muted); }
1823
- textarea { width:100%; min-height:3.2em; font:inherit; border:1px solid var(--rule); border-radius:8px; padding:8px; margin:6px 0; }
1824
- button { font:inherit; font-weight:600; border:none; border-radius:8px; padding:8px 14px; background:var(--accent); color:#fff; cursor:pointer; }
1825
- button:disabled { opacity:0.5; cursor:default; }
1826
- .props { list-style:none; margin:0; padding:0; }
1827
- .props li { border-top:1px solid var(--rule); padding:12px 0; display:grid; grid-template-columns:1fr auto; gap:4px 16px; }
1828
- .props li:first-child { border-top:none; }
1829
- .chip { font-size:0.8rem; font-weight:700; border-radius:999px; padding:2px 10px; white-space:nowrap; align-self:start; justify-self:start; }
1830
- .chip.good { color:var(--good); background:var(--goodSoft); }
1831
- .chip.bad { color:var(--bad); background:var(--badSoft); }
1832
- .chip.warn { color:var(--warn); background:var(--warnSoft); }
1833
- .chip.quiet { color:var(--muted); background:var(--rule); }
1834
- .props .choice-said { color:var(--muted); font-size:0.94rem; grid-column:1 / -1; }
1835
- details { grid-column:1 / -1; font-size:0.88rem; }
1836
- 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; }
1837
- .empty { color:var(--muted); }
1838
- @media (max-width:640px) { .props li { grid-template-columns:1fr; } }
1839
- </style>
1840
- </head>
1841
- <body>
1842
- <main>
1843
- <header>
1844
- <h1>Yichus studio<span id="project"></span></h1>
1845
- <p id="updated">Waiting for your agent.</p>
1846
- <p><a class="guide" href="https://yich.us/start/studio.html#page" target="_blank" rel="noopener">How to read this page</a> (opens in a new tab)</p>
1847
- </header>
1848
- <section aria-labelledby="h-asked">
1849
- <h2 id="h-asked">What you asked for</h2>
1850
- <p class="label">Your words</p>
1851
- <p class="yours" id="description">Nothing yet. Tell your agent what you want to build.</p>
1852
- </section>
1853
- <section aria-labelledby="h-now">
1854
- <h2 id="h-now">What is happening now</h2>
1855
- <div class="frame"><span class="tag">Yichus output</span>
1856
- <p class="now" id="summary">No checks have run yet.</p>
1857
- </div>
1858
- </section>
1859
- <section aria-labelledby="h-questions" id="questions-section" hidden>
1860
- <h2 id="h-questions">Questions for you</h2>
1861
- <p class="lede">Your agent will ask you these too. Answer here or in the chat, whichever is easier. <a class="guide" href="https://yich.us/start/studio.html#questions" target="_blank" rel="noopener">About these questions</a></p>
1862
- <div class="frame"><span class="tag">Yichus output</span><div id="questions"></div></div>
1863
- </section>
1864
- <section aria-labelledby="h-props">
1865
- <h2 id="h-props">What your program must do</h2>
1866
- <p class="lede"><a class="guide" href="https://yich.us/start/studio.html#statuses" target="_blank" rel="noopener">What each status means</a></p>
1867
- <div class="frame"><span class="tag">Yichus output</span><ul class="props" id="props"><li class="empty">No properties chosen yet.</li></ul></div>
1868
- </section>
1869
- <section aria-labelledby="h-map">
1870
- <h2 id="h-map">The shape of your program</h2>
1871
- <p class="lede"><a class="guide" href="https://yich.us/start/studio.html#shape" target="_blank" rel="noopener">How to read the picture</a></p>
1872
- <div class="frame"><div class="system-map" id="map"><p class="empty">The map appears once the program compiles.</p></div></div>
1873
- </section>
1874
- </main>
1875
- <script type="module">
1876
- import { mount } from './system-map.js';
3003
+ import path9 from "path";
1877
3004
 
1878
- const STATUS = {
1879
- unanswered: ['waiting for your answer', 'quiet'], decided: ['decided', 'good'], assumed: ['holds, under an assumption', 'warn'],
1880
- proven: ['proven', 'good'], bounded: ['holds in every scenario checked', 'good'], broken: ['broken', 'bad'],
1881
- mismatch: ['the code says otherwise', 'bad'], 'needs-case': ['scenario being written', 'quiet'],
1882
- inconclusive: ['not decided yet', 'warn'], 'no-check': ['no check needed', 'quiet'], pending: ['answered, waiting for your agent to check', 'quiet'],
1883
- };
1884
- const el = (tag, props = {}, ...kids) => { const n = Object.assign(document.createElement(tag), props); n.append(...kids); return n; };
1885
- let mapHash = null;
1886
-
1887
- function questionCard(q, saved) {
1888
- const card = el('div', { className: 'question' });
1889
- card.append(el('p', { className: 'ask', textContent: q.ask }), el('p', { className: 'why', textContent: q.why }));
1890
- const name = 'q-' + q.id;
1891
- for (const c of q.choices) {
1892
- const input = el('input', { type: 'radio', name, value: c.id, checked: saved?.choice === c.id });
1893
- card.append(el('label', { className: 'choice' }, input, el('span', {}, c.label, el('small', { textContent: c.means }))));
1894
- }
1895
- const words = el('textarea', { placeholder: 'In your own words (optional)', value: saved?.words ?? '' });
1896
- const status = el('span', { className: 'empty', textContent: saved ? ' Saved. Your agent will check it next.' : '' });
1897
- const save = el('button', { type: 'button', textContent: 'Save answer' });
1898
- save.addEventListener('click', async () => {
1899
- const picked = card.querySelector('input[name="' + CSS.escape(name) + '"]:checked');
1900
- if (!picked) { status.textContent = ' Pick one of the choices first.'; return; }
1901
- save.disabled = true;
1902
- const label = q.choices.find(c => c.id === picked.value).label;
1903
- const response = await fetch('/answer', { method: 'POST', headers: { 'content-type': 'application/json' },
1904
- body: JSON.stringify({ question: q.id, choice: picked.value, words: words.value.trim() || label }) });
1905
- const result = await response.json();
1906
- status.textContent = result.ok ? ' Saved. Your agent will check it next.' : ' ' + result.error;
1907
- save.disabled = false;
1908
- });
1909
- card.append(el('p', { className: 'label', textContent: 'Your words' }), words, save, status);
1910
- return card;
1911
- }
1912
-
1913
- function propertyRow(p, saved) {
1914
- const stale = saved && (!p.answer || p.answer.answeredAt !== saved.answeredAt);
1915
- const [words, tone] = STATUS[stale ? 'pending' : p.status];
1916
- const answer = stale ? saved : p.answer;
1917
- const label = p.question.choices.find(c => c.id === answer.choice)?.label ?? answer.choice;
1918
- const li = el('li', {}, el('span', { textContent: p.question.ask }), el('span', { className: 'chip ' + tone, textContent: words }));
1919
- li.append(el('span', { className: 'choice-said', textContent: 'You chose: ' + label + (answer.words && answer.words !== label ? ' ("' + answer.words + '")' : '') }));
1920
- return li;
1921
- }
1922
-
1923
- async function refresh() {
1924
- const { state, requirements } = await (await fetch('/state')).json();
1925
- const savedOf = id => requirements.answers.find(a => a.question === id);
1926
- if (requirements.description) document.getElementById('description').textContent = requirements.description;
1927
- if (!state) return;
1928
- document.getElementById('project').textContent = ': ' + state.project;
1929
- document.getElementById('updated').textContent = 'Last checked ' + new Date(state.updatedAt).toLocaleTimeString() + '.';
1930
- document.getElementById('summary').textContent = state.summary;
1931
- const open = state.properties.filter(p => p.status === 'unanswered' && !savedOf(p.question.id));
1932
- document.getElementById('questions-section').hidden = !open.length;
1933
- // Keep a half-written answer: only rebuild cards when the questions change.
1934
- const qs = document.getElementById('questions');
1935
- const ids = open.map(p => p.question.id).join(' ');
1936
- if (qs.dataset.ids !== ids) { qs.dataset.ids = ids; qs.replaceChildren(...open.map(p => questionCard(p.question, savedOf(p.question.id)))); }
1937
- const chosen = state.properties.filter(p => p.answer || savedOf(p.question.id));
1938
- document.getElementById('props').replaceChildren(...(chosen.length ? chosen.map(p => propertyRow(p, savedOf(p.question.id)))
1939
- : [el('li', { className: 'empty', textContent: 'No properties chosen yet.' })]));
1940
- const map = document.getElementById('map');
1941
- if (state.map && state.sourcesHash !== mapHash) { mapHash = state.sourcesHash; mount(map, state.map); }
1942
- else if (!state.map && state.mapError) map.replaceChildren(el('p', { className: 'empty', textContent: 'The map could not be drawn: ' + state.mapError }));
1943
- }
1944
-
1945
- new EventSource('/events').addEventListener('change', () => refresh());
1946
- refresh();
1947
- </script>
1948
- </body>
1949
- </html>
1950
- `;
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"];
1951
3007
 
1952
3008
  // src/studio/page-server.ts
1953
- var PAGE_ASSETS = ["system-map.js", "label-fit.js", "system-map-record.js", "system-map.css"];
1954
- function personView(state, names) {
3009
+ var PAGE_ASSETS = page_assets_default;
3010
+ function personView(state, names, marks = null) {
1955
3011
  if (!state) return null;
3012
+ const { sections, whatHappens, anchors, drawing } = personDiagrams(state, names);
1956
3013
  return {
1957
3014
  project: state.project,
1958
3015
  updatedAt: state.updatedAt,
@@ -1964,23 +3021,34 @@ function personView(state, names) {
1964
3021
  status: p.status
1965
3022
  })),
1966
3023
  map: state.map ? relabelSystemMap(state.map, names) : null,
1967
- mapError: state.mapError
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 }] : []) }
1968
3032
  };
1969
3033
  }
1970
3034
  function stateBody(root) {
1971
3035
  const { description, answers, names } = loadRequirements(root);
1972
- return { state: personView(readState(root), names ?? {}), requirements: { description, answers } };
3036
+ return { state: personView(readState(root), names ?? {}, loadMarks(root)), requirements: { description, answers } };
1973
3037
  }
1974
3038
  var STUDIO_PORTS = [4317, 4318, 4319, 4320, 4321, 0];
1975
3039
  async function startPageServer(root, assetsDir, port = null, now = () => /* @__PURE__ */ new Date()) {
1976
3040
  ensureStudioDir(root);
1977
3041
  const clients = /* @__PURE__ */ new Set();
1978
- const watched = ["state.json", "requirements.json"].map((name) => path7.join(studioDir(root), name));
3042
+ const watched = ["state.json", "requirements.json", "marks.json"].map((name) => path9.join(studioDir(root), name));
1979
3043
  const changed = (now2, before) => {
1980
3044
  if (now2.mtimeMs !== before.mtimeMs || now2.size !== before.size) for (const res of clients) res.write("event: change\ndata: {}\n\n");
1981
3045
  };
1982
- for (const file of watched) fs6.watchFile(file, { interval: 250 }, changed);
3046
+ for (const file of watched) fs9.watchFile(file, { interval: 250 }, changed);
1983
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
+ ]);
1984
3052
  const server = http.createServer((req, res) => {
1985
3053
  const send = (status, type, body) => {
1986
3054
  res.writeHead(status, { "content-type": type, "cache-control": "no-store", "x-content-type-options": "nosniff" });
@@ -2005,9 +3073,10 @@ async function startPageServer(root, assetsDir, port = null, now = () => /* @__P
2005
3073
  }
2006
3074
  const asset = PAGE_ASSETS.find((a) => url.pathname === `/${a}`);
2007
3075
  if (req.method === "GET" && asset) {
2008
- return send(200, asset.endsWith(".css") ? "text/css" : "text/javascript", fs6.readFileSync(path7.join(assetsDir, asset), "utf8"));
3076
+ return send(200, asset.endsWith(".css") ? "text/css" : "text/javascript", fs9.readFileSync(path9.join(assetsDir, asset), "utf8"));
2009
3077
  }
2010
- if (req.method === "POST" && url.pathname === "/answer") {
3078
+ const take = req.method === "POST" ? takes.get(url.pathname) : void 0;
3079
+ if (take) {
2011
3080
  if (req.headers["content-type"] !== "application/json") return send(415, "text/plain", "Send JSON.");
2012
3081
  let body = "";
2013
3082
  req.setEncoding("utf8");
@@ -2020,9 +3089,9 @@ async function startPageServer(root, assetsDir, port = null, now = () => /* @__P
2020
3089
  try {
2021
3090
  args = JSON.parse(body);
2022
3091
  } catch {
2023
- return send(400, "application/json", JSON.stringify({ ok: false, error: "The answer is not JSON." }));
3092
+ return send(400, "application/json", JSON.stringify({ ok: false, error: "The request is not JSON." }));
2024
3093
  }
2025
- const result = recordAnswer(root, args, now());
3094
+ const result = take(args);
2026
3095
  send(result.ok ? 200 : 400, "application/json", JSON.stringify(result));
2027
3096
  });
2028
3097
  return;
@@ -2045,7 +3114,7 @@ async function startPageServer(root, assetsDir, port = null, now = () => /* @__P
2045
3114
  return {
2046
3115
  url: `http://127.0.0.1:${bound}/`,
2047
3116
  async close() {
2048
- for (const file of watched) fs6.unwatchFile(file, changed);
3117
+ for (const file of watched) fs9.unwatchFile(file, changed);
2049
3118
  const closed = new Promise((resolve) => server.close(() => resolve()));
2050
3119
  server.closeAllConnections();
2051
3120
  await closed;