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/README.md +2 -2
- package/dist/cli.js +27 -0
- package/dist/cli.js.map +1 -1
- package/dist/main.js +210 -88
- package/dist/main.js.map +1 -1
- 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
|
@@ -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
|
|
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
|
-
{
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
-
{
|
|
1381
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
1410
|
-
|
|
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
|
-
|
|
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(/[
|
|
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
|
|
1763
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
3098
|
-
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));
|
|
3099
3216
|
let last = readState(root);
|
|
3100
|
-
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())] }))()));
|
|
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
|
|
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
|
|
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
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
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
|
|
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,
|
|
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) => ({
|
|
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
|
|
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
|