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/README.md +45 -16
- package/dist/cli.js +1328 -259
- package/dist/cli.js.map +1 -1
- package/dist/studio-page/dom.js +22 -0
- package/dist/studio-page/label-fit.js +7 -0
- package/dist/studio-page/layer-diagram.js +458 -0
- package/dist/studio-page/layer-groups.js +129 -0
- package/dist/studio-page/source-link.js +17 -0
- package/dist/studio-page/studio-views.js +256 -0
- package/dist/studio-page/system-map-record.js +122 -8
- package/dist/studio-page/system-map.js +461 -85
- package/dist/studio-page/what-happens-record.js +217 -0
- package/dist/studio-page/what-happens.js +752 -0
- package/dist/studio-reference/deploy-platforms.md +79 -0
- package/dist/studio-reference/portable-programs.md +58 -0
- package/dist/studio-reference/porting.md +32 -0
- package/dist/studio-reference/refund-desk-one-round.bosatsu +196 -0
- package/dist/studio-reference/refund-desk.bosatsu +194 -0
- package/dist/studio-reference/refund-policy.bosatsu +97 -0
- package/dist/studio-reference/refund-test.bosatsu +75 -0
- package/dist/studio-reference/the-page.md +107 -0
- package/dist/studio-reference/worker-authority.md +159 -0
- package/package.json +2 -2
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
|
|
9
|
-
import
|
|
8
|
+
import fs8 from "fs";
|
|
9
|
+
import path8 from "path";
|
|
10
10
|
import { isDeepStrictEqual, parseArgs as parseArgs2 } from "util";
|
|
11
|
-
import { z as
|
|
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
|
|
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
|
|
280
|
+
import { z as z6 } from "zod";
|
|
280
281
|
|
|
281
282
|
// ../../web/system-map-record.js
|
|
282
|
-
var MARKS_VERSION = "yichus-marks-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
771
|
+
const walk3 = (v, key) => {
|
|
646
772
|
if (typeof v === "string") visit(v, key);
|
|
647
|
-
else if (Array.isArray(v)) v.forEach((x) =>
|
|
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))
|
|
775
|
+
for (const [k, x] of Object.entries(v)) if (!skipped(k, x)) walk3(x, k);
|
|
650
776
|
}
|
|
651
777
|
};
|
|
652
|
-
|
|
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
|
|
833
|
+
const walk3 = (v, key) => {
|
|
708
834
|
if (typeof v === "string") return text3(v, key);
|
|
709
|
-
if (Array.isArray(v)) return v.map((x) =>
|
|
710
|
-
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, skipped(k, x) ? x :
|
|
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 =
|
|
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
|
|
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())
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
922
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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 =
|
|
930
|
-
topology:
|
|
931
|
-
verdict:
|
|
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 =
|
|
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
|
-
|
|
1068
|
-
|
|
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
|
-
"
|
|
1071
|
-
|
|
1072
|
-
'
|
|
1073
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: `${
|
|
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
|
-
|
|
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: [
|
|
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)
|
|
1157
|
-
|
|
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
|
|
1728
|
+
import { z as z10 } from "zod";
|
|
1189
1729
|
|
|
1190
|
-
// src/studio/
|
|
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) =>
|
|
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
|
-
${
|
|
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 =
|
|
1297
|
-
var AnswerArgs =
|
|
1298
|
-
var
|
|
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:
|
|
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.
|
|
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
|
|
1352
|
-
if (!
|
|
1353
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
1570
|
-
const repeated =
|
|
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 =
|
|
1632
|
-
const want =
|
|
2843
|
+
const dir = path8.dirname(file);
|
|
2844
|
+
const want = path8.basename(file);
|
|
1633
2845
|
let names;
|
|
1634
2846
|
try {
|
|
1635
|
-
names =
|
|
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 ?
|
|
2865
|
+
return scored.length ? path8.join(dir, scored[0].name) : void 0;
|
|
1654
2866
|
}
|
|
1655
2867
|
function readInput(file) {
|
|
1656
2868
|
try {
|
|
1657
|
-
return
|
|
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) =>
|
|
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 =
|
|
1698
|
-
const outputStat =
|
|
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 =
|
|
1701
|
-
if (
|
|
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
|
-
|
|
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)
|
|
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
|
|
3001
|
+
import fs9 from "fs";
|
|
1790
3002
|
import http from "http";
|
|
1791
|
-
import
|
|
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
|
-
|
|
1879
|
-
|
|
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 =
|
|
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) =>
|
|
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)
|
|
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",
|
|
3076
|
+
return send(200, asset.endsWith(".css") ? "text/css" : "text/javascript", fs9.readFileSync(path9.join(assetsDir, asset), "utf8"));
|
|
2009
3077
|
}
|
|
2010
|
-
|
|
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
|
|
3092
|
+
return send(400, "application/json", JSON.stringify({ ok: false, error: "The request is not JSON." }));
|
|
2024
3093
|
}
|
|
2025
|
-
const result =
|
|
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)
|
|
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;
|