yichus 0.3.2 → 0.3.4
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 +5 -2
- package/dist/cli.js +27 -0
- package/dist/cli.js.map +1 -1
- package/dist/main.js +400 -118
- package/dist/main.js.map +1 -1
- package/dist/studio-page/system-map-record.js +93 -7
- package/dist/studio-page/system-map.js +60 -23
- package/dist/studio-page/what-happens-record.js +90 -6
- package/dist/studio-page/what-happens.js +72 -13
- package/dist/studio-reference/calculator.md +1 -0
- package/dist/studio-reference/deploy-platforms.md +3 -0
- package/dist/studio-reference/mutants/double-signup.bosatsu +149 -0
- package/dist/studio-reference/mutants/overfill.bosatsu +153 -0
- package/dist/studio-reference/near-miss-case.json +104 -0
- package/dist/studio-reference/near-miss-signups.bosatsu +139 -0
- package/dist/studio-reference/protocol-cases.md +7 -1
- package/dist/studio-reference/signups-case.json +122 -0
- package/dist/studio-reference/signups-laws.bosatsu +32 -0
- package/dist/studio-reference/signups.bosatsu +155 -0
- package/dist/studio-reference/spots.bosatsu +27 -0
- package/dist/studio-reference/the-page.md +7 -5
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -301,15 +301,16 @@ var VOCABULARY = {
|
|
|
301
301
|
modelRing: "violet ring: the entry reaches the model <- entries[].model",
|
|
302
302
|
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",
|
|
303
303
|
netRing: "orange ring: the entry reaches the network <- entries[].net",
|
|
304
|
-
guard:
|
|
304
|
+
guard: 'red diamond on the path where the route can refuse, the check named above it (the program\u2019s definition it calls, "<table> row found", "<table> row\u2019s <field>" for a branch on a field of a read row, or "a test in <definition>" for a comparison or library answer that definition branches on), failure stem below; a branch no path refuses on is not drawn here <- entries[].guards (structure.scenarios[]: branches where one arm ends only in refusals), wrappers[].guards, entries[].passed[].guards, entries[].beside.guards',
|
|
305
|
+
policy: "under the db, each table with its access policy as the access check proves it: own = each caller reads and writes only their own row; no row rule = the access check holds no rule over its rows, so who reads or writes which row is decided only by the code drawn on the routes; guarded = rows pass the named guard; role \u2026 only = only callers with that role reach its rows; unchecked = the access check gave this table no verdict; a policy ending \u201Cbroken\u201D is one the code breaks, and \u201Cnot proven\u201D one the check could not settle; counted when the tables are too many to list <- resources[kind=db].policies",
|
|
305
306
|
hull: "translucent region in the hull band: paths merge into it, run one shared stretch, and split; everything inside it runs inside the wrapper; its right wall, drawn in the hull ink, is where the wrapper returns <- wrappers[]",
|
|
306
307
|
socket: "broken-rim ring: a function the caller hands in runs here; its centre fills like a station <- entries[].passed[]",
|
|
307
308
|
token: "small grey word under a path: the value the caller fixes for the wrapper <- entries[].policy",
|
|
308
309
|
afterDot: "dot in the after band: a call the entry makes after its wrapper returns <- entries[].beside.calls",
|
|
309
310
|
builder: "plus-disc under a station with the call that builds the handle there <- uses[how=local].builtBy",
|
|
310
311
|
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]',
|
|
311
|
-
fork:
|
|
312
|
-
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
|
+
fork: 'three prongs and a type: a choice keyed on a plain String or Int; under the mark that makes it (under a list of checks, named by the check it is inside: "can_see on String"), else on the quiet line <- keyed[]',
|
|
313
|
+
bundle: 'one cable with how many routes it carries ("4 routes"): entries with the same wrapper and identical marks; its title names every route: by the one part of the path that differs, among at most six ("/{tags, tasks}/sync"), or each by name when there are at most four; else a pattern with *; opening it draws each one <- bundles[]',
|
|
313
314
|
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",
|
|
314
315
|
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[]',
|
|
315
316
|
loop: "loop arc with package names: packages whose types lead back to themselves <- phases.cycles",
|
|
@@ -330,6 +331,7 @@ var VOCABULARY = {
|
|
|
330
331
|
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`,
|
|
331
332
|
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[]'
|
|
332
333
|
},
|
|
334
|
+
names: "every check, call and table name is a label the code's author (often an AI) chose; the picture proves where a check runs, what it is given and what happens when it fails, never what it decides: a reader who doubts the author reads that rule itself.",
|
|
333
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."
|
|
334
336
|
};
|
|
335
337
|
var bare = (id) => String(id).split("::").pop();
|
|
@@ -449,9 +451,51 @@ function programsOf(st, layers = null) {
|
|
|
449
451
|
});
|
|
450
452
|
}
|
|
451
453
|
var outsideResourcesOf = (steps) => uniq((steps || []).map((e) => e.resource).filter((r) => r === "model" || r === "net"));
|
|
454
|
+
var leavesUnder = (t) => t.kind === "leaf" ? [t] : t.arms.flatMap((a) => leavesUnder(a.next));
|
|
455
|
+
function refusingForks(tree) {
|
|
456
|
+
const out = [];
|
|
457
|
+
const go = (t) => {
|
|
458
|
+
if (!t || t.kind !== "node") return;
|
|
459
|
+
const refused = t.arms.map((a) => leavesUnder(a.next).every((l) => l.outcome.kind === "refused"));
|
|
460
|
+
if (t.concern?.layer !== "platform" && refused.some(Boolean) && !refused.every(Boolean)) out.push(t);
|
|
461
|
+
t.arms.forEach((a) => go(a.next));
|
|
462
|
+
};
|
|
463
|
+
go(tree);
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
var forkId = (n, own) => n.from.kind === "call" && own.has(n.from.callee) ? n.from.callee : n.id;
|
|
467
|
+
var forkParts = (n, own) => n.from.kind === "call" && own.has(n.from.callee) ? { definition: n.from.callee } : n.from.kind === "read" && n.from.table ? { table: n.from.table, ...n.from.path?.length ? { field: n.from.path.join(".") } : {} } : n.from.site ? { definition: n.from.site } : {};
|
|
468
|
+
var forkKey = (own) => (n) => n.from.kind === "call" && own.has(n.from.callee) ? n.from.callee : `${forkLabel(n, own)} ${n.from.site}`;
|
|
469
|
+
var forkLabel = (n, own) => n.from.kind === "call" ? own.has(n.from.callee) ? bare(n.from.callee) : `a test in ${bare(n.from.site)}` : n.from.kind === "read" && n.from.table ? n.from.path?.length ? `${n.from.table} row\u2019s ${n.from.path.join(".")}` : `${n.from.table} row found` : n.from.site ? `a test in ${bare(n.from.site)}` : "a computed branch";
|
|
470
|
+
var policyWord = (r) => {
|
|
471
|
+
const rule = policyRule(r);
|
|
472
|
+
if (rule === "unchecked") return rule;
|
|
473
|
+
return r.status === "violated" ? `${rule}, broken` : r.proven ? rule : `${rule}, not proven`;
|
|
474
|
+
};
|
|
475
|
+
var policyRule = (r) => {
|
|
476
|
+
const roles = () => r.roles.join(", "), guards = () => r.guards.join(", ");
|
|
477
|
+
switch (r.policyKind) {
|
|
478
|
+
case "OwnerScoped":
|
|
479
|
+
return "own";
|
|
480
|
+
case "Public":
|
|
481
|
+
return "no row rule";
|
|
482
|
+
case "Guarded":
|
|
483
|
+
return `guarded by ${guards()}`;
|
|
484
|
+
// with no guard, only the roles reach its rows
|
|
485
|
+
case "GuardedOrRoles":
|
|
486
|
+
return r.guards.length ? `guarded by ${guards()} or role ${roles()}` : `role ${roles()} only`;
|
|
487
|
+
case "PublicReadRoleWrite":
|
|
488
|
+
return `anyone reads; role ${roles()} writes`;
|
|
489
|
+
default:
|
|
490
|
+
return "unchecked";
|
|
491
|
+
}
|
|
492
|
+
};
|
|
452
493
|
function buildSystemMap(engine, { program }) {
|
|
453
494
|
for (const [k, v] of Object.entries(engine)) if (!v.ok) throw new Error(`${k}: ${v.error}`);
|
|
454
495
|
const report = engine.report, st = engine.structure, abs = engine.abstractions.abstractions, layers = engine.layers.layers;
|
|
496
|
+
const whole = (s) => s.tree && !s.truncated && !s.unsupported;
|
|
497
|
+
const trees = new Map((st.scenarios || []).filter(whole).map((s) => [s.entry, refusingForks(s.tree)]));
|
|
498
|
+
const ownDefs = new Set(layers.packages.flatMap((p) => p.definitions.map((d) => `${p.name}::${d.name}`)));
|
|
455
499
|
const routes = (report.surface.routes || []).slice().sort((a, b) => a.path.localeCompare(b.path));
|
|
456
500
|
const engEntry = new Map(st.entries.map((e) => [e.entry, e]));
|
|
457
501
|
const shapes = new Map(abs.routeShapes.map((r) => [r.path, r]));
|
|
@@ -466,11 +510,17 @@ function buildSystemMap(engine, { program }) {
|
|
|
466
510
|
resources: outsideResourcesOf(inst.besideEffectSteps),
|
|
467
511
|
from: "abstractions.instantiations[].beside*"
|
|
468
512
|
});
|
|
513
|
+
const resultOf = (sig) => sig.includes("->") ? sig.slice(sig.lastIndexOf("->") + 2).trim() : null;
|
|
514
|
+
const returnsCallers = (g) => {
|
|
515
|
+
const result = resultOf(g.signature);
|
|
516
|
+
return result !== null && (/^[a-z]\w*$/.test(result) || (g.parameterGroups || []).flat().some((sl) => sl.role === "function" && sl.returnType === result));
|
|
517
|
+
};
|
|
518
|
+
const wraps = (g) => !!g && (/\bIO\[/.test(g.signature) || g.envelope.effects.length > 0 || returnsCallers(g) && (g.envelope.guards.length > 0 || g.envelope.dispatchesOn.length > 0));
|
|
469
519
|
const entries = routes.map((r) => {
|
|
470
520
|
const key = `route:${r.path}`;
|
|
471
521
|
const ee = engEntry.get(key);
|
|
472
522
|
const rs = shapes.get(r.path);
|
|
473
|
-
const wrapper = rs.shapedByGenerator;
|
|
523
|
+
const wrapper = wraps(gens.get(rs.shapedByGenerator)) ? rs.shapedByGenerator : null;
|
|
474
524
|
const inst = wrapper ? insts.find((i) => i.callerId === rs.handlerId && i.generatorId === wrapper) : null;
|
|
475
525
|
const passed = (inst ? inst.slotBindings : []).filter((sb) => ["function", "effect"].includes(sb.slotRole)).map((sb) => ({
|
|
476
526
|
slot: sb.slotName,
|
|
@@ -507,8 +557,9 @@ function buildSystemMap(engine, { program }) {
|
|
|
507
557
|
model: ee.resources.includes("model"),
|
|
508
558
|
net: ee.resources.includes("net"),
|
|
509
559
|
writes: reach.some((e) => isWrite(e.op)),
|
|
510
|
-
|
|
511
|
-
|
|
560
|
+
// every branch of the entry's own where a path refuses (a wrapper's are
|
|
561
|
+
// its own, and a function handed to it keeps its own slot)
|
|
562
|
+
...trees.has(key) ? ((forks) => ({ guards: forks.map((n) => forkLabel(n, ownDefs)), guardIds: forks.map((n) => forkId(n, ownDefs)), guardParts: forks.map((n) => forkParts(n, ownDefs)) }))(uniqBy(trees.get(key).filter((n) => n.concern?.layer !== "cross" && !(inst ? inst.slotBindings : []).some((sb) => (sb.guards || []).includes(forkId(n, ownDefs)))), forkKey(ownDefs))) : { guards: rs.handlerGuards.map(bare), guardIds: rs.handlerGuards },
|
|
512
563
|
wrapper,
|
|
513
564
|
via: rs.viaSpecialization ? bare(rs.viaSpecialization) : null,
|
|
514
565
|
policy: (inst ? inst.slotBindings : []).filter((s) => s.slotRole === "semantic-object").map((s) => s.value).filter(Boolean),
|
|
@@ -598,7 +649,12 @@ function buildSystemMap(engine, { program }) {
|
|
|
598
649
|
}
|
|
599
650
|
const present = uniq(st.ioKinds.flatMap((b) => b.resources));
|
|
600
651
|
const tables = uniq(entries.flatMap((e) => [...e.reach, ...e.beside ? e.beside.effects : []].map((r) => r.table))).sort();
|
|
601
|
-
const
|
|
652
|
+
const policies = engine.access ? Object.fromEntries(engine.access.report.resources.filter((r) => tables.includes(r.resource)).map((r) => [r.resource, policyWord(r)])) : null;
|
|
653
|
+
const resources = ["db", "model", "net"].filter((k) => present.includes(k)).map((k) => ({
|
|
654
|
+
kind: k,
|
|
655
|
+
...k === "db" ? { tables, ...policies ? { policies, policyFrom: "access.report.resources[].policyKind, guards, roles" } : {} } : {},
|
|
656
|
+
from: "structure.ioKinds[].resources"
|
|
657
|
+
}));
|
|
602
658
|
const ghostResources = ["db", "model", "net"].filter((k) => !present.includes(k)).map((k) => ({ kind: k, from: "structure.ioKinds[] checked: no site of this kind" }));
|
|
603
659
|
const transports = uniq(st.entries.map((e) => e.transport));
|
|
604
660
|
const callers = [];
|
|
@@ -712,13 +768,22 @@ function bundlesOf(entries, uses, keyed) {
|
|
|
712
768
|
}
|
|
713
769
|
function pathPattern(paths) {
|
|
714
770
|
if (paths.length === 1) return paths[0];
|
|
771
|
+
const sorted = [...paths].sort();
|
|
715
772
|
const segs = paths.map((p) => p.split("/"));
|
|
716
773
|
const n = Math.min(...segs.map((s) => s.length));
|
|
717
774
|
const out = [];
|
|
775
|
+
const vary = [];
|
|
718
776
|
for (let i = 0; i < n; i++) {
|
|
719
777
|
const v = uniq(segs.map((s) => s[i]));
|
|
720
778
|
out.push(v.length === 1 ? v[0] : "*");
|
|
779
|
+
if (v.length > 1) vary.push([i, v]);
|
|
780
|
+
}
|
|
781
|
+
if (segs.every((s) => s.length === n) && vary.length === 1 && vary[0][1].length <= 6) {
|
|
782
|
+
const [[i, v]] = vary;
|
|
783
|
+
out[i] = `{${[...v].sort().join(", ")}}`;
|
|
721
784
|
}
|
|
785
|
+
if (segs.every((s) => s.length === n) && vary.length <= 1) return out.join("/");
|
|
786
|
+
if (paths.length <= 4) return sorted.join(", ");
|
|
722
787
|
if (segs.every((s) => s.length === n)) return out.join("/");
|
|
723
788
|
let k = 0;
|
|
724
789
|
while (k < n && out[k] !== "*") k++;
|
|
@@ -750,6 +815,7 @@ var STRUCTURAL = /* @__PURE__ */ new Set([
|
|
|
750
815
|
"vars",
|
|
751
816
|
"id"
|
|
752
817
|
]);
|
|
818
|
+
var DESCRIBED = /* @__PURE__ */ new Set(["guards", "policies", "policyFrom"]);
|
|
753
819
|
var WHAT = {
|
|
754
820
|
path: "an action the app answers",
|
|
755
821
|
routes: "an action the app answers",
|
|
@@ -758,10 +824,15 @@ var WHAT = {
|
|
|
758
824
|
roles: "a permission an action requires, held by a person's role or by the app's own server",
|
|
759
825
|
policy: "a setting an action passes to shared code",
|
|
760
826
|
tokens: "a setting an action passes to shared code",
|
|
761
|
-
fields: "an operation a pluggable part offers"
|
|
827
|
+
fields: "an operation a pluggable part offers",
|
|
828
|
+
field: "a field of a kind of record the app keeps"
|
|
762
829
|
};
|
|
763
830
|
var PACKAGE = "a section of the program";
|
|
764
831
|
var DEFINITION = "a piece of the program";
|
|
832
|
+
var MADE = "a kind of value the program works out";
|
|
833
|
+
var OFFERED = "a part the program offers for others to use";
|
|
834
|
+
var DEFINED = { makes: MADE, exports: OFFERED, exported: OFFERED };
|
|
835
|
+
var PACKAGE_KEYS = /* @__PURE__ */ new Set(["package", "pkg", "instantiatingPackages"]);
|
|
765
836
|
var VALUE = "a kind of value";
|
|
766
837
|
var TYPES = /* @__PURE__ */ new Set(["type", "types", "signature"]);
|
|
767
838
|
var WORD = /[A-Za-z_][\w/]*::\w+|[A-Za-z_]\w*/g;
|
|
@@ -781,7 +852,7 @@ var leaves = (record, visit) => {
|
|
|
781
852
|
if (typeof v === "string") visit(v, key);
|
|
782
853
|
else if (Array.isArray(v)) v.forEach((x) => walk3(x, key));
|
|
783
854
|
else if (v && typeof v === "object") {
|
|
784
|
-
for (const [k, x] of Object.entries(v)) if (!skipped(k, x)) walk3(x, k);
|
|
855
|
+
for (const [k, x] of Object.entries(v)) if (!skipped(k, x) && !DESCRIBED.has(k)) walk3(x, k);
|
|
785
856
|
}
|
|
786
857
|
};
|
|
787
858
|
walk3(record, null);
|
|
@@ -789,16 +860,19 @@ var leaves = (record, visit) => {
|
|
|
789
860
|
function nameableParts(map, own) {
|
|
790
861
|
const found = /* @__PURE__ */ new Map();
|
|
791
862
|
const theirs2 = (pkg) => !own || own.has(pkg);
|
|
863
|
+
const rank = (what) => [OFFERED, MADE, PACKAGE].indexOf(what);
|
|
792
864
|
const add = (code, what) => {
|
|
793
|
-
if (code && !found.has(code) && !(code in PREDEF_WORDS) && (what !== PACKAGE || theirs2(code))) found.set(code, what);
|
|
865
|
+
if (code && (!found.has(code) || rank(what) > rank(found.get(code))) && !(code in PREDEF_WORDS) && (what !== PACKAGE || theirs2(code))) found.set(code, what);
|
|
794
866
|
};
|
|
795
|
-
const idParts = (id) => {
|
|
867
|
+
const idParts = (id, key) => {
|
|
796
868
|
const i = id.lastIndexOf("::");
|
|
797
869
|
if (!theirs2(id.slice(0, i))) return;
|
|
798
870
|
add(id.slice(0, i), PACKAGE);
|
|
799
|
-
add(id.slice(i + 2), DEFINITION);
|
|
871
|
+
add(id.slice(i + 2), DEFINED[key] ?? DEFINITION);
|
|
800
872
|
};
|
|
873
|
+
const isPackage = (s, key) => own?.has(s) || PACKAGE_KEYS.has(key) || s.includes("/");
|
|
801
874
|
leaves(map, (s, key) => {
|
|
875
|
+
if (key === "guardIds" && s.startsWith("route:")) return;
|
|
802
876
|
if (s.startsWith("route:")) return add(s.slice(6), WHAT.path);
|
|
803
877
|
if (TYPES.has(key)) {
|
|
804
878
|
for (const w of s.match(WORD) ?? []) if (w.includes("::")) idParts(w);
|
|
@@ -806,12 +880,12 @@ function nameableParts(map, own) {
|
|
|
806
880
|
return;
|
|
807
881
|
}
|
|
808
882
|
if (s.includes("::")) {
|
|
809
|
-
for (const w of s.match(WORD)) if (w.includes("::")) idParts(w);
|
|
883
|
+
for (const w of s.match(WORD)) if (w.includes("::")) idParts(w, key);
|
|
810
884
|
return;
|
|
811
885
|
}
|
|
812
886
|
if (STRUCTURAL.has(key)) return;
|
|
813
|
-
if (key === "name" || key
|
|
814
|
-
add(s, WHAT[key] ?? DEFINITION);
|
|
887
|
+
if (key === "name" || PACKAGE_KEYS.has(key) || key === "cycles" || key === "to" || own?.has(s)) return add(s, isPackage(s, key) ? PACKAGE : DEFINITION);
|
|
888
|
+
add(s, WHAT[key] ?? DEFINED[key] ?? DEFINITION);
|
|
815
889
|
});
|
|
816
890
|
for (const b of map.bundles ?? []) if (b.count > 1) found.delete(b.label);
|
|
817
891
|
return [...found].map(([code, what]) => ({ code, what })).sort((a, b) => a.code.localeCompare(b.code));
|
|
@@ -863,19 +937,22 @@ var VOCABULARY2 = {
|
|
|
863
937
|
version: MARKS_VERSION,
|
|
864
938
|
added: ["outcome"],
|
|
865
939
|
marks: {
|
|
866
|
-
entryHead:
|
|
940
|
+
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 (when no entry is described, that one line is all the rack draws, and it only counts them: "8 entries"; a scenario is a requirement someone wrote, so its absence says nothing about tests); under the head, "same steps as \u2026" names the entries whose trees take the same steps on their own tables and checks <- entries[], ghosts[], entries[].sameShape',
|
|
867
941
|
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",
|
|
868
|
-
fork:
|
|
869
|
-
|
|
942
|
+
fork: 'three prongs: a branch the code takes on the request, the caller, a value read from a table, or a call result, with "\u2190 request", "\u2190 caller" or "\u2190 <table> as read" saying where what it tests comes from; the arm each line takes is written where it leaves <- tree nodes, nodes[].from.sources',
|
|
943
|
+
docs: "under the names line, in italic, the comment the author wrote above each named check, marked unchecked: what the author says it decides, which the engine does not confirm <- docs (structure.docs)",
|
|
944
|
+
rows: `under the tree, for each table its stations touch (and those of the entries named in "same steps as"), what one row holds, by its checked type: "posts holds List[Post(id: String, \u2026, edited: Bool)]"; a field that is a record gives only its type's name, a choice of bare cases its cases ("visibility: Open | Members") <- rows (structure.rows)`,
|
|
945
|
+
station: "ring on a line: an effect on that path, its verb and table beside it; filled = it writes; mint = the platform hands out a fresh row id. Under it, in grey, the row it touches (the caller's row; the row named by a request field; one fixed row; a new row; every row, and the guard that keeps them) and, for a write, whether the row is replaced whole or updated from what it read, then where what it writes comes from, field by field (\u2190 caller, \u2190 request.<field>, \u2190 a literal, \u2190 new id, \u2190 a call and what it was computed from) <- arms[].effects[].key, value, kept (access report.findings[operation=keep_visible].provenBy)",
|
|
870
946
|
modelRing: "violet ring: the path reaches the model <- effects[resource=model]",
|
|
871
947
|
netRing: "orange ring: the path reaches the network <- effects[resource=net]",
|
|
872
948
|
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",
|
|
873
949
|
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",
|
|
874
950
|
struck: "a struck ghost: something checked and found absent (an entry nobody described, a write a claim says a path must not make) <- ghosts[], claims holding with not",
|
|
875
951
|
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",
|
|
876
|
-
compared: 'a line under the rack
|
|
952
|
+
compared: 'a line under the rack when a base was compared: "compared with <base> \xB7 N fixed \xB7 M still failing"; with no base, no line <- diff',
|
|
877
953
|
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: the effects it makes before its test above the arms, then one row per arm read as a side stub (the arm, the effects that arm makes, its struck writes, what the request gets, ticks), then "continue"; 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`
|
|
878
954
|
},
|
|
955
|
+
names: "every check, call and table name is a label the code's author (often an AI) chose; the picture proves where a check runs, what it is given and what happens when it fails, never what it decides: a reader who doubts the author reads that rule itself.",
|
|
879
956
|
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.",
|
|
880
957
|
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."
|
|
881
958
|
};
|
|
@@ -953,6 +1030,11 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
953
1030
|
const claims = st.claims || { scenarios: [], undescribed: (st.scenarios || []).map((s) => s.entry) };
|
|
954
1031
|
const byEntry = new Map(claims.scenarios.filter((s) => s.status === "matched").map((s) => [s.entry, s]));
|
|
955
1032
|
const routes = new Map((st.entries || []).map((e) => [e.entry, e]));
|
|
1033
|
+
const kept = new Map((engine.access?.report?.findings || []).filter((f) => f.operation === "keep_visible" && f.kind === "GuardProven" && f.status === "holds").map((f) => [`${f.package}::${f.binding}\0${f.resource}`, `${(f.provenBy || []).join(" and ")} passes for the caller and the row`]));
|
|
1034
|
+
const withKept = (es) => es.map((e) => {
|
|
1035
|
+
const by = e.kind === "DbQuery" && e.tables.length === 1 ? kept.get(`${e.site}\0${e.tables[0]}`) : null;
|
|
1036
|
+
return by ? { ...e, kept: by } : e;
|
|
1037
|
+
});
|
|
956
1038
|
const entries = st.scenarios.map((s) => {
|
|
957
1039
|
const meta = routes.get(s.entry) || {};
|
|
958
1040
|
const sc = byEntry.get(s.entry);
|
|
@@ -960,7 +1042,7 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
960
1042
|
const copy = (t) => {
|
|
961
1043
|
if (!t) return null;
|
|
962
1044
|
if (t.kind === "leaf") return { ...t, words: names.get(t.id) || null, claims: [], witnesses: [], status: "grey" };
|
|
963
|
-
return { ...t, words: names.get(t.id) || null, arms: t.arms.map((a) => ({ ...a, words: names.get(a.id) || null, next: copy(a.next) })) };
|
|
1045
|
+
return { ...t, words: names.get(t.id) || null, arms: t.arms.map((a) => ({ ...a, effects: withKept(a.effects), words: names.get(a.id) || null, next: copy(a.next) })) };
|
|
964
1046
|
};
|
|
965
1047
|
return {
|
|
966
1048
|
entry: s.entry,
|
|
@@ -970,7 +1052,7 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
970
1052
|
transport: meta.transport || null,
|
|
971
1053
|
title: sc?.title || null,
|
|
972
1054
|
described: !!sc,
|
|
973
|
-
effects: s.effects,
|
|
1055
|
+
effects: withKept(s.effects),
|
|
974
1056
|
tree: copy(s.tree),
|
|
975
1057
|
leafCount: Number(s.leafCount),
|
|
976
1058
|
truncated: s.truncated,
|
|
@@ -980,6 +1062,17 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
980
1062
|
from: "structure.scenarios[]"
|
|
981
1063
|
};
|
|
982
1064
|
});
|
|
1065
|
+
const shapeOf = (e) => {
|
|
1066
|
+
const eff = (x) => `${x.op}${x.key ? `@${x.key.kind}` : ""}${x.value ? `<${x.value.kind}` : ""}${x.kept ? "|kept" : ""}`;
|
|
1067
|
+
const go = (t) => !t ? "" : t.kind === "leaf" ? `(${t.outcome.kind}:${(t.outcome.statuses || (t.outcome.status ? [t.outcome.status] : [])).join("/")})` : `[${t.from.kind}${t.arms.map((a) => `{${a.effects.map(eff).join(",")}${go(a.next)}}`).join("")}]`;
|
|
1068
|
+
return `${(e.effects || []).map(eff).join(",")}${go(e.tree)}`;
|
|
1069
|
+
};
|
|
1070
|
+
const byShape = /* @__PURE__ */ new Map();
|
|
1071
|
+
for (const e of entries) byShape.set(shapeOf(e), [...byShape.get(shapeOf(e)) || [], e.entry]);
|
|
1072
|
+
for (const e of entries) {
|
|
1073
|
+
const same = byShape.get(shapeOf(e)).filter((x) => x !== e.entry);
|
|
1074
|
+
if (same.length) e.sameShape = { entries: same, from: "derived: entries[] trees equal in node kinds, arms, effect verbs, row keys and value kinds, and outcomes" };
|
|
1075
|
+
}
|
|
983
1076
|
const entryOf = new Map(entries.map((e) => [e.entry, e]));
|
|
984
1077
|
const leafById = new Map(entries.flatMap((e) => leavesOf(e.tree).map((l) => [l.id, l])));
|
|
985
1078
|
const checked = [], notes = [], refused = [], mismatches = [];
|
|
@@ -1085,6 +1178,18 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
1085
1178
|
refused,
|
|
1086
1179
|
mismatches,
|
|
1087
1180
|
diff,
|
|
1181
|
+
// the comment above each definition, as its author wrote it (structure.docs); never checked
|
|
1182
|
+
docs: ((callees) => Object.fromEntries(Object.entries(st.docs || {}).filter(([id]) => callees.has(id))))(
|
|
1183
|
+
new Set(entries.flatMap((e) => {
|
|
1184
|
+
const ids = [];
|
|
1185
|
+
walk(e.tree, (n) => {
|
|
1186
|
+
if (n.kind === "node" && n.from?.kind === "call") ids.push(n.from.callee);
|
|
1187
|
+
});
|
|
1188
|
+
return ids;
|
|
1189
|
+
}))
|
|
1190
|
+
),
|
|
1191
|
+
// what each table holds, by its checked row type (structure.rows)
|
|
1192
|
+
rows: Object.fromEntries(Object.entries(st.rows || {}).map(([table, schema]) => [table, rowShape(schema)])),
|
|
1088
1193
|
counts: {
|
|
1089
1194
|
entries: entries.length,
|
|
1090
1195
|
described: entries.length - ghosts.length,
|
|
@@ -1098,6 +1203,14 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
1098
1203
|
evidence: { tools: ["api_organize:structure", ...engine.access ? ["api_access_check"] : []], derived: ["diff: base red leaves matched by id, their failed obligations by identity"] }
|
|
1099
1204
|
};
|
|
1100
1205
|
}
|
|
1206
|
+
var typeName = (s) => s.type ? s.type.split("::").pop() : null;
|
|
1207
|
+
var fieldType = (s) => ({ string: "String", int: "Int", float64: "Float64", bool: "Bool", unit: "Unit", json: "Json" })[s.kind] || (s.kind === "list" ? `List[${fieldType(s.element)}]` : s.kind === "unsupported" ? s.rendered : s.kind === "union" && s.variants.length && s.variants.every((v) => !v.fields.length) ? s.variants.map((v) => v.tag).join(" | ") : typeName(s));
|
|
1208
|
+
function rowShape(s) {
|
|
1209
|
+
if (s.kind === "list") return `List[${rowShape(s.element)}]`;
|
|
1210
|
+
if (s.kind === "struct") return `${typeName(s)}(${s.fields.map((f) => `${f.name}: ${fieldType(f)}`).join(", ")})`;
|
|
1211
|
+
if (s.kind === "union") return s.variants.map((v) => v.fields.length ? `${v.tag}(${v.fields.map((f) => `${f.name}: ${fieldType(f)}`).join(", ")})` : v.tag).join(" | ");
|
|
1212
|
+
return fieldType(s);
|
|
1213
|
+
}
|
|
1101
1214
|
function stationLabel(e) {
|
|
1102
1215
|
const verb = e.op === "mint id" ? "mint" : e.op;
|
|
1103
1216
|
if (e.tables.length) return `${verb} \xB7 ${e.tables.join(", ")}`;
|
|
@@ -1152,6 +1265,7 @@ var Step = z3.looseObject({ op: z3.string(), tables: z3.array(z3.string()).defau
|
|
|
1152
1265
|
var Route = z3.looseObject({
|
|
1153
1266
|
path: z3.string(),
|
|
1154
1267
|
handler: z3.string(),
|
|
1268
|
+
handlerBinding: z3.string(),
|
|
1155
1269
|
authority: z3.string().optional(),
|
|
1156
1270
|
tables: z3.array(z3.string()).default([]),
|
|
1157
1271
|
inferredSteps: z3.array(Step).default([])
|
|
@@ -1170,8 +1284,10 @@ var Table = z3.looseObject({
|
|
|
1170
1284
|
});
|
|
1171
1285
|
var Report = z3.looseObject({
|
|
1172
1286
|
surface: z3.looseObject({ routes: z3.array(Route).default([]) }).default({ routes: [] }),
|
|
1173
|
-
dataModel: z3.looseObject({ tables: z3.array(Table).default([]) }).default({ tables: [] })
|
|
1287
|
+
dataModel: z3.looseObject({ tables: z3.array(Table).default([]) }).default({ tables: [] }),
|
|
1288
|
+
shape: z3.looseObject({ packages: z3.array(z3.looseObject({ name: z3.string() })).default([]) }).default({ packages: [] })
|
|
1174
1289
|
});
|
|
1290
|
+
var handlerOf = (report, route) => route.handlerBinding.includes("::") ? route.handlerBinding : `${report.shape.packages[0]?.name}::${route.handlerBinding}`;
|
|
1175
1291
|
var Entry = z3.looseObject({ entry: z3.string(), resources: z3.array(z3.string()).default([]) });
|
|
1176
1292
|
var Structure = z3.looseObject({ entries: z3.array(Entry).default([]) });
|
|
1177
1293
|
var ACCESS_RULES = {
|
|
@@ -1252,11 +1368,36 @@ function accessQuestion(table, names) {
|
|
|
1252
1368
|
ask: `Your app saves "${shown(names, table.name)}"${table.name in names ? "" : " records"}. Who should be able to see one, and who should be able to change it?`,
|
|
1253
1369
|
why: "Without a rule the app has no way to know a record is private. Yichus checks every place the app reads or changes these records against the rule you choose.",
|
|
1254
1370
|
choices: [
|
|
1255
|
-
{
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1371
|
+
{
|
|
1372
|
+
id: "owner",
|
|
1373
|
+
label: "Only the person who created it",
|
|
1374
|
+
means: "Every read and write is tied to the signed-in person, and the check proves no one reaches another person's records. No one else, not even a manager, sees or changes them, so anything someone else decides about them, such as an approval, belongs in records only the person who decides it may change.",
|
|
1375
|
+
unchecked: "Decisions someone else should make about these records, such as an approval: the person who creates one can change all of it."
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
id: "shared",
|
|
1379
|
+
label: "Only the people a rule lets in",
|
|
1380
|
+
means: "For example the members of a team, or the people a record was shared with. Every read and change goes through that rule, except work done under a role you name, such as an administrator or the app's own server, and the check proves nothing else skips it.",
|
|
1381
|
+
unchecked: "Whether the rule lets in the right people. The check proves every read and change goes through it; who it lets in is what your agent wrote."
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
id: "server-only",
|
|
1385
|
+
label: "No person, only the app's own server",
|
|
1386
|
+
means: "Records the app keeps for itself, such as which number belongs to whom or which messages it has already seen. Only work the app's own server starts may read or change them, and the check proves nothing a person calls reaches them.",
|
|
1387
|
+
unchecked: "What the app's own server does with these records."
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
id: "everyone-reads",
|
|
1391
|
+
label: "Everyone can see it; only certain roles can change it",
|
|
1392
|
+
means: "Anyone can read; changes need a role the app gives out.",
|
|
1393
|
+
unchecked: "Who is given the role: that is up to sign-in."
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
id: "public",
|
|
1397
|
+
label: "Anyone, even without signing in",
|
|
1398
|
+
means: "Nothing in these records is private.",
|
|
1399
|
+
unchecked: "Nothing protects these records: anyone, even without signing in, can read, change and delete every one."
|
|
1400
|
+
}
|
|
1260
1401
|
],
|
|
1261
1402
|
fact: `The program stores rows in "${table.name}" (policy declared: ${table.policy ?? "none"}).`
|
|
1262
1403
|
};
|
|
@@ -1270,8 +1411,18 @@ function togetherQuestion(table, handlers, names) {
|
|
|
1270
1411
|
ask: `Two requests change the same record in "${shown(names, table.name)}" at the same moment (${who}). What should happen?`,
|
|
1271
1412
|
why: "When a request reads a record and then writes it back, a second request in between can be silently lost, or both can pass a limit only one should, such as two people taking the last spot. People rarely notice until data goes missing.",
|
|
1272
1413
|
choices: [
|
|
1273
|
-
{
|
|
1274
|
-
|
|
1414
|
+
{
|
|
1415
|
+
id: "keep-both",
|
|
1416
|
+
label: "Both changes are kept",
|
|
1417
|
+
means: "Each change happens as one indivisible step that sees the one before it, so neither can overwrite the other unseen and a limit the app checks is never passed twice; the check proves no change is lost.",
|
|
1418
|
+
unchecked: "A limit counted across many records, such as sign-ups against the spots on a shift. A rule you give can check that, with a scenario that runs every action that changes the records the rule is about."
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
id: "refuse-second",
|
|
1422
|
+
label: "The second is refused and they try again",
|
|
1423
|
+
means: "A change made to an out-of-date copy is turned away; the check proves no change is lost.",
|
|
1424
|
+
unchecked: "A limit counted across many records, such as sign-ups against the spots on a shift. A rule you give can check that, with a scenario that runs every action that changes the records the rule is about."
|
|
1425
|
+
}
|
|
1275
1426
|
],
|
|
1276
1427
|
fact: `${handlers.join(", ")} ${handlers.length === 1 ? "reads" : "read"} "${table.name}" and then ${handlers.length === 1 ? "writes" : "write"} it.`
|
|
1277
1428
|
};
|
|
@@ -1284,7 +1435,12 @@ function partialQuestion(route, tables, names) {
|
|
|
1284
1435
|
ask: `If the server stops halfway through "${shown(names, route.path)}" (a crash, a database error, a timeout), may part of its changes stay saved?`,
|
|
1285
1436
|
why: `This action changes ${tables.map((t) => `"${shown(names, t)}"`).join(" and ")} in more than one step. A stop between steps can leave them disagreeing, for example money taken but no receipt.`,
|
|
1286
1437
|
choices: [
|
|
1287
|
-
{
|
|
1438
|
+
{
|
|
1439
|
+
id: "all-or-nothing",
|
|
1440
|
+
label: "All or nothing",
|
|
1441
|
+
means: "Every change this action makes is saved together or not at all; Yichus stops it at each step and checks nothing half-done stays saved.",
|
|
1442
|
+
unchecked: "Requests and starting records the failure scenario does not list."
|
|
1443
|
+
},
|
|
1288
1444
|
{ id: "partial-ok", label: "Partial is fine", means: "No check is added for this action." }
|
|
1289
1445
|
],
|
|
1290
1446
|
fact: `${route.handler} writes ${tables.map((t) => `"${t}"`).join(", ")} in more than one step.`
|
|
@@ -1299,13 +1455,26 @@ function outsideQuestion(route, kinds, names) {
|
|
|
1299
1455
|
ask: `"${shown(names, route.path)}" calls ${service}. If that service is down or slow, what should the person using your app see?`,
|
|
1300
1456
|
why: "Outside services fail often. Without a plan, a failure can leave a half-finished record or charge for work that never happened.",
|
|
1301
1457
|
choices: [
|
|
1302
|
-
{
|
|
1303
|
-
|
|
1458
|
+
{
|
|
1459
|
+
id: "fail-clean",
|
|
1460
|
+
label: "An error, with nothing saved",
|
|
1461
|
+
means: "Yichus makes the service fail and checks that nothing from this action stays saved.",
|
|
1462
|
+
unchecked: "Requests, starting records and answers from the service that the failure scenario does not list."
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
id: "finish-later",
|
|
1466
|
+
label: "Save the request and finish it later",
|
|
1467
|
+
means: "Yichus makes the service fail and checks the saved request is tried again and finished exactly once.",
|
|
1468
|
+
unchecked: "Requests, starting records and answers from the service that the failure scenario does not list."
|
|
1469
|
+
},
|
|
1304
1470
|
{ id: "no-plan", label: "No preference", means: "No check is added for this action." }
|
|
1305
1471
|
],
|
|
1306
1472
|
fact: `${route.handler} reaches ${kinds.join(" and ")}.`
|
|
1307
1473
|
};
|
|
1308
1474
|
}
|
|
1475
|
+
var writeSteps = (route) => route.inferredSteps.filter((s) => s.resource === "db" && s.op !== "read" && s.op !== "query");
|
|
1476
|
+
var touchedBy = (route) => [.../* @__PURE__ */ new Set([...route.tables, ...route.inferredSteps.filter((s) => s.resource === "db").flatMap((s) => s.tables)])];
|
|
1477
|
+
var changedBy = (route) => [...new Set(writeSteps(route).flatMap((s) => s.tables))];
|
|
1309
1478
|
var PERSON_SIGNS_IN = /* @__PURE__ */ new Set(["authenticated", "publishable", "requires-roles"]);
|
|
1310
1479
|
var signIn = {
|
|
1311
1480
|
id: "sign-in",
|
|
@@ -1344,9 +1513,7 @@ function questionsFor(report, structure, names = {}) {
|
|
|
1344
1513
|
if (both.length) questions.push(togetherQuestion(table, both, names));
|
|
1345
1514
|
}
|
|
1346
1515
|
for (const route of report.surface.routes) {
|
|
1347
|
-
|
|
1348
|
-
const tables = [...new Set(writes.flatMap((s) => s.tables))];
|
|
1349
|
-
if (writes.length >= 2) questions.push(partialQuestion(route, tables, names));
|
|
1516
|
+
if (writeSteps(route).length >= 2) questions.push(partialQuestion(route, changedBy(route), names));
|
|
1350
1517
|
}
|
|
1351
1518
|
for (const route of report.surface.routes) {
|
|
1352
1519
|
const entry = structure.entries.find((e) => e.entry === `route:${route.path}`);
|
|
@@ -1574,7 +1741,7 @@ var LawCheck = z6.looseObject({
|
|
|
1574
1741
|
notSampled: z6.array(z6.looseObject({ binding: z6.string() })).optional(),
|
|
1575
1742
|
reach: z6.array(z6.looseObject({ binding: z6.string(), usedBy: z6.array(z6.string()) })).optional()
|
|
1576
1743
|
});
|
|
1577
|
-
var caseFile = (root, questionId) => path3.join(studioDir(root), "cases", `${questionId.replace(/[
|
|
1744
|
+
var caseFile = (root, questionId) => path3.join(studioDir(root), "cases", `${questionId.replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-|-$/g, "")}.json`);
|
|
1578
1745
|
var answerOf = (requirements, id) => requirements.answers.find((a) => a.question === id) ?? null;
|
|
1579
1746
|
function instancesFor(requirements) {
|
|
1580
1747
|
const choice = answerOf(requirements, "topology")?.choice;
|
|
@@ -1627,6 +1794,7 @@ async function judge(root, engine, sources, question, saved, report, verify, tog
|
|
|
1627
1794
|
evidence: personal.map((r) => `${r.path} reaches "${question.subject}" and is called ${r.authority === "requires-roles" ? "by a person with a role" : "by a person"}, not by the app's own server.`)
|
|
1628
1795
|
};
|
|
1629
1796
|
}
|
|
1797
|
+
if (answer.choice === "public") return { ...base, status: "no-check", check };
|
|
1630
1798
|
const prop = byId(`access:${question.subject}`) ?? byId("access-coverage");
|
|
1631
1799
|
if (!prop) return { ...base, status: "inconclusive", check: "api_verify", evidence: ["api_verify reported no access property for this table."] };
|
|
1632
1800
|
return {
|
|
@@ -1652,35 +1820,47 @@ async function judge(root, engine, sources, question, saved, report, verify, tog
|
|
|
1652
1820
|
case "partial":
|
|
1653
1821
|
case "outside": {
|
|
1654
1822
|
if (answer.choice === "partial-ok" || answer.choice === "no-plan") return { ...base, status: "no-check" };
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
if (!fs3.existsSync(file)) return { ...base, casePath: rel, status: "needs-case", check: "api_protocol_check", evidence: [`No protocol case at ${rel} yet.`] };
|
|
1658
|
-
let protocolCase;
|
|
1659
|
-
try {
|
|
1660
|
-
protocolCase = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
1661
|
-
} catch (error) {
|
|
1662
|
-
return {
|
|
1663
|
-
...base,
|
|
1664
|
-
casePath: rel,
|
|
1665
|
-
check: "api_protocol_check",
|
|
1666
|
-
status: "inconclusive",
|
|
1667
|
-
evidence: [`${rel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`]
|
|
1668
|
-
};
|
|
1669
|
-
}
|
|
1670
|
-
const result = Protocol.parse(await engine.call("api_protocol_check", { sources: [...sources, ...caseSources(root)], case: protocolCase }));
|
|
1671
|
-
const status = result.protocol?.status;
|
|
1672
|
-
const check = `api_protocol_check ${rel}`;
|
|
1673
|
-
if (!status) return { ...base, casePath: rel, check, status: "inconclusive", evidence: [result.error ?? "The protocol check gave no status."] };
|
|
1674
|
-
return {
|
|
1675
|
-
...base,
|
|
1676
|
-
casePath: rel,
|
|
1677
|
-
check,
|
|
1678
|
-
evidence: [`${result.protocol?.name ?? rel}: ${status}`],
|
|
1679
|
-
status: status === "holds" ? "bounded" : status === "violated" ? "broken" : "inconclusive"
|
|
1680
|
-
};
|
|
1823
|
+
const ran = await runCase(root, engine, sources, question.id);
|
|
1824
|
+
return { ...base, casePath: ran.casePath, check: ran.check, status: ran.status, evidence: ran.evidence };
|
|
1681
1825
|
}
|
|
1682
1826
|
}
|
|
1683
1827
|
}
|
|
1828
|
+
var CaseRequests = z6.looseObject({ requests: z6.array(z6.looseObject({ call: z6.looseObject({ binding: z6.string() }) })) });
|
|
1829
|
+
async function runCase(root, engine, sources, id, runs = []) {
|
|
1830
|
+
const file = caseFile(root, id);
|
|
1831
|
+
const casePath = path3.relative(root, file).split(path3.sep).join("/");
|
|
1832
|
+
if (!fs3.existsSync(file)) return { casePath, status: "needs-case", check: "api_protocol_check", evidence: [`No protocol case at ${casePath} yet.`] };
|
|
1833
|
+
let protocolCase;
|
|
1834
|
+
try {
|
|
1835
|
+
protocolCase = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
return {
|
|
1838
|
+
casePath,
|
|
1839
|
+
check: "api_protocol_check",
|
|
1840
|
+
status: "inconclusive",
|
|
1841
|
+
evidence: [`${casePath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`]
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
const called = CaseRequests.safeParse(protocolCase);
|
|
1845
|
+
const bindings = called.success ? called.data.requests.map((r) => r.call.binding) : [];
|
|
1846
|
+
const missing = runs.filter((r) => !bindings.includes(r.binding));
|
|
1847
|
+
if (missing.length) return {
|
|
1848
|
+
casePath,
|
|
1849
|
+
check: "api_protocol_check",
|
|
1850
|
+
status: "needs-case",
|
|
1851
|
+
evidence: missing.map((r) => `${casePath} sends no request to ${r.path} (${r.binding}) yet.`)
|
|
1852
|
+
};
|
|
1853
|
+
const result = Protocol.parse(await engine.call("api_protocol_check", { sources: [...sources, ...caseSources(root)], case: protocolCase }));
|
|
1854
|
+
const status = result.protocol?.status;
|
|
1855
|
+
const check = `api_protocol_check ${casePath}`;
|
|
1856
|
+
if (!status) return { casePath, check, status: "inconclusive", evidence: [result.error ?? "The protocol check gave no status."] };
|
|
1857
|
+
return {
|
|
1858
|
+
casePath,
|
|
1859
|
+
check,
|
|
1860
|
+
evidence: [`${result.protocol?.name ?? casePath}: ${status}`],
|
|
1861
|
+
status: status === "holds" ? "bounded" : status === "violated" ? "broken" : "inconclusive"
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1684
1864
|
var reason = (result) => typeof result.error === "string" ? result.error : "no reason given";
|
|
1685
1865
|
async function checkLaws(engine, sources) {
|
|
1686
1866
|
const answer = async (mode) => {
|
|
@@ -1719,14 +1899,27 @@ async function checkLaws(engine, sources) {
|
|
|
1719
1899
|
}
|
|
1720
1900
|
var lawNamed = (laws, ref) => laws.find((l) => l.binding === ref || l.binding.endsWith(`::${ref}`) || l.name === ref);
|
|
1721
1901
|
var unused = (law) => law.usedBy !== null && !law.usedBy.length;
|
|
1722
|
-
function judgeRule(rule, laws) {
|
|
1902
|
+
async function judgeRule(rule, laws, report, root, engine, sources) {
|
|
1723
1903
|
const found = rule.laws.map((ref) => [ref, lawNamed(laws, ref)]);
|
|
1724
1904
|
const missing = found.filter(([, law]) => !law).map(([ref]) => ref);
|
|
1725
1905
|
const checked = found.flatMap(([, law]) => law ? [law] : []);
|
|
1726
1906
|
const evidence = [...missing.map((ref) => `No law named ${ref} in the program.`), ...checked.flatMap((l) => l.evidence.map((e) => `${l.name ?? l.binding}: ${e}`))];
|
|
1727
1907
|
const status = missing.length || !checked.length ? "needs-law" : checked.some((l) => l.status === "broken") ? "broken" : checked.some((l) => l.status === "inconclusive") ? "inconclusive" : checked.some(unused) ? "unused" : checked.every((l) => l.status === "proven") ? "proven" : "bounded";
|
|
1728
|
-
return { ...rule, status, evidence };
|
|
1729
|
-
|
|
1908
|
+
if (status !== "proven" && status !== "bounded") return { ...rule, status, evidence };
|
|
1909
|
+
const reachKnown = checked.every((l) => l.usedBy !== null);
|
|
1910
|
+
const own = new Set(checked.flatMap((l) => l.usedBy ?? []).flatMap((entry) => entry.startsWith("route:") ? [entry.slice("route:".length)] : []));
|
|
1911
|
+
const routes = report.surface.routes;
|
|
1912
|
+
const records = new Set(reachKnown ? routes.filter((r) => own.has(r.path)).flatMap(touchedBy) : routes.flatMap(changedBy));
|
|
1913
|
+
const others = routes.filter((r) => !own.has(r.path) && changedBy(r).some((t) => records.has(t)));
|
|
1914
|
+
if (!others.length) return { ...rule, status, evidence };
|
|
1915
|
+
const runs = routes.filter((r) => own.has(r.path) || others.includes(r)).map((r) => ({ path: r.path, handler: r.handler, binding: handlerOf(report, r) }));
|
|
1916
|
+
const ran = await runCase(root, engine, sources, `rule:${rule.rule}`, runs);
|
|
1917
|
+
const across = { casePath: ran.casePath, actions: runs.map((r) => r.path), others: others.map((r) => r.path) };
|
|
1918
|
+
const changed = [...records].filter((t) => others.some((r) => changedBy(r).includes(t))).map((t) => `"${t}"`).join(", ");
|
|
1919
|
+
const changing = reachKnown ? `${others.map((r) => r.path).join(", ")} ${others.length === 1 ? "changes" : "change"} ${changed} without deciding with the rule's laws.` : `Which actions decide with the rule's laws is not known, so its scenario runs every action that changes ${changed}.`;
|
|
1920
|
+
return { ...rule, across, evidence: [...evidence, changing, ...ran.evidence], status: ran.status };
|
|
1921
|
+
}
|
|
1922
|
+
var keepRule = (actions) => `every one of ${actions.join(", ")} keep the rule, deciding with the same functions as its laws. Change the code, never the scenario's rule.`;
|
|
1730
1923
|
var USE_THE_LAW = "If the function the law checks is in the program, make the action the rule is about decide with it. If it is defined next to the law, move it into the program, have the law check it there, and make the action decide with it. Never weaken what the law says.";
|
|
1731
1924
|
var QUESTION_PROPERTIES = (id) => id.startsWith("access:") || id === "access-coverage" || id === "rmw-transactions" || id === "integrity";
|
|
1732
1925
|
var GUIDE = {
|
|
@@ -1735,6 +1928,12 @@ var GUIDE = {
|
|
|
1735
1928
|
partial: "Transaction protocols: finite cases, retries, and receipts",
|
|
1736
1929
|
outside: "Transaction protocols: finite cases, retries, and receipts"
|
|
1737
1930
|
};
|
|
1931
|
+
var DIRECTED_TOOLS = {
|
|
1932
|
+
addRule: "api_add_rule",
|
|
1933
|
+
deploy: "api_deploy",
|
|
1934
|
+
suggestProperties: "api_suggest_properties",
|
|
1935
|
+
protocolCheck: "api_protocol_check"
|
|
1936
|
+
};
|
|
1738
1937
|
var SCAFFOLD_ARGS = {
|
|
1739
1938
|
package_name: "My/App/Api",
|
|
1740
1939
|
service_name: "my_app",
|
|
@@ -1831,7 +2030,7 @@ function nextStep(state, compileErrors, say2 = MCP) {
|
|
|
1831
2030
|
instructions: [
|
|
1832
2031
|
...mismatched.map((p) => {
|
|
1833
2032
|
const rule = ACCESS_RULES[p.answer.choice];
|
|
1834
|
-
return `Make the AccessSpec rule for "${p.question.subject}" ${rule.write}${p.answer.choice === "server-only" ? ", and reach it only from worker routes" : ""} (${say2.api(
|
|
2033
|
+
return `Make the AccessSpec rule for "${p.question.subject}" ${rule.write}${p.answer.choice === "server-only" ? ", and reach it only from worker routes" : ""} (${say2.api(DIRECTED_TOOLS.addRule)} for the snippet; ${say2.api("api_guide", { section: rule.guide })} for what it means).`;
|
|
1835
2034
|
}),
|
|
1836
2035
|
next
|
|
1837
2036
|
],
|
|
@@ -1840,7 +2039,7 @@ function nextStep(state, compileErrors, say2 = MCP) {
|
|
|
1840
2039
|
const broken = state.properties.filter((p) => p.status === "broken" || p.status === "inconclusive");
|
|
1841
2040
|
const failing = state.other.filter((o) => o.status !== "warning");
|
|
1842
2041
|
const warnings = state.other.filter((o) => o.status === "warning");
|
|
1843
|
-
const brokenRules = state.rules.filter((r) => r.status !== "proven" && r.status !== "bounded");
|
|
2042
|
+
const brokenRules = state.rules.filter((r) => r.status !== "proven" && r.status !== "bounded" && r.status !== "needs-case");
|
|
1844
2043
|
const ruled = new Set(state.rules.flatMap((r) => r.laws.flatMap((ref) => lawNamed(state.laws, ref)?.binding ?? [])));
|
|
1845
2044
|
const brokenLaws = state.laws.filter((l) => !ruled.has(l.binding) && (l.status === "broken" || l.status === "inconclusive" || unused(l)));
|
|
1846
2045
|
const notHolding = broken.length + failing.length + brokenRules.length + brokenLaws.length + (state.lawsError ? 1 : 0);
|
|
@@ -1855,7 +2054,7 @@ function nextStep(state, compileErrors, say2 = MCP) {
|
|
|
1855
2054
|
...broken.map((p) => `"${p.question.ask}" The person chose "${p.question.choices.find((c) => c.id === p.answer?.choice)?.label}".${GUIDE[p.question.kind] ? ` ${capital(say2.api("api_guide", { section: GUIDE[p.question.kind] }))} for how.` : ""}`),
|
|
1856
2055
|
...failing.map((o) => `${o.title} (${o.id}) is ${o.status}.`),
|
|
1857
2056
|
...warnings.map((o) => `${o.title} (${o.id}) is a warning, not a failure: read its evidence. Change the code if it shows something the person did not choose, such as a stored field an action gives away; otherwise tell the person what it means.`),
|
|
1858
|
-
...brokenRules.map((r) => r.status === "needs-law" ? `The person's rule "${r.rule}" has no law in the program that checks it. Write one (${say2.reference("laws")} for how), then record the rules again with ${say2.name("yichus_rules")}.` : r.status === "unused" ? `The person's rule "${r.rule}" is checked by a law whose check calls nothing any action of the app reaches, so the law says nothing about what the app's actions do. ${USE_THE_LAW}` : `The person's rule "${r.rule}" is ${r.status === "broken" ? "broken: a law that states it fails" : "not decided by its laws"}. Change the code, never the law.`),
|
|
2057
|
+
...brokenRules.map((r) => r.status === "needs-law" ? `The person's rule "${r.rule}" has no law in the program that checks it. Write one (${say2.reference("laws")} for how), then record the rules again with ${say2.name("yichus_rules")}.` : r.status === "unused" ? `The person's rule "${r.rule}" is checked by a law whose check calls nothing any action of the app reaches, so the law says nothing about what the app's actions do. ${USE_THE_LAW}` : r.across ? r.status === "broken" ? `The person's rule "${r.rule}" is broken: its laws pass, and the scenario that runs every action that changes its records fails. Make ${keepRule(r.across.actions)}` : `The person's rule "${r.rule}" is not decided by the scenario that runs every action that changes its records. If the evidence shows ${r.across.casePath} itself is refused, fix that file, never its rule; otherwise make ${keepRule(r.across.actions)}` : `The person's rule "${r.rule}" is ${r.status === "broken" ? "broken: a law that states it fails" : "not decided by its laws"}. Change the code, never the law.`),
|
|
1859
2058
|
...brokenLaws.map((l) => `The law ${l.name ? `"${l.name}" (${l.binding})` : l.binding} ${l.status === "broken" ? "is broken. Change the code, never the law." : l.status === "inconclusive" ? "is not decided. Change the code, never the law." : `checks code no action of the app reaches. ${USE_THE_LAW}`}`),
|
|
1860
2059
|
...state.lawsError ? ["The engine could not check the program's laws, so no rule the person stated is checked. If the error below points at the program, fix the program; otherwise tell the person their rules are not checked yet."] : [],
|
|
1861
2060
|
`Never weaken a rule, delete a check, a law or a claim, or declare one server to make a check pass: change the code. ${next}`
|
|
@@ -1914,13 +2113,15 @@ function nextStep(state, compileErrors, say2 = MCP) {
|
|
|
1914
2113
|
]
|
|
1915
2114
|
};
|
|
1916
2115
|
const cases = state.properties.filter((p) => p.status === "needs-case");
|
|
1917
|
-
|
|
2116
|
+
const ruleCases = state.rules.flatMap((r) => r.status === "needs-case" && r.across ? [{ rule: r.rule, ...r.across }] : []);
|
|
2117
|
+
if (cases.length || ruleCases.length) return {
|
|
1918
2118
|
stage: "write-case",
|
|
1919
|
-
summary: "Writing the failure scenarios that test your answers.",
|
|
2119
|
+
summary: cases.length ? "Writing the failure scenarios that test your answers." : "Writing a scenario that runs every action that changes the records your rules are about.",
|
|
1920
2120
|
instructions: [
|
|
1921
2121
|
`${capital(say2.reference("protocol-case"))}: it gives the case specification and a complete worked case with its program.`,
|
|
1922
2122
|
"Put any Bosatsu observation predicates the case names in .yichus/cases/*.bosatsu. The studio adds those files to protocol checks only, so they never count as part of the app.",
|
|
1923
2123
|
...cases.map((p) => `Write ${p.casePath}: a protocol-case-1.0 for ${p.question.subject} whose invariants state the person's choice ("${p.question.choices.find((c) => c.id === p.answer?.choice)?.label}"), with crashes${p.question.kind === "outside" ? " and failed external results" : ""} at each step.`),
|
|
2124
|
+
...ruleCases.map((c) => `Write ${c.casePath}: a protocol-case-1.0 for the person's rule "${c.rule}". ${c.others.join(", ")} ${c.others.length === 1 ? "changes" : "change"} the same records as the actions that decide with the rule, so the case sends a request to every one of ${c.actions.join(", ")}, from starting records where the rule matters (one spot left, say), and its invariant states the rule over the records after every step: for a limit, what each record counts matches the records it counts, and never passes the limit. Write the observation predicate from the rule, not from the code.`),
|
|
1924
2125
|
`State the person's rule exactly; never weaken it to match the code. ${next}`
|
|
1925
2126
|
]
|
|
1926
2127
|
};
|
|
@@ -1934,7 +2135,7 @@ function nextStep(state, compileErrors, say2 = MCP) {
|
|
|
1934
2135
|
...assumed.length ? ["Say plainly which results depend on running exactly one server."] : [],
|
|
1935
2136
|
...data === "move" || data === "same-database" ? [`Before any record moves, list for the person every kind of record the old place keeps that this app does not (for example timestamps or a message log), and ask about each: add it to the app, or agree to leave it behind. ${capital(say2.reference("porting"))} says how to bring the data over.`] : [],
|
|
1936
2137
|
`When they describe a new feature, change the code and ${say2.use("yichus_next")} again: new facts can raise new questions.`,
|
|
1937
|
-
sections.length ? `When they want the app online, follow the ${sectionsText(sections)} section${sections.length === 1 ? "" : "s"} of the deploy reference (${say2.reference("deploy")}), and ${say2.api(
|
|
2138
|
+
sections.length ? `When they want the app online, follow the ${sectionsText(sections)} section${sections.length === 1 ? "" : "s"} of the deploy reference (${say2.reference("deploy")}), and ${say2.api(DIRECTED_TOOLS.deploy)} to generate the server.` : `The program keeps no records and has no actions people call, so it needs no server. When they want it on a page, ${say2.reference("calculator")}.`
|
|
1938
2139
|
]
|
|
1939
2140
|
};
|
|
1940
2141
|
}
|
|
@@ -2002,11 +2203,12 @@ async function direct({ root, engine, requirements, withMap = true, previous = n
|
|
|
2002
2203
|
const checked = await checkLaws(engine, sources);
|
|
2003
2204
|
base.laws = checked.laws;
|
|
2004
2205
|
base.lawsError = checked.error;
|
|
2005
|
-
|
|
2206
|
+
for (const rule of requirements.rules ?? []) base.rules.push(await judgeRule(rule, base.laws, report, root, engine, sources));
|
|
2006
2207
|
base.hasActions = report.surface.routes.length > 0;
|
|
2208
|
+
let access = null;
|
|
2007
2209
|
if (base.hasActions || requirements.scenarios?.length) {
|
|
2008
2210
|
try {
|
|
2009
|
-
|
|
2211
|
+
access = await call(engine, "api_access_check", { sources });
|
|
2010
2212
|
const record = WhatHappens.parse(buildWhatHappens({ structure: structureRaw, access }, { program: base.project }));
|
|
2011
2213
|
base.whatHappens = base.hasActions ? record : { ...record, entries: [], ghosts: [], claims: [], witnesses: [], refused: [], notes: [] };
|
|
2012
2214
|
} catch (error) {
|
|
@@ -2023,7 +2225,7 @@ async function direct({ root, engine, requirements, withMap = true, previous = n
|
|
|
2023
2225
|
await call(engine, "api_organize", { sources, lens: "abstractions" }),
|
|
2024
2226
|
await call(engine, "api_organize", { sources, lens: "layers" })
|
|
2025
2227
|
];
|
|
2026
|
-
base.map = buildSystemMap({ report, structure: structureRaw, abstractions, layers }, { program: base.project });
|
|
2228
|
+
base.map = buildSystemMap({ report, structure: structureRaw, abstractions, layers, ...access ? { access } : {} }, { program: base.project });
|
|
2027
2229
|
base.layers = layers;
|
|
2028
2230
|
} catch (error) {
|
|
2029
2231
|
base.mapError = error instanceof Error ? error.message : String(error);
|
|
@@ -2050,6 +2252,7 @@ function readState(root) {
|
|
|
2050
2252
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2051
2253
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2052
2254
|
import { z as z10 } from "zod";
|
|
2255
|
+
import { OpenFamilies } from "@yichus/api-mcp";
|
|
2053
2256
|
|
|
2054
2257
|
// src/studio/marks.ts
|
|
2055
2258
|
import fs4 from "fs";
|
|
@@ -2202,6 +2405,7 @@ function recordMarks(root, args, now) {
|
|
|
2202
2405
|
}
|
|
2203
2406
|
|
|
2204
2407
|
// src/studio/page.ts
|
|
2408
|
+
var HOLDING = ["proven", "bounded", "assumed", "no-check"];
|
|
2205
2409
|
var PAGE_STATUS = {
|
|
2206
2410
|
unanswered: ["waiting for your answer", "quiet"],
|
|
2207
2411
|
decided: ["decided", "good"],
|
|
@@ -2217,6 +2421,9 @@ var PAGE_STATUS = {
|
|
|
2217
2421
|
"no-check": ["no check needed", "quiet"],
|
|
2218
2422
|
pending: ["answered, waiting for your agent to check", "quiet"]
|
|
2219
2423
|
};
|
|
2424
|
+
function ruleUnchecked(across) {
|
|
2425
|
+
return `Anything in your words that its laws do not state: your agent wrote the laws from your words.${across ? " Across actions, only the starting records and requests its scenario lists." : ""}`;
|
|
2426
|
+
}
|
|
2220
2427
|
var PAGE_HEADINGS = {
|
|
2221
2428
|
asked: "What you asked for",
|
|
2222
2429
|
now: "What is happening now",
|
|
@@ -2366,6 +2573,7 @@ import { mount } from './system-map.js';
|
|
|
2366
2573
|
import { mountSections, mountWhatHappens, reportViewing, showMarks } from './studio-views.js';
|
|
2367
2574
|
|
|
2368
2575
|
const STATUS = ${JSON.stringify(PAGE_STATUS)};
|
|
2576
|
+
const HOLDING = ${JSON.stringify(HOLDING)};
|
|
2369
2577
|
const NO_ACTIONS = ${JSON.stringify(NO_ACTIONS)};
|
|
2370
2578
|
const el = (tag, props = {}, ...kids) => { const n = Object.assign(document.createElement(tag), props); n.append(...kids); return n; };
|
|
2371
2579
|
let mapHash = null, drawn = '';
|
|
@@ -2400,7 +2608,9 @@ function questionCard(q, saved) {
|
|
|
2400
2608
|
function ruleRow(r) {
|
|
2401
2609
|
const [words, tone] = STATUS[r.status];
|
|
2402
2610
|
const li = el('li', {}, el('span', { textContent: 'A rule you gave: ' }, el('em', { textContent: r.rule })), el('span', { className: 'chip ' + tone, textContent: words }));
|
|
2403
|
-
li.append(el('span', { className: 'choice-said', textContent: 'Checked with ' + (r.laws === 1 ? '1 law' : r.laws + ' laws') + ' your agent wrote
|
|
2611
|
+
li.append(el('span', { className: 'choice-said', textContent: 'Checked with ' + (r.laws === 1 ? '1 law' : r.laws + ' laws') + ' your agent wrote'
|
|
2612
|
+
+ (r.across ? ', and a scenario that runs every action that changes these records.' : '.') }));
|
|
2613
|
+
if (r.unchecked) li.append(el('span', { className: 'choice-said', textContent: 'Not checked: ' + r.unchecked }));
|
|
2404
2614
|
return li;
|
|
2405
2615
|
}
|
|
2406
2616
|
|
|
@@ -2412,6 +2622,8 @@ function propertyRow(p, saved) {
|
|
|
2412
2622
|
const li = el('li', {}, el('span', { textContent: p.question.ask }), el('span', { className: 'chip ' + tone, textContent: words }));
|
|
2413
2623
|
li.dataset.property = p.question.id;
|
|
2414
2624
|
li.append(el('span', { className: 'choice-said', textContent: 'You chose: ' + label + (answer.words && answer.words !== label ? ' ("' + answer.words + '")' : '') }));
|
|
2625
|
+
const unchecked = !stale && HOLDING.includes(p.status) && p.question.choices.find(c => c.id === answer.choice)?.unchecked;
|
|
2626
|
+
if (unchecked) li.append(el('span', { className: 'choice-said', textContent: 'Not checked: ' + unchecked }));
|
|
2415
2627
|
return li;
|
|
2416
2628
|
}
|
|
2417
2629
|
|
|
@@ -2419,7 +2631,8 @@ async function refresh() {
|
|
|
2419
2631
|
const { state, requirements } = await (await fetch('/state')).json();
|
|
2420
2632
|
const savedOf = id => requirements.answers.find(a => a.question === id);
|
|
2421
2633
|
if (requirements.description) document.getElementById('description').textContent = requirements.description;
|
|
2422
|
-
|
|
2634
|
+
// Before the first check there is nothing drawn, but the person may already be looking.
|
|
2635
|
+
if (!state) { viewing(null); return; }
|
|
2423
2636
|
document.getElementById('project').textContent = ': ' + state.project;
|
|
2424
2637
|
document.getElementById('updated').textContent = 'Last checked ' + new Date(state.updatedAt).toLocaleTimeString() + '.';
|
|
2425
2638
|
document.getElementById('summary').textContent = state.summary;
|
|
@@ -2488,7 +2701,14 @@ var Report2 = z9.object({
|
|
|
2488
2701
|
/** The drawing the anchors belong to (person.ts), or null before one is drawn. */
|
|
2489
2702
|
drawing: z9.string().max(64).nullable()
|
|
2490
2703
|
}).strict();
|
|
2491
|
-
var Viewing = Report2.extend({
|
|
2704
|
+
var Viewing = Report2.extend({
|
|
2705
|
+
schemaVersion: z9.literal("yichus-viewing-1"),
|
|
2706
|
+
at: z9.string(),
|
|
2707
|
+
/** The page server the report came through: only it can tell that its page went. */
|
|
2708
|
+
server: z9.string().optional(),
|
|
2709
|
+
/** When the last copy of the page open on that server went away, until a page connects again. */
|
|
2710
|
+
closed: z9.string().optional()
|
|
2711
|
+
}).strict();
|
|
2492
2712
|
var viewingPath = (root) => path5.join(studioDir(root), "viewing.json");
|
|
2493
2713
|
function loadViewing(root) {
|
|
2494
2714
|
const file = viewingPath(root);
|
|
@@ -2497,14 +2717,28 @@ function loadViewing(root) {
|
|
|
2497
2717
|
if (!parsed.success) throw new Error(`${file} is not a yichus-viewing-1 file: ${z9.prettifyError(parsed.error)}`);
|
|
2498
2718
|
return parsed.data;
|
|
2499
2719
|
}
|
|
2500
|
-
function
|
|
2501
|
-
const parsed = Report2.safeParse(report);
|
|
2502
|
-
if (!parsed.success) return { ok: false, error: `The page's report is not one this studio reads. ${z9.prettifyError(parsed.error)}` };
|
|
2720
|
+
function writeViewing(root, viewing) {
|
|
2503
2721
|
ensureStudioDir(root);
|
|
2504
2722
|
const file = viewingPath(root), written = `${file}.${process.pid}.tmp`;
|
|
2505
|
-
fs5.writeFileSync(written, `${JSON.stringify(Viewing.parse(
|
|
2723
|
+
fs5.writeFileSync(written, `${JSON.stringify(Viewing.parse(viewing), null, 2)}
|
|
2506
2724
|
`);
|
|
2507
2725
|
fs5.renameSync(written, file);
|
|
2726
|
+
}
|
|
2727
|
+
function recordClosed(root, server, now) {
|
|
2728
|
+
const last = loadViewing(root);
|
|
2729
|
+
if (last?.server === server) writeViewing(root, { ...last, closed: now.toISOString() });
|
|
2730
|
+
}
|
|
2731
|
+
function recordOpened(root, server) {
|
|
2732
|
+
const last = loadViewing(root);
|
|
2733
|
+
if (last?.server === server && last.closed) {
|
|
2734
|
+
const { closed: _, ...open } = last;
|
|
2735
|
+
writeViewing(root, open);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
function recordViewing(root, server, report, now) {
|
|
2739
|
+
const parsed = Report2.safeParse(report);
|
|
2740
|
+
if (!parsed.success) return { ok: false, error: `The page's report is not one this studio reads. ${z9.prettifyError(parsed.error)}` };
|
|
2741
|
+
writeViewing(root, { schemaVersion: "yichus-viewing-1", at: now.toISOString(), server, ...parsed.data });
|
|
2508
2742
|
return { ok: true };
|
|
2509
2743
|
}
|
|
2510
2744
|
|
|
@@ -2552,13 +2786,16 @@ function pageText(root, pageUrl, now, surface) {
|
|
|
2552
2786
|
const chosen = (state?.properties ?? []).filter((p) => p.answer || savedOf(p.question.id));
|
|
2553
2787
|
const rules = state?.rules ?? [];
|
|
2554
2788
|
if (!chosen.length && !rules.length) out.push("No properties chosen yet.");
|
|
2555
|
-
for (const r of rules)
|
|
2789
|
+
for (const r of rules) {
|
|
2790
|
+
out.push(`A rule the person gave: "${r.rule}" Status: ${PAGE_STATUS[r.status][0]}. Checked with ${r.laws.length === 1 ? "1 law" : `${r.laws.length} laws`} you wrote${r.across ? `, and a scenario that runs every action that changes these records (${r.across.actions.join(", ")})` : ""}.${r.status === "proven" || r.status === "bounded" ? ` Not checked: ${ruleUnchecked(r.across !== void 0)}` : ""}`);
|
|
2791
|
+
}
|
|
2556
2792
|
for (const p of chosen) {
|
|
2557
2793
|
const saved = savedOf(p.question.id);
|
|
2558
2794
|
const stale = saved && (!p.answer || p.answer.answeredAt !== saved.answeredAt);
|
|
2559
2795
|
const answer = stale ? saved : p.answer;
|
|
2560
2796
|
const label2 = p.question.choices.find((c) => c.id === answer.choice)?.label ?? answer.choice;
|
|
2561
|
-
|
|
2797
|
+
const unchecked = !stale && HOLDING.includes(p.status) && p.question.choices.find((c) => c.id === answer.choice)?.unchecked;
|
|
2798
|
+
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}")` : ""}.${unchecked ? ` Not checked: ${unchecked}` : ""}`);
|
|
2562
2799
|
}
|
|
2563
2800
|
heading("happens");
|
|
2564
2801
|
const step = (s, pad) => {
|
|
@@ -2611,7 +2848,10 @@ function pageText(root, pageUrl, now, surface) {
|
|
|
2611
2848
|
`;
|
|
2612
2849
|
}
|
|
2613
2850
|
function inViewLines(viewing, codeAt, said2, now, pageUrl) {
|
|
2614
|
-
|
|
2851
|
+
const link = pageUrl ? `Give the person its link: ${pageUrl}` : "The page is not running: start `yichus studio --page` in the background in this folder, then run this again for its link.";
|
|
2852
|
+
if (!viewing) return [`The page has not said what is on screen: it may not be open. ${link}`];
|
|
2853
|
+
if (viewing.closed) return [`The person closed the page ${ago(viewing.closed, now)}, so it is not open now. ${pageUrl ? `If you want them to look at it, give them its link again: ${pageUrl}` : link}`];
|
|
2854
|
+
if (!pageUrl) return [`The page is not open: its server stopped after it last said what was on screen. ${link}`];
|
|
2615
2855
|
const parts = (anchors) => !viewing.current ? [] : anchors.flatMap((a) => codeAt.has(a) ? [`[${codeAt.get(a)}] ${said2.get(codeAt.get(a))}`] : []);
|
|
2616
2856
|
const list = (what, items) => items.length ? [`${what}:`, ...items.map((i) => ` ${i}`)] : [];
|
|
2617
2857
|
const { touched } = viewing;
|
|
@@ -2678,12 +2918,20 @@ var reference_default = {
|
|
|
2678
2918
|
]
|
|
2679
2919
|
},
|
|
2680
2920
|
"protocol-case": {
|
|
2681
|
-
about: "The
|
|
2921
|
+
about: "The scenarios the studio checks for an action that must be all or nothing or must survive an outside service failing, and for a rule other actions could go around: the case format and complete worked cases.",
|
|
2682
2922
|
files: [
|
|
2683
2923
|
{ name: "protocol-cases.md", from: "docs/studio/protocol-cases.md" },
|
|
2684
2924
|
{ name: "protocol-specification.md", from: "docs/protocol-verification.md", sections: ["Case format: protocol-case-1.0", "Observation predicates", "Trace assertions", "Reachability witnesses", "Interpretation and limits"] },
|
|
2685
2925
|
{ name: "withdrawal-case.json", from: "eval/corpus/webmcp-traps/iteration-003/programs/Q15/twin/case.json" },
|
|
2686
|
-
{ name: "withdrawal.bosatsu", from: "eval/corpus/webmcp-traps/iteration-003/programs/Q15/twin/ledger.bosatsu" }
|
|
2926
|
+
{ name: "withdrawal.bosatsu", from: "eval/corpus/webmcp-traps/iteration-003/programs/Q15/twin/ledger.bosatsu" },
|
|
2927
|
+
{ name: "signups.bosatsu", from: "docs/studio/laws-example/signups/signups.bosatsu" },
|
|
2928
|
+
{ name: "signups-laws.bosatsu", from: "docs/studio/laws-example/signups/signups-laws.bosatsu" },
|
|
2929
|
+
{ name: "signups-case.json", from: "docs/studio/laws-example/signups/cases/signups-case.json" },
|
|
2930
|
+
{ name: "spots.bosatsu", from: "docs/studio/laws-example/signups/cases/spots.bosatsu" },
|
|
2931
|
+
{ name: "near-miss-signups.bosatsu", from: "docs/studio/laws-example/signups/near-miss/signups.bosatsu" },
|
|
2932
|
+
{ name: "near-miss-case.json", from: "docs/studio/laws-example/signups/cases/near-miss-case.json" },
|
|
2933
|
+
{ name: "mutants/double-signup.bosatsu", from: "docs/studio/laws-example/signups/mutants/double-signup.bosatsu" },
|
|
2934
|
+
{ name: "mutants/overfill.bosatsu", from: "docs/studio/laws-example/signups/mutants/overfill.bosatsu" }
|
|
2687
2935
|
]
|
|
2688
2936
|
},
|
|
2689
2937
|
calculator: {
|
|
@@ -2761,7 +3009,7 @@ function stepText(state, pageUrl) {
|
|
|
2761
3009
|
}
|
|
2762
3010
|
if (state.rules.length) {
|
|
2763
3011
|
lines.push("", "Rules the person stated:");
|
|
2764
|
-
for (const r of state.rules) lines.push(` "${r.rule}" (${r.laws.join(", ")}): ${STATUS_WORDS[r.status]}`);
|
|
3012
|
+
for (const r of state.rules) lines.push(` "${r.rule}" (${[...r.laws, ...r.across ? [r.across.casePath] : []].join(", ")}): ${STATUS_WORDS[r.status]}`);
|
|
2765
3013
|
}
|
|
2766
3014
|
if (pageUrl) lines.push("", `Studio page: ${pageUrl}`);
|
|
2767
3015
|
return `${lines.join("\n")}
|
|
@@ -2784,7 +3032,7 @@ The loop:
|
|
|
2784
3032
|
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.
|
|
2785
3033
|
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.
|
|
2786
3034
|
|
|
2787
|
-
|
|
3035
|
+
The core api_* tools are listed too, and api_tools opens a specialist family (access, concurrency, proofs and more): its tools join the list. Leave out sources and the studio supplies the project's files. Pages on yich.us, such as data apps and the playground, come through a separate connection (@yichus/mcp); building needs only this one.`;
|
|
2788
3036
|
var START = {
|
|
2789
3037
|
name: "yichus_start",
|
|
2790
3038
|
description: "Record what the person wants built, in their own words, and get the studio page link to give them. Call this first.",
|
|
@@ -2953,7 +3201,7 @@ function pageNote(pageUrl, say2) {
|
|
|
2953
3201
|
].join("\n");
|
|
2954
3202
|
}
|
|
2955
3203
|
function createStudioMcpServer({ root, engine, catalog, pageUrl, now = () => /* @__PURE__ */ new Date(), locate = builtCopy }) {
|
|
2956
|
-
const server = new Server({ name: "yichus-studio", version: "0.3.
|
|
3204
|
+
const server = new Server({ name: "yichus-studio", version: "0.3.4" }, { capabilities: { tools: { listChanged: true } }, instructions: INSTRUCTIONS });
|
|
2957
3205
|
const active = /* @__PURE__ */ new Set();
|
|
2958
3206
|
const track = (task) => {
|
|
2959
3207
|
active.add(task);
|
|
@@ -2961,10 +3209,12 @@ function createStudioMcpServer({ root, engine, catalog, pageUrl, now = () => /*
|
|
|
2961
3209
|
});
|
|
2962
3210
|
return task;
|
|
2963
3211
|
};
|
|
2964
|
-
let
|
|
2965
|
-
const
|
|
3212
|
+
let entries = null;
|
|
3213
|
+
const catalogEntries2 = async () => entries ??= (await catalog()).map((e) => ({ ...e, tool: studioTool(e.tool) }));
|
|
3214
|
+
const catalogTools = async () => (await catalogEntries2()).map((e) => e.tool);
|
|
3215
|
+
const families = new OpenFamilies(false, Object.values(DIRECTED_TOOLS));
|
|
2966
3216
|
let last = readState(root);
|
|
2967
|
-
server.setRequestHandler(ListToolsRequestSchema, () => track((async () => ({ tools: [START, NEXT, NAME, ANSWER, RULES, SCENARIO, POINT, PAGE, REFERENCE_TOOL, ...await
|
|
3217
|
+
server.setRequestHandler(ListToolsRequestSchema, () => track((async () => ({ tools: [START, NEXT, NAME, ANSWER, RULES, SCENARIO, POINT, PAGE, REFERENCE_TOOL, ...families.listed(await catalogEntries2())] }))()));
|
|
2968
3218
|
server.setRequestHandler(CallToolRequestSchema, (request) => track(callTool(request.params)));
|
|
2969
3219
|
async function callTool(params) {
|
|
2970
3220
|
const { name, arguments: args = {} } = params;
|
|
@@ -3009,7 +3259,7 @@ Next: call yichus_next.`, { pageUrl });
|
|
|
3009
3259
|
return said2 ? text(said2) : text(`No reference topic ${String(args.topic)}.
|
|
3010
3260
|
${topicList((topic) => `topic "${topic}"`)}`, void 0, true);
|
|
3011
3261
|
}
|
|
3012
|
-
const tool = (await
|
|
3262
|
+
const tool = (await catalogTools()).find((t) => t.name === name);
|
|
3013
3263
|
if (!tool) return text(`Unknown tool ${name}.`, void 0, true);
|
|
3014
3264
|
const filled = { ...args };
|
|
3015
3265
|
const props = tool.inputSchema.properties ?? {};
|
|
@@ -3019,15 +3269,7 @@ ${topicList((topic) => `topic "${topic}"`)}`, void 0, true);
|
|
|
3019
3269
|
const topology2 = loadRequirements(root).answers.find((a) => a.question === "topology")?.choice;
|
|
3020
3270
|
if ("instances" in props && filled.instances === void 0 && topology2) filled.instances = topology2 === "one" ? "1" : "many";
|
|
3021
3271
|
const result = await engine.call(name, filled);
|
|
3022
|
-
if (name
|
|
3023
|
-
const titles = {
|
|
3024
|
-
ok: true,
|
|
3025
|
-
tool: "api_guide",
|
|
3026
|
-
titles: result.sections.map((s) => s.title),
|
|
3027
|
-
note: "The whole guide is long. Call api_guide with section set to one of these titles, or to part of one."
|
|
3028
|
-
};
|
|
3029
|
-
return text(JSON.stringify(titles), titles);
|
|
3030
|
-
}
|
|
3272
|
+
if (families.record(name, result)) await server.sendToolListChanged();
|
|
3031
3273
|
return text(JSON.stringify(result), result, result.ok !== true);
|
|
3032
3274
|
}
|
|
3033
3275
|
return {
|
|
@@ -3222,7 +3464,7 @@ ${said2}
|
|
|
3222
3464
|
var HELP = `yichus \u2014 the Yichus WebMCP analysis engine, locally
|
|
3223
3465
|
|
|
3224
3466
|
yichus tools
|
|
3225
|
-
yichus call TOOL [--args JSON_FILE|-] [--source FILE ... | --project DIR]
|
|
3467
|
+
yichus call TOOL [--args JSON_FILE|-] [--arg-file NAME=JSON_FILE ...] [--source FILE ... | --project DIR]
|
|
3226
3468
|
yichus verify FILE ... [--instances 1|many] [--block-on-warning ID ...]
|
|
3227
3469
|
yichus lean FILE ... --lean-wasm-root DIRECTORY [--timeout-ms N]
|
|
3228
3470
|
${STUDIO_HELP}
|
|
@@ -3234,6 +3476,8 @@ Options:
|
|
|
3234
3476
|
(default the current folder), as the studio reads
|
|
3235
3477
|
it (call only).
|
|
3236
3478
|
--args FILE|- Tool arguments as a JSON object; - reads stdin.
|
|
3479
|
+
--arg-file NAME=FILE Set one argument to the JSON in FILE, such as a
|
|
3480
|
+
saved snapshot (repeatable; call only).
|
|
3237
3481
|
--block-on-warning ID A property id whose warning blocks the verdict, as a
|
|
3238
3482
|
violation would (repeatable; all names every property;
|
|
3239
3483
|
verify only). An id the verdict lacks exits 2.
|
|
@@ -3297,6 +3541,7 @@ var objectJson = (raw, label2) => objectValue(JSON.parse(raw), label2);
|
|
|
3297
3541
|
var FLAGS = {
|
|
3298
3542
|
source: { type: "string", multiple: true },
|
|
3299
3543
|
args: { type: "string" },
|
|
3544
|
+
"arg-file": { type: "string", multiple: true },
|
|
3300
3545
|
project: { type: "string" },
|
|
3301
3546
|
instances: { type: "string" },
|
|
3302
3547
|
"block-on-warning": { type: "string", multiple: true },
|
|
@@ -3317,7 +3562,7 @@ function parse(argv2) {
|
|
|
3317
3562
|
if (command !== "tools" && command !== "call" && command !== "verify" && command !== "lean") {
|
|
3318
3563
|
throw new Error("Expected tools, call TOOL, verify FILE, or lean FILE; use --help for usage.");
|
|
3319
3564
|
}
|
|
3320
|
-
const opts = { command, sources: [], expects: [], blockOnWarnings: values["block-on-warning"] ?? [] };
|
|
3565
|
+
const opts = { command, sources: [], argFiles: [], expects: [], blockOnWarnings: values["block-on-warning"] ?? [] };
|
|
3321
3566
|
if (command === "call") {
|
|
3322
3567
|
opts.tool = rest.shift();
|
|
3323
3568
|
if (!opts.tool) throw new Error("call requires a tool name");
|
|
@@ -3325,6 +3570,14 @@ function parse(argv2) {
|
|
|
3325
3570
|
if ((command === "tools" || command === "call") && rest.length) throw new Error(`Unexpected argument: ${rest[0]}`);
|
|
3326
3571
|
opts.sources = [...rest, ...values.source ?? []];
|
|
3327
3572
|
opts.args = values.args;
|
|
3573
|
+
for (const value of values["arg-file"] ?? []) {
|
|
3574
|
+
const equal = value.indexOf("=");
|
|
3575
|
+
if (equal <= 0 || equal === value.length - 1) throw new Error("--arg-file requires NAME=FILE");
|
|
3576
|
+
const name = value.slice(0, equal);
|
|
3577
|
+
if (opts.argFiles.some((a) => a.name === name)) throw new Error(`--arg-file names ${name} twice`);
|
|
3578
|
+
opts.argFiles.push({ name, file: value.slice(equal + 1) });
|
|
3579
|
+
}
|
|
3580
|
+
if (opts.argFiles.length && command !== "call") throw new Error("--arg-file is for call");
|
|
3328
3581
|
opts.project = values.project;
|
|
3329
3582
|
if (opts.project !== void 0 && command !== "call") throw new Error("--project is for call");
|
|
3330
3583
|
if (opts.project !== void 0 && opts.sources.length) throw new Error("Use either --source files or --project, not both");
|
|
@@ -3430,6 +3683,15 @@ function sourceNames(paths) {
|
|
|
3430
3683
|
}
|
|
3431
3684
|
async function argumentsFor(opts, io) {
|
|
3432
3685
|
const args = opts.args ? objectJson(opts.args === "-" ? await io.readStdin() : readInput(opts.args), "Arguments") : {};
|
|
3686
|
+
for (const { name, file } of opts.argFiles) {
|
|
3687
|
+
if (Object.hasOwn(args, name)) throw new Error(`Use either --arg-file ${name}=... or args.${name}, not both`);
|
|
3688
|
+
try {
|
|
3689
|
+
args[name] = JSON.parse(readInput(file));
|
|
3690
|
+
} catch (error) {
|
|
3691
|
+
if (error instanceof SyntaxError) throw new Error(`--arg-file ${name}: ${file} is not JSON (${error.message})`);
|
|
3692
|
+
throw error;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3433
3695
|
if (opts.sources.length) {
|
|
3434
3696
|
if (Object.hasOwn(args, "sources")) throw new Error("Use either --source files or args.sources, not both");
|
|
3435
3697
|
const names = sourceNames(opts.sources);
|
|
@@ -3457,7 +3719,7 @@ function clearOutput(opts) {
|
|
|
3457
3719
|
if (!opts.output) return;
|
|
3458
3720
|
const output = path9.resolve(opts.output);
|
|
3459
3721
|
const outputStat = fs9.existsSync(output) ? fs9.statSync(output) : void 0;
|
|
3460
|
-
for (const input of [...opts.sources, opts.args, opts.bundle].filter((p) => !!p && p !== "-")) {
|
|
3722
|
+
for (const input of [...opts.sources, ...opts.argFiles.map((a) => a.file), opts.args, opts.bundle].filter((p) => !!p && p !== "-")) {
|
|
3461
3723
|
const inputStat = fs9.existsSync(input) ? fs9.statSync(input) : void 0;
|
|
3462
3724
|
if (path9.resolve(input) === output || outputStat && inputStat && inputStat.dev === outputStat.dev && inputStat.ino === outputStat.ino) {
|
|
3463
3725
|
throw new Error("--output must not overwrite an input file");
|
|
@@ -3492,16 +3754,22 @@ async function runCli(argv2, io) {
|
|
|
3492
3754
|
timeoutMs
|
|
3493
3755
|
), "Lean result");
|
|
3494
3756
|
} else {
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3757
|
+
const progress = opts.command === "call" ? callProgress(opts.tool, Array.isArray(args.sources) ? args.sources.length : void 0) : void 0;
|
|
3758
|
+
let stageStart = io.now();
|
|
3759
|
+
const finished = (stage) => {
|
|
3760
|
+
const now = io.now();
|
|
3761
|
+
io.stderr(` ${stage} in ${((now - stageStart) / 1e3).toFixed(1)} s.
|
|
3762
|
+
`);
|
|
3763
|
+
stageStart = now;
|
|
3764
|
+
};
|
|
3765
|
+
if (progress) io.stderr(progress);
|
|
3500
3766
|
const engine = await io.loadEngine(opts.bundle);
|
|
3767
|
+
if (progress) finished("Loaded the engine");
|
|
3501
3768
|
if (opts.command === "call" && !Object.hasOwn(args, "sources") && readsProgram(engine, opts.tool)) {
|
|
3502
3769
|
args.sources = folderSources(opts.tool, opts.project);
|
|
3503
3770
|
}
|
|
3504
|
-
const
|
|
3771
|
+
const tool = opts.command === "verify" ? "api_verify" : opts.tool;
|
|
3772
|
+
const raw = opts.command === "tools" ? engine.listTools() : progress ? engine.callTool(tool, JSON.stringify(args), finished) : engine.callTool(tool, JSON.stringify(args));
|
|
3505
3773
|
result = objectJson(raw, "Engine result");
|
|
3506
3774
|
}
|
|
3507
3775
|
if (typeof result.ok !== "boolean") throw new Error("Engine result must explicitly contain boolean ok");
|
|
@@ -3547,7 +3815,7 @@ async function runCli(argv2, io) {
|
|
|
3547
3815
|
import { once } from "events";
|
|
3548
3816
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3549
3817
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3550
|
-
import { EngineWorkerClient,
|
|
3818
|
+
import { EngineWorkerClient, catalogEntries } from "@yichus/api-mcp";
|
|
3551
3819
|
|
|
3552
3820
|
// src/studio/page-server.ts
|
|
3553
3821
|
import fs10 from "fs";
|
|
@@ -3573,7 +3841,13 @@ function personView(state, names, marks = null) {
|
|
|
3573
3841
|
status: p.status
|
|
3574
3842
|
})),
|
|
3575
3843
|
// A state written before rules were kept has none.
|
|
3576
|
-
rules: (state.rules ?? []).map((r) => ({
|
|
3844
|
+
rules: (state.rules ?? []).map((r) => ({
|
|
3845
|
+
rule: r.rule,
|
|
3846
|
+
status: r.status,
|
|
3847
|
+
laws: r.laws.length,
|
|
3848
|
+
across: r.across !== void 0,
|
|
3849
|
+
unchecked: r.status === "proven" || r.status === "bounded" ? ruleUnchecked(r.across !== void 0) : null
|
|
3850
|
+
})),
|
|
3577
3851
|
map: state.map ? relabelSystemMap(state.map, names) : null,
|
|
3578
3852
|
mapError: state.mapError,
|
|
3579
3853
|
sections,
|
|
@@ -3601,9 +3875,10 @@ async function startPageServer(root, assetsDir, port = null, now = () => /* @__P
|
|
|
3601
3875
|
};
|
|
3602
3876
|
for (const file of watched) fs10.watchFile(file, { interval: 250 }, changed);
|
|
3603
3877
|
const hosts = /* @__PURE__ */ new Set();
|
|
3878
|
+
let self = "", closing = false;
|
|
3604
3879
|
const takes = /* @__PURE__ */ new Map([
|
|
3605
3880
|
["/answer", (args) => recordAnswer(root, args, now())],
|
|
3606
|
-
["/viewing", (args) => recordViewing(root, args, now())]
|
|
3881
|
+
["/viewing", (args) => recordViewing(root, self, args, now())]
|
|
3607
3882
|
]);
|
|
3608
3883
|
const server = http.createServer((req, res) => {
|
|
3609
3884
|
const send = (status, type, body) => {
|
|
@@ -3624,7 +3899,11 @@ async function startPageServer(root, assetsDir, port = null, now = () => /* @__P
|
|
|
3624
3899
|
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
|
|
3625
3900
|
res.write(": connected\n\n");
|
|
3626
3901
|
clients.add(res);
|
|
3627
|
-
|
|
3902
|
+
recordOpened(root, self);
|
|
3903
|
+
req.on("close", () => {
|
|
3904
|
+
clients.delete(res);
|
|
3905
|
+
if (!clients.size && !closing) recordClosed(root, self, now());
|
|
3906
|
+
});
|
|
3628
3907
|
return;
|
|
3629
3908
|
}
|
|
3630
3909
|
const asset = PAGE_ASSETS.find((a) => url.pathname === `/${a}`);
|
|
@@ -3667,9 +3946,11 @@ async function startPageServer(root, assetsDir, port = null, now = () => /* @__P
|
|
|
3667
3946
|
}
|
|
3668
3947
|
const bound = server.address().port;
|
|
3669
3948
|
hosts.add(`127.0.0.1:${bound}`).add(`localhost:${bound}`);
|
|
3949
|
+
self = `http://127.0.0.1:${bound}/`;
|
|
3670
3950
|
return {
|
|
3671
|
-
url:
|
|
3951
|
+
url: self,
|
|
3672
3952
|
async close() {
|
|
3953
|
+
closing = true;
|
|
3673
3954
|
for (const file of watched) fs10.unwatchFile(file, changed);
|
|
3674
3955
|
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
3675
3956
|
server.closeAllConnections();
|
|
@@ -3697,7 +3978,7 @@ function workerEngine(bundle) {
|
|
|
3697
3978
|
return objectResult(await worker.callTool(tool, JSON.stringify(args), `studio-${++id}`), tool);
|
|
3698
3979
|
},
|
|
3699
3980
|
async catalog() {
|
|
3700
|
-
return
|
|
3981
|
+
return catalogEntries(await worker.listTools(`studio-${++id}`));
|
|
3701
3982
|
},
|
|
3702
3983
|
close: () => worker.close()
|
|
3703
3984
|
};
|
|
@@ -3757,6 +4038,7 @@ process.exitCode = STUDIO_COMMANDS.has(argv[0] ?? "") ? await runStudioCommand(a
|
|
|
3757
4038
|
// fails a synchronous read with EAGAIN on macOS.
|
|
3758
4039
|
readStdin: () => text2(process.stdin),
|
|
3759
4040
|
stdout: (text3) => process.stdout.write(text3),
|
|
3760
|
-
stderr: (text3) => process.stderr.write(text3)
|
|
4041
|
+
stderr: (text3) => process.stderr.write(text3),
|
|
4042
|
+
now: () => performance.now()
|
|
3761
4043
|
});
|
|
3762
4044
|
//# sourceMappingURL=main.js.map
|