yichus 0.3.3 → 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/dist/main.js CHANGED
@@ -829,6 +829,10 @@ var WHAT = {
829
829
  };
830
830
  var PACKAGE = "a section of the program";
831
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"]);
832
836
  var VALUE = "a kind of value";
833
837
  var TYPES = /* @__PURE__ */ new Set(["type", "types", "signature"]);
834
838
  var WORD = /[A-Za-z_][\w/]*::\w+|[A-Za-z_]\w*/g;
@@ -856,15 +860,17 @@ var leaves = (record, visit) => {
856
860
  function nameableParts(map, own) {
857
861
  const found = /* @__PURE__ */ new Map();
858
862
  const theirs2 = (pkg) => !own || own.has(pkg);
863
+ const rank = (what) => [OFFERED, MADE, PACKAGE].indexOf(what);
859
864
  const add = (code, what) => {
860
- 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);
861
866
  };
862
- const idParts = (id) => {
867
+ const idParts = (id, key) => {
863
868
  const i = id.lastIndexOf("::");
864
869
  if (!theirs2(id.slice(0, i))) return;
865
870
  add(id.slice(0, i), PACKAGE);
866
- add(id.slice(i + 2), DEFINITION);
871
+ add(id.slice(i + 2), DEFINED[key] ?? DEFINITION);
867
872
  };
873
+ const isPackage = (s, key) => own?.has(s) || PACKAGE_KEYS.has(key) || s.includes("/");
868
874
  leaves(map, (s, key) => {
869
875
  if (key === "guardIds" && s.startsWith("route:")) return;
870
876
  if (s.startsWith("route:")) return add(s.slice(6), WHAT.path);
@@ -874,12 +880,12 @@ function nameableParts(map, own) {
874
880
  return;
875
881
  }
876
882
  if (s.includes("::")) {
877
- 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);
878
884
  return;
879
885
  }
880
886
  if (STRUCTURAL.has(key)) return;
881
- if (key === "name" || key === "package" || key === "pkg" || key === "instantiatingPackages" || key === "cycles" || key === "to") return add(s, s.includes("/") ? PACKAGE : DEFINITION);
882
- 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);
883
889
  });
884
890
  for (const b of map.bundles ?? []) if (b.count > 1) found.delete(b.label);
885
891
  return [...found].map(([code, what]) => ({ code, what })).sort((a, b) => a.code.localeCompare(b.code));
@@ -1259,6 +1265,7 @@ var Step = z3.looseObject({ op: z3.string(), tables: z3.array(z3.string()).defau
1259
1265
  var Route = z3.looseObject({
1260
1266
  path: z3.string(),
1261
1267
  handler: z3.string(),
1268
+ handlerBinding: z3.string(),
1262
1269
  authority: z3.string().optional(),
1263
1270
  tables: z3.array(z3.string()).default([]),
1264
1271
  inferredSteps: z3.array(Step).default([])
@@ -1277,8 +1284,10 @@ var Table = z3.looseObject({
1277
1284
  });
1278
1285
  var Report = z3.looseObject({
1279
1286
  surface: z3.looseObject({ routes: z3.array(Route).default([]) }).default({ routes: [] }),
1280
- 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: [] })
1281
1289
  });
1290
+ var handlerOf = (report, route) => route.handlerBinding.includes("::") ? route.handlerBinding : `${report.shape.packages[0]?.name}::${route.handlerBinding}`;
1282
1291
  var Entry = z3.looseObject({ entry: z3.string(), resources: z3.array(z3.string()).default([]) });
1283
1292
  var Structure = z3.looseObject({ entries: z3.array(Entry).default([]) });
1284
1293
  var ACCESS_RULES = {
@@ -1359,11 +1368,36 @@ function accessQuestion(table, names) {
1359
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?`,
1360
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.",
1361
1370
  choices: [
1362
- { id: "owner", label: "Only the person who created it", means: "Every read and write is tied to the signed-in person, and the check proves no one reaches another person's records. 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." },
1363
- { id: "shared", label: "Only the people a rule lets in", 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." },
1364
- { id: "server-only", label: "No person, only the app's own server", 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." },
1365
- { id: "everyone-reads", label: "Everyone can see it; only certain roles can change it", means: "Anyone can read; changes need a role the app gives out." },
1366
- { id: "public", label: "Anyone, even without signing in", means: "Nothing in these records is private." }
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
+ }
1367
1401
  ],
1368
1402
  fact: `The program stores rows in "${table.name}" (policy declared: ${table.policy ?? "none"}).`
1369
1403
  };
@@ -1377,8 +1411,18 @@ function togetherQuestion(table, handlers, names) {
1377
1411
  ask: `Two requests change the same record in "${shown(names, table.name)}" at the same moment (${who}). What should happen?`,
1378
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.",
1379
1413
  choices: [
1380
- { id: "keep-both", label: "Both changes are kept", 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." },
1381
- { id: "refuse-second", label: "The second is refused and they try again", means: "A change made to an out-of-date copy is turned away; the check proves no change is lost." }
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
+ }
1382
1426
  ],
1383
1427
  fact: `${handlers.join(", ")} ${handlers.length === 1 ? "reads" : "read"} "${table.name}" and then ${handlers.length === 1 ? "writes" : "write"} it.`
1384
1428
  };
@@ -1391,7 +1435,12 @@ function partialQuestion(route, tables, names) {
1391
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?`,
1392
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.`,
1393
1437
  choices: [
1394
- { id: "all-or-nothing", label: "All or nothing", means: "Every change this action makes is saved together or not at all; Yichus stops it at each step and checks nothing half-done stays saved." },
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
+ },
1395
1444
  { id: "partial-ok", label: "Partial is fine", means: "No check is added for this action." }
1396
1445
  ],
1397
1446
  fact: `${route.handler} writes ${tables.map((t) => `"${t}"`).join(", ")} in more than one step.`
@@ -1406,13 +1455,26 @@ function outsideQuestion(route, kinds, names) {
1406
1455
  ask: `"${shown(names, route.path)}" calls ${service}. If that service is down or slow, what should the person using your app see?`,
1407
1456
  why: "Outside services fail often. Without a plan, a failure can leave a half-finished record or charge for work that never happened.",
1408
1457
  choices: [
1409
- { id: "fail-clean", label: "An error, with nothing saved", means: "Yichus makes the service fail and checks that nothing from this action stays saved." },
1410
- { id: "finish-later", label: "Save the request and finish it later", means: "Yichus makes the service fail and checks the saved request is tried again and finished exactly once." },
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
+ },
1411
1470
  { id: "no-plan", label: "No preference", means: "No check is added for this action." }
1412
1471
  ],
1413
1472
  fact: `${route.handler} reaches ${kinds.join(" and ")}.`
1414
1473
  };
1415
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))];
1416
1478
  var PERSON_SIGNS_IN = /* @__PURE__ */ new Set(["authenticated", "publishable", "requires-roles"]);
1417
1479
  var signIn = {
1418
1480
  id: "sign-in",
@@ -1451,9 +1513,7 @@ function questionsFor(report, structure, names = {}) {
1451
1513
  if (both.length) questions.push(togetherQuestion(table, both, names));
1452
1514
  }
1453
1515
  for (const route of report.surface.routes) {
1454
- const writes = route.inferredSteps.filter((s) => s.resource === "db" && s.op !== "read" && s.op !== "query");
1455
- const tables = [...new Set(writes.flatMap((s) => s.tables))];
1456
- if (writes.length >= 2) questions.push(partialQuestion(route, tables, names));
1516
+ if (writeSteps(route).length >= 2) questions.push(partialQuestion(route, changedBy(route), names));
1457
1517
  }
1458
1518
  for (const route of report.surface.routes) {
1459
1519
  const entry = structure.entries.find((e) => e.entry === `route:${route.path}`);
@@ -1681,7 +1741,7 @@ var LawCheck = z6.looseObject({
1681
1741
  notSampled: z6.array(z6.looseObject({ binding: z6.string() })).optional(),
1682
1742
  reach: z6.array(z6.looseObject({ binding: z6.string(), usedBy: z6.array(z6.string()) })).optional()
1683
1743
  });
1684
- var caseFile = (root, questionId) => path3.join(studioDir(root), "cases", `${questionId.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "")}.json`);
1744
+ var caseFile = (root, questionId) => path3.join(studioDir(root), "cases", `${questionId.replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-|-$/g, "")}.json`);
1685
1745
  var answerOf = (requirements, id) => requirements.answers.find((a) => a.question === id) ?? null;
1686
1746
  function instancesFor(requirements) {
1687
1747
  const choice = answerOf(requirements, "topology")?.choice;
@@ -1734,6 +1794,7 @@ async function judge(root, engine, sources, question, saved, report, verify, tog
1734
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.`)
1735
1795
  };
1736
1796
  }
1797
+ if (answer.choice === "public") return { ...base, status: "no-check", check };
1737
1798
  const prop = byId(`access:${question.subject}`) ?? byId("access-coverage");
1738
1799
  if (!prop) return { ...base, status: "inconclusive", check: "api_verify", evidence: ["api_verify reported no access property for this table."] };
1739
1800
  return {
@@ -1759,35 +1820,47 @@ async function judge(root, engine, sources, question, saved, report, verify, tog
1759
1820
  case "partial":
1760
1821
  case "outside": {
1761
1822
  if (answer.choice === "partial-ok" || answer.choice === "no-plan") return { ...base, status: "no-check" };
1762
- const file = caseFile(root, question.id);
1763
- const rel = path3.relative(root, file).split(path3.sep).join("/");
1764
- if (!fs3.existsSync(file)) return { ...base, casePath: rel, status: "needs-case", check: "api_protocol_check", evidence: [`No protocol case at ${rel} yet.`] };
1765
- let protocolCase;
1766
- try {
1767
- protocolCase = JSON.parse(fs3.readFileSync(file, "utf8"));
1768
- } catch (error) {
1769
- return {
1770
- ...base,
1771
- casePath: rel,
1772
- check: "api_protocol_check",
1773
- status: "inconclusive",
1774
- evidence: [`${rel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`]
1775
- };
1776
- }
1777
- const result = Protocol.parse(await engine.call("api_protocol_check", { sources: [...sources, ...caseSources(root)], case: protocolCase }));
1778
- const status = result.protocol?.status;
1779
- const check = `api_protocol_check ${rel}`;
1780
- if (!status) return { ...base, casePath: rel, check, status: "inconclusive", evidence: [result.error ?? "The protocol check gave no status."] };
1781
- return {
1782
- ...base,
1783
- casePath: rel,
1784
- check,
1785
- evidence: [`${result.protocol?.name ?? rel}: ${status}`],
1786
- status: status === "holds" ? "bounded" : status === "violated" ? "broken" : "inconclusive"
1787
- };
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 };
1788
1825
  }
1789
1826
  }
1790
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
+ }
1791
1864
  var reason = (result) => typeof result.error === "string" ? result.error : "no reason given";
1792
1865
  async function checkLaws(engine, sources) {
1793
1866
  const answer = async (mode) => {
@@ -1826,14 +1899,27 @@ async function checkLaws(engine, sources) {
1826
1899
  }
1827
1900
  var lawNamed = (laws, ref) => laws.find((l) => l.binding === ref || l.binding.endsWith(`::${ref}`) || l.name === ref);
1828
1901
  var unused = (law) => law.usedBy !== null && !law.usedBy.length;
1829
- function judgeRule(rule, laws) {
1902
+ async function judgeRule(rule, laws, report, root, engine, sources) {
1830
1903
  const found = rule.laws.map((ref) => [ref, lawNamed(laws, ref)]);
1831
1904
  const missing = found.filter(([, law]) => !law).map(([ref]) => ref);
1832
1905
  const checked = found.flatMap(([, law]) => law ? [law] : []);
1833
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}`))];
1834
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";
1835
- return { ...rule, status, evidence };
1836
- }
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.`;
1837
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.";
1838
1924
  var QUESTION_PROPERTIES = (id) => id.startsWith("access:") || id === "access-coverage" || id === "rmw-transactions" || id === "integrity";
1839
1925
  var GUIDE = {
@@ -1842,6 +1928,12 @@ var GUIDE = {
1842
1928
  partial: "Transaction protocols: finite cases, retries, and receipts",
1843
1929
  outside: "Transaction protocols: finite cases, retries, and receipts"
1844
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
+ };
1845
1937
  var SCAFFOLD_ARGS = {
1846
1938
  package_name: "My/App/Api",
1847
1939
  service_name: "my_app",
@@ -1938,7 +2030,7 @@ function nextStep(state, compileErrors, say2 = MCP) {
1938
2030
  instructions: [
1939
2031
  ...mismatched.map((p) => {
1940
2032
  const rule = ACCESS_RULES[p.answer.choice];
1941
- 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("api_add_rule")} for the snippet; ${say2.api("api_guide", { section: rule.guide })} for what it means).`;
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).`;
1942
2034
  }),
1943
2035
  next
1944
2036
  ],
@@ -1947,7 +2039,7 @@ function nextStep(state, compileErrors, say2 = MCP) {
1947
2039
  const broken = state.properties.filter((p) => p.status === "broken" || p.status === "inconclusive");
1948
2040
  const failing = state.other.filter((o) => o.status !== "warning");
1949
2041
  const warnings = state.other.filter((o) => o.status === "warning");
1950
- 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");
1951
2043
  const ruled = new Set(state.rules.flatMap((r) => r.laws.flatMap((ref) => lawNamed(state.laws, ref)?.binding ?? [])));
1952
2044
  const brokenLaws = state.laws.filter((l) => !ruled.has(l.binding) && (l.status === "broken" || l.status === "inconclusive" || unused(l)));
1953
2045
  const notHolding = broken.length + failing.length + brokenRules.length + brokenLaws.length + (state.lawsError ? 1 : 0);
@@ -1962,7 +2054,7 @@ function nextStep(state, compileErrors, say2 = MCP) {
1962
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.` : ""}`),
1963
2055
  ...failing.map((o) => `${o.title} (${o.id}) is ${o.status}.`),
1964
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.`),
1965
- ...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.`),
1966
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}`}`),
1967
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."] : [],
1968
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}`
@@ -2021,13 +2113,15 @@ function nextStep(state, compileErrors, say2 = MCP) {
2021
2113
  ]
2022
2114
  };
2023
2115
  const cases = state.properties.filter((p) => p.status === "needs-case");
2024
- if (cases.length) return {
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 {
2025
2118
  stage: "write-case",
2026
- 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.",
2027
2120
  instructions: [
2028
2121
  `${capital(say2.reference("protocol-case"))}: it gives the case specification and a complete worked case with its program.`,
2029
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.",
2030
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.`),
2031
2125
  `State the person's rule exactly; never weaken it to match the code. ${next}`
2032
2126
  ]
2033
2127
  };
@@ -2041,7 +2135,7 @@ function nextStep(state, compileErrors, say2 = MCP) {
2041
2135
  ...assumed.length ? ["Say plainly which results depend on running exactly one server."] : [],
2042
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.`] : [],
2043
2137
  `When they describe a new feature, change the code and ${say2.use("yichus_next")} again: new facts can raise new questions.`,
2044
- 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("api_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")}.`
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")}.`
2045
2139
  ]
2046
2140
  };
2047
2141
  }
@@ -2109,7 +2203,7 @@ async function direct({ root, engine, requirements, withMap = true, previous = n
2109
2203
  const checked = await checkLaws(engine, sources);
2110
2204
  base.laws = checked.laws;
2111
2205
  base.lawsError = checked.error;
2112
- base.rules = (requirements.rules ?? []).map((r) => judgeRule(r, base.laws));
2206
+ for (const rule of requirements.rules ?? []) base.rules.push(await judgeRule(rule, base.laws, report, root, engine, sources));
2113
2207
  base.hasActions = report.surface.routes.length > 0;
2114
2208
  let access = null;
2115
2209
  if (base.hasActions || requirements.scenarios?.length) {
@@ -2158,6 +2252,7 @@ function readState(root) {
2158
2252
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2159
2253
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2160
2254
  import { z as z10 } from "zod";
2255
+ import { OpenFamilies } from "@yichus/api-mcp";
2161
2256
 
2162
2257
  // src/studio/marks.ts
2163
2258
  import fs4 from "fs";
@@ -2310,6 +2405,7 @@ function recordMarks(root, args, now) {
2310
2405
  }
2311
2406
 
2312
2407
  // src/studio/page.ts
2408
+ var HOLDING = ["proven", "bounded", "assumed", "no-check"];
2313
2409
  var PAGE_STATUS = {
2314
2410
  unanswered: ["waiting for your answer", "quiet"],
2315
2411
  decided: ["decided", "good"],
@@ -2325,6 +2421,9 @@ var PAGE_STATUS = {
2325
2421
  "no-check": ["no check needed", "quiet"],
2326
2422
  pending: ["answered, waiting for your agent to check", "quiet"]
2327
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
+ }
2328
2427
  var PAGE_HEADINGS = {
2329
2428
  asked: "What you asked for",
2330
2429
  now: "What is happening now",
@@ -2474,6 +2573,7 @@ import { mount } from './system-map.js';
2474
2573
  import { mountSections, mountWhatHappens, reportViewing, showMarks } from './studio-views.js';
2475
2574
 
2476
2575
  const STATUS = ${JSON.stringify(PAGE_STATUS)};
2576
+ const HOLDING = ${JSON.stringify(HOLDING)};
2477
2577
  const NO_ACTIONS = ${JSON.stringify(NO_ACTIONS)};
2478
2578
  const el = (tag, props = {}, ...kids) => { const n = Object.assign(document.createElement(tag), props); n.append(...kids); return n; };
2479
2579
  let mapHash = null, drawn = '';
@@ -2508,7 +2608,9 @@ function questionCard(q, saved) {
2508
2608
  function ruleRow(r) {
2509
2609
  const [words, tone] = STATUS[r.status];
2510
2610
  const li = el('li', {}, el('span', { textContent: 'A rule you gave: ' }, el('em', { textContent: r.rule })), el('span', { className: 'chip ' + tone, textContent: words }));
2511
- 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 }));
2512
2614
  return li;
2513
2615
  }
2514
2616
 
@@ -2520,6 +2622,8 @@ function propertyRow(p, saved) {
2520
2622
  const li = el('li', {}, el('span', { textContent: p.question.ask }), el('span', { className: 'chip ' + tone, textContent: words }));
2521
2623
  li.dataset.property = p.question.id;
2522
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 }));
2523
2627
  return li;
2524
2628
  }
2525
2629
 
@@ -2682,13 +2786,16 @@ function pageText(root, pageUrl, now, surface) {
2682
2786
  const chosen = (state?.properties ?? []).filter((p) => p.answer || savedOf(p.question.id));
2683
2787
  const rules = state?.rules ?? [];
2684
2788
  if (!chosen.length && !rules.length) out.push("No properties chosen yet.");
2685
- for (const r of rules) 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.`);
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
+ }
2686
2792
  for (const p of chosen) {
2687
2793
  const saved = savedOf(p.question.id);
2688
2794
  const stale = saved && (!p.answer || p.answer.answeredAt !== saved.answeredAt);
2689
2795
  const answer = stale ? saved : p.answer;
2690
2796
  const label2 = p.question.choices.find((c) => c.id === answer.choice)?.label ?? answer.choice;
2691
- 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}")` : ""}.`);
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}` : ""}`);
2692
2799
  }
2693
2800
  heading("happens");
2694
2801
  const step = (s, pad) => {
@@ -2811,12 +2918,20 @@ var reference_default = {
2811
2918
  ]
2812
2919
  },
2813
2920
  "protocol-case": {
2814
- about: "The failure scenarios the studio checks for an action that must be all or nothing, or must survive an outside service failing: the case format and a complete worked case.",
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.",
2815
2922
  files: [
2816
2923
  { name: "protocol-cases.md", from: "docs/studio/protocol-cases.md" },
2817
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"] },
2818
2925
  { name: "withdrawal-case.json", from: "eval/corpus/webmcp-traps/iteration-003/programs/Q15/twin/case.json" },
2819
- { 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" }
2820
2935
  ]
2821
2936
  },
2822
2937
  calculator: {
@@ -2894,7 +3009,7 @@ function stepText(state, pageUrl) {
2894
3009
  }
2895
3010
  if (state.rules.length) {
2896
3011
  lines.push("", "Rules the person stated:");
2897
- 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]}`);
2898
3013
  }
2899
3014
  if (pageUrl) lines.push("", `Studio page: ${pageUrl}`);
2900
3015
  return `${lines.join("\n")}
@@ -2917,7 +3032,7 @@ The loop:
2917
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.
2918
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.
2919
3034
 
2920
- 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.`;
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.`;
2921
3036
  var START = {
2922
3037
  name: "yichus_start",
2923
3038
  description: "Record what the person wants built, in their own words, and get the studio page link to give them. Call this first.",
@@ -3086,7 +3201,7 @@ function pageNote(pageUrl, say2) {
3086
3201
  ].join("\n");
3087
3202
  }
3088
3203
  function createStudioMcpServer({ root, engine, catalog, pageUrl, now = () => /* @__PURE__ */ new Date(), locate = builtCopy }) {
3089
- const server = new Server({ name: "yichus-studio", version: "0.3.3" }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
3204
+ const server = new Server({ name: "yichus-studio", version: "0.3.4" }, { capabilities: { tools: { listChanged: true } }, instructions: INSTRUCTIONS });
3090
3205
  const active = /* @__PURE__ */ new Set();
3091
3206
  const track = (task) => {
3092
3207
  active.add(task);
@@ -3094,10 +3209,12 @@ function createStudioMcpServer({ root, engine, catalog, pageUrl, now = () => /*
3094
3209
  });
3095
3210
  return task;
3096
3211
  };
3097
- let tools = null;
3098
- const catalogTools2 = async () => tools ??= (await catalog()).map(studioTool);
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));
3099
3216
  let last = readState(root);
3100
- server.setRequestHandler(ListToolsRequestSchema, () => track((async () => ({ tools: [START, NEXT, NAME, ANSWER, RULES, SCENARIO, POINT, PAGE, REFERENCE_TOOL, ...await catalogTools2()] }))()));
3217
+ server.setRequestHandler(ListToolsRequestSchema, () => track((async () => ({ tools: [START, NEXT, NAME, ANSWER, RULES, SCENARIO, POINT, PAGE, REFERENCE_TOOL, ...families.listed(await catalogEntries2())] }))()));
3101
3218
  server.setRequestHandler(CallToolRequestSchema, (request) => track(callTool(request.params)));
3102
3219
  async function callTool(params) {
3103
3220
  const { name, arguments: args = {} } = params;
@@ -3142,7 +3259,7 @@ Next: call yichus_next.`, { pageUrl });
3142
3259
  return said2 ? text(said2) : text(`No reference topic ${String(args.topic)}.
3143
3260
  ${topicList((topic) => `topic "${topic}"`)}`, void 0, true);
3144
3261
  }
3145
- const tool = (await catalogTools2()).find((t) => t.name === name);
3262
+ const tool = (await catalogTools()).find((t) => t.name === name);
3146
3263
  if (!tool) return text(`Unknown tool ${name}.`, void 0, true);
3147
3264
  const filled = { ...args };
3148
3265
  const props = tool.inputSchema.properties ?? {};
@@ -3152,15 +3269,7 @@ ${topicList((topic) => `topic "${topic}"`)}`, void 0, true);
3152
3269
  const topology2 = loadRequirements(root).answers.find((a) => a.question === "topology")?.choice;
3153
3270
  if ("instances" in props && filled.instances === void 0 && topology2) filled.instances = topology2 === "one" ? "1" : "many";
3154
3271
  const result = await engine.call(name, filled);
3155
- if (name === "api_guide" && filled.section === void 0 && Array.isArray(result.sections)) {
3156
- const titles = {
3157
- ok: true,
3158
- tool: "api_guide",
3159
- titles: result.sections.map((s) => s.title),
3160
- note: "The whole guide is long. Call api_guide with section set to one of these titles, or to part of one."
3161
- };
3162
- return text(JSON.stringify(titles), titles);
3163
- }
3272
+ if (families.record(name, result)) await server.sendToolListChanged();
3164
3273
  return text(JSON.stringify(result), result, result.ok !== true);
3165
3274
  }
3166
3275
  return {
@@ -3645,16 +3754,22 @@ async function runCli(argv2, io) {
3645
3754
  timeoutMs
3646
3755
  ), "Lean result");
3647
3756
  } else {
3648
- if (opts.command === "call") {
3649
- const files = Array.isArray(args.sources) ? args.sources.length : void 0;
3650
- const progress = callProgress(opts.tool, files);
3651
- if (progress) io.stderr(progress);
3652
- }
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);
3653
3766
  const engine = await io.loadEngine(opts.bundle);
3767
+ if (progress) finished("Loaded the engine");
3654
3768
  if (opts.command === "call" && !Object.hasOwn(args, "sources") && readsProgram(engine, opts.tool)) {
3655
3769
  args.sources = folderSources(opts.tool, opts.project);
3656
3770
  }
3657
- const raw = opts.command === "tools" ? engine.listTools() : engine.callTool(opts.command === "verify" ? "api_verify" : opts.tool, JSON.stringify(args));
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));
3658
3773
  result = objectJson(raw, "Engine result");
3659
3774
  }
3660
3775
  if (typeof result.ok !== "boolean") throw new Error("Engine result must explicitly contain boolean ok");
@@ -3700,7 +3815,7 @@ async function runCli(argv2, io) {
3700
3815
  import { once } from "events";
3701
3816
  import { fileURLToPath as fileURLToPath2 } from "url";
3702
3817
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3703
- import { EngineWorkerClient, catalogTools } from "@yichus/api-mcp";
3818
+ import { EngineWorkerClient, catalogEntries } from "@yichus/api-mcp";
3704
3819
 
3705
3820
  // src/studio/page-server.ts
3706
3821
  import fs10 from "fs";
@@ -3726,7 +3841,13 @@ function personView(state, names, marks = null) {
3726
3841
  status: p.status
3727
3842
  })),
3728
3843
  // A state written before rules were kept has none.
3729
- rules: (state.rules ?? []).map((r) => ({ rule: r.rule, status: r.status, laws: r.laws.length })),
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
+ })),
3730
3851
  map: state.map ? relabelSystemMap(state.map, names) : null,
3731
3852
  mapError: state.mapError,
3732
3853
  sections,
@@ -3857,7 +3978,7 @@ function workerEngine(bundle) {
3857
3978
  return objectResult(await worker.callTool(tool, JSON.stringify(args), `studio-${++id}`), tool);
3858
3979
  },
3859
3980
  async catalog() {
3860
- return catalogTools(await worker.listTools(`studio-${++id}`));
3981
+ return catalogEntries(await worker.listTools(`studio-${++id}`));
3861
3982
  },
3862
3983
  close: () => worker.close()
3863
3984
  };
@@ -3917,6 +4038,7 @@ process.exitCode = STUDIO_COMMANDS.has(argv[0] ?? "") ? await runStudioCommand(a
3917
4038
  // fails a synchronous read with EAGAIN on macOS.
3918
4039
  readStdin: () => text2(process.stdin),
3919
4040
  stdout: (text3) => process.stdout.write(text3),
3920
- stderr: (text3) => process.stderr.write(text3)
4041
+ stderr: (text3) => process.stderr.write(text3),
4042
+ now: () => performance.now()
3921
4043
  });
3922
4044
  //# sourceMappingURL=main.js.map