yichus 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/main.js +191 -31
- package/dist/main.js.map +1 -1
- package/dist/studio-page/system-map-record.js +93 -7
- package/dist/studio-page/system-map.js +82 -26
- package/dist/studio-page/what-happens-record.js +90 -6
- package/dist/studio-page/what-happens.js +72 -13
- package/dist/studio-reference/refund-desk-one-round.bosatsu +21 -18
- package/dist/studio-reference/refund-desk.bosatsu +21 -18
- package/dist/studio-reference/withdrawal.bosatsu +11 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ shipped in `@yichus/api-mcp`; installing the release requires Node 20+,
|
|
|
6
6
|
without Java, sbt, Nix, or a browser.
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
|
-
npm install --save-dev yichus@0.3.
|
|
9
|
+
npm install --save-dev yichus@0.3.3
|
|
10
10
|
npx yichus tools
|
|
11
11
|
npx yichus call api_guide
|
|
12
12
|
```
|
|
@@ -29,7 +29,7 @@ and the studio tells the agent what to do next after every change:
|
|
|
29
29
|
```json
|
|
30
30
|
{
|
|
31
31
|
"mcpServers": {
|
|
32
|
-
"yichus": { "command": "npx", "args": ["-y", "yichus@0.3.
|
|
32
|
+
"yichus": { "command": "npx", "args": ["-y", "yichus@0.3.3", "studio"] }
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
```
|
|
@@ -219,6 +219,9 @@ different folders are named by the folders that tell them apart
|
|
|
219
219
|
Use `--args options.json` (or `--args -` for stdin) for the remaining
|
|
220
220
|
arguments. Values retain their JSON types. You can supply a complete
|
|
221
221
|
`sources` array there instead of `--source`; mixing the two is rejected.
|
|
222
|
+
`--arg-file NAME=FILE` sets one argument to the JSON in a file, such as
|
|
223
|
+
a saved snapshot for `api_snapshot` (`--arg-file snapshot=ledger.snap.json`);
|
|
224
|
+
naming the same argument in `--args` too is rejected.
|
|
222
225
|
|
|
223
226
|
```json
|
|
224
227
|
{"instances":"1"}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../package.json","../src/node-version.ts","../src/cli.ts"],"sourcesContent":["{\n \"name\": \"yichus\",\n \"version\": \"0.3.
|
|
1
|
+
{"version":3,"sources":["../package.json","../src/node-version.ts","../src/cli.ts"],"sourcesContent":["{\n \"name\": \"yichus\",\n \"version\": \"0.3.3\",\n \"description\": \"Local Bosatsu compilation, Yichus analysis, and generated apps using the WebMCP engine\",\n \"license\": \"MIT\",\n \"homepage\": \"https://yich.us\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/snoble/yichus.git\",\n \"directory\": \"packages/yichus-cli\"\n },\n \"type\": \"module\",\n \"bin\": {\n \"yichus\": \"./dist/cli.js\"\n },\n \"files\": [\n \"dist\",\n \"README.md\"\n ],\n \"scripts\": {\n \"build\": \"tsup && node ./scripts/copy-studio-assets.mjs\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run --coverage\",\n \"prepack\": \"npm run build\"\n },\n \"dependencies\": {\n \"@yichus/api-mcp\": \"workspace:*\",\n \"zod\": \"^4.3.6\",\n \"@modelcontextprotocol/sdk\": \"^1.26.0\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^22.0.0\",\n \"tsup\": \"^8.3.0\",\n \"typescript\": \"^5.6.0\",\n \"vitest\": \"^2.1.0\",\n \"@vitest/coverage-v8\": \"2.1.9\"\n },\n \"engines\": {\n \"node\": \">=20\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n","/**\n * The sentence that stops the command on a Node older than the package's\n * `engines.node` floor, or undefined when the running Node meets it.\n */\nexport function nodeVersionProblem(running: string, required: string): string | undefined {\n const floor = /^>=(\\d+)$/.exec(required);\n if (!floor) throw new Error(`engines.node must read >=N, not ${required}`);\n const needed = Number(floor[1]);\n if (Number(running.split('.')[0]) >= needed) return undefined;\n return `yichus needs Node ${needed} or newer; this is Node ${running}. Install Node ${needed} or newer and run the command again.`;\n}\n","#!/usr/bin/env node\n// The Node check runs before the rest of the command loads, so an older\n// Node gets a sentence naming what to install rather than an error from\n// inside the engine.\nimport { engines } from '../package.json';\nimport { nodeVersionProblem } from './node-version.js';\n\nconst problem = nodeVersionProblem(process.versions.node, engines.node);\nif (problem) {\n process.stderr.write(`${problem}\\n`);\n process.exitCode = 1;\n} else {\n await import('./main.js');\n}\n"],"mappings":";;;AAqCE,cAAW;AAAA,EACT,MAAQ;AACV;;;ACnCK,SAAS,mBAAmB,SAAiB,UAAsC;AACxF,QAAM,QAAQ,YAAY,KAAK,QAAQ;AACvC,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,mCAAmC,QAAQ,EAAE;AACzE,QAAM,SAAS,OAAO,MAAM,CAAC,CAAC;AAC9B,MAAI,OAAO,QAAQ,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,OAAQ,QAAO;AACpD,SAAO,qBAAqB,MAAM,2BAA2B,OAAO,kBAAkB,MAAM;AAC9F;;;ACHA,IAAM,UAAU,mBAAmB,QAAQ,SAAS,MAAM,QAAQ,IAAI;AACtE,IAAI,SAAS;AACX,UAAQ,OAAO,MAAM,GAAG,OAAO;AAAA,CAAI;AACnC,UAAQ,WAAW;AACrB,OAAO;AACL,QAAM,OAAO,WAAW;AAC1B;","names":[]}
|
package/dist/main.js
CHANGED
|
@@ -301,15 +301,16 @@ var VOCABULARY = {
|
|
|
301
301
|
modelRing: "violet ring: the entry reaches the model <- entries[].model",
|
|
302
302
|
reachedBy: "under each resource in the rack: how many routes reach it, the resource drawn at that share of the routes (never smaller than its name) <- entries[].db, model, net, beside.resources",
|
|
303
303
|
netRing: "orange ring: the entry reaches the network <- entries[].net",
|
|
304
|
-
guard:
|
|
304
|
+
guard: 'red diamond on the path where the route can refuse, the check named above it (the program\u2019s definition it calls, "<table> row found", "<table> row\u2019s <field>" for a branch on a field of a read row, or "a test in <definition>" for a comparison or library answer that definition branches on), failure stem below; a branch no path refuses on is not drawn here <- entries[].guards (structure.scenarios[]: branches where one arm ends only in refusals), wrappers[].guards, entries[].passed[].guards, entries[].beside.guards',
|
|
305
|
+
policy: "under the db, each table with its access policy as the access check proves it: own = each caller reads and writes only their own row; no row rule = the access check holds no rule over its rows, so who reads or writes which row is decided only by the code drawn on the routes; guarded = rows pass the named guard; role \u2026 only = only callers with that role reach its rows; unchecked = the access check gave this table no verdict; a policy ending \u201Cbroken\u201D is one the code breaks, and \u201Cnot proven\u201D one the check could not settle; counted when the tables are too many to list <- resources[kind=db].policies",
|
|
305
306
|
hull: "translucent region in the hull band: paths merge into it, run one shared stretch, and split; everything inside it runs inside the wrapper; its right wall, drawn in the hull ink, is where the wrapper returns <- wrappers[]",
|
|
306
307
|
socket: "broken-rim ring: a function the caller hands in runs here; its centre fills like a station <- entries[].passed[]",
|
|
307
308
|
token: "small grey word under a path: the value the caller fixes for the wrapper <- entries[].policy",
|
|
308
309
|
afterDot: "dot in the after band: a call the entry makes after its wrapper returns <- entries[].beside.calls",
|
|
309
310
|
builder: "plus-disc under a station with the call that builds the handle there <- uses[how=local].builtBy",
|
|
310
311
|
unknown: 'a ? in a circle on the rail, with "partly traced" beside it and a sentence on hover: the engine traced the handle only on some paths <- uses[how=mixed]',
|
|
311
|
-
fork:
|
|
312
|
-
bundle: 'one cable with how many routes it carries ("4 routes"): entries with the same wrapper and identical marks; its title names every route; opening it draws each one <- bundles[]',
|
|
312
|
+
fork: 'three prongs and a type: a choice keyed on a plain String or Int; under the mark that makes it (under a list of checks, named by the check it is inside: "can_see on String"), else on the quiet line <- keyed[]',
|
|
313
|
+
bundle: 'one cable with how many routes it carries ("4 routes"): entries with the same wrapper and identical marks; its title names every route: by the one part of the path that differs, among at most six ("/{tags, tasks}/sync"), or each by name when there are at most four; else a pattern with *; opening it draws each one <- bundles[]',
|
|
313
314
|
anchor: "where the rail starts: platform when no definition of the program can make the handle; ? when the trace stops and the program could <- uses[].anchor",
|
|
314
315
|
step: 'loop arc with a type on the quiet line: functions of the package take that type and return it; gathered, a count in words ("8 steps"), as are choices, db uses and generic parts <- steps[]',
|
|
315
316
|
loop: "loop arc with package names: packages whose types lead back to themselves <- phases.cycles",
|
|
@@ -330,6 +331,7 @@ var VOCABULARY = {
|
|
|
330
331
|
replyLoop: `one solid arc from a lane's foot back into the program, with one loop node naming what repeats the step within one call, and "each call of \u2026 runs it" naming the routes that run the rounds <- programs[].engines[].replies, loop, starters`,
|
|
331
332
|
replaced: 'a check named "(in place of \u2026)": in the compared base record the same route had that other check at the same place, and it is on that route no longer; says nothing about whether the two do the same <- compare.replaced[]'
|
|
332
333
|
},
|
|
334
|
+
names: "every check, call and table name is a label the code's author (often an AI) chose; the picture proves where a check runs, what it is given and what happens when it fails, never what it decides: a reader who doubts the author reads that rule itself.",
|
|
333
335
|
rule: "every guard, wrapper, socket, fork, handle, station, step and resource the engine records is drawn at its fixed place, in every view; where nothing is drawn, the engine checked and found none."
|
|
334
336
|
};
|
|
335
337
|
var bare = (id) => String(id).split("::").pop();
|
|
@@ -449,9 +451,51 @@ function programsOf(st, layers = null) {
|
|
|
449
451
|
});
|
|
450
452
|
}
|
|
451
453
|
var outsideResourcesOf = (steps) => uniq((steps || []).map((e) => e.resource).filter((r) => r === "model" || r === "net"));
|
|
454
|
+
var leavesUnder = (t) => t.kind === "leaf" ? [t] : t.arms.flatMap((a) => leavesUnder(a.next));
|
|
455
|
+
function refusingForks(tree) {
|
|
456
|
+
const out = [];
|
|
457
|
+
const go = (t) => {
|
|
458
|
+
if (!t || t.kind !== "node") return;
|
|
459
|
+
const refused = t.arms.map((a) => leavesUnder(a.next).every((l) => l.outcome.kind === "refused"));
|
|
460
|
+
if (t.concern?.layer !== "platform" && refused.some(Boolean) && !refused.every(Boolean)) out.push(t);
|
|
461
|
+
t.arms.forEach((a) => go(a.next));
|
|
462
|
+
};
|
|
463
|
+
go(tree);
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
var forkId = (n, own) => n.from.kind === "call" && own.has(n.from.callee) ? n.from.callee : n.id;
|
|
467
|
+
var forkParts = (n, own) => n.from.kind === "call" && own.has(n.from.callee) ? { definition: n.from.callee } : n.from.kind === "read" && n.from.table ? { table: n.from.table, ...n.from.path?.length ? { field: n.from.path.join(".") } : {} } : n.from.site ? { definition: n.from.site } : {};
|
|
468
|
+
var forkKey = (own) => (n) => n.from.kind === "call" && own.has(n.from.callee) ? n.from.callee : `${forkLabel(n, own)} ${n.from.site}`;
|
|
469
|
+
var forkLabel = (n, own) => n.from.kind === "call" ? own.has(n.from.callee) ? bare(n.from.callee) : `a test in ${bare(n.from.site)}` : n.from.kind === "read" && n.from.table ? n.from.path?.length ? `${n.from.table} row\u2019s ${n.from.path.join(".")}` : `${n.from.table} row found` : n.from.site ? `a test in ${bare(n.from.site)}` : "a computed branch";
|
|
470
|
+
var policyWord = (r) => {
|
|
471
|
+
const rule = policyRule(r);
|
|
472
|
+
if (rule === "unchecked") return rule;
|
|
473
|
+
return r.status === "violated" ? `${rule}, broken` : r.proven ? rule : `${rule}, not proven`;
|
|
474
|
+
};
|
|
475
|
+
var policyRule = (r) => {
|
|
476
|
+
const roles = () => r.roles.join(", "), guards = () => r.guards.join(", ");
|
|
477
|
+
switch (r.policyKind) {
|
|
478
|
+
case "OwnerScoped":
|
|
479
|
+
return "own";
|
|
480
|
+
case "Public":
|
|
481
|
+
return "no row rule";
|
|
482
|
+
case "Guarded":
|
|
483
|
+
return `guarded by ${guards()}`;
|
|
484
|
+
// with no guard, only the roles reach its rows
|
|
485
|
+
case "GuardedOrRoles":
|
|
486
|
+
return r.guards.length ? `guarded by ${guards()} or role ${roles()}` : `role ${roles()} only`;
|
|
487
|
+
case "PublicReadRoleWrite":
|
|
488
|
+
return `anyone reads; role ${roles()} writes`;
|
|
489
|
+
default:
|
|
490
|
+
return "unchecked";
|
|
491
|
+
}
|
|
492
|
+
};
|
|
452
493
|
function buildSystemMap(engine, { program }) {
|
|
453
494
|
for (const [k, v] of Object.entries(engine)) if (!v.ok) throw new Error(`${k}: ${v.error}`);
|
|
454
495
|
const report = engine.report, st = engine.structure, abs = engine.abstractions.abstractions, layers = engine.layers.layers;
|
|
496
|
+
const whole = (s) => s.tree && !s.truncated && !s.unsupported;
|
|
497
|
+
const trees = new Map((st.scenarios || []).filter(whole).map((s) => [s.entry, refusingForks(s.tree)]));
|
|
498
|
+
const ownDefs = new Set(layers.packages.flatMap((p) => p.definitions.map((d) => `${p.name}::${d.name}`)));
|
|
455
499
|
const routes = (report.surface.routes || []).slice().sort((a, b) => a.path.localeCompare(b.path));
|
|
456
500
|
const engEntry = new Map(st.entries.map((e) => [e.entry, e]));
|
|
457
501
|
const shapes = new Map(abs.routeShapes.map((r) => [r.path, r]));
|
|
@@ -466,11 +510,17 @@ function buildSystemMap(engine, { program }) {
|
|
|
466
510
|
resources: outsideResourcesOf(inst.besideEffectSteps),
|
|
467
511
|
from: "abstractions.instantiations[].beside*"
|
|
468
512
|
});
|
|
513
|
+
const resultOf = (sig) => sig.includes("->") ? sig.slice(sig.lastIndexOf("->") + 2).trim() : null;
|
|
514
|
+
const returnsCallers = (g) => {
|
|
515
|
+
const result = resultOf(g.signature);
|
|
516
|
+
return result !== null && (/^[a-z]\w*$/.test(result) || (g.parameterGroups || []).flat().some((sl) => sl.role === "function" && sl.returnType === result));
|
|
517
|
+
};
|
|
518
|
+
const wraps = (g) => !!g && (/\bIO\[/.test(g.signature) || g.envelope.effects.length > 0 || returnsCallers(g) && (g.envelope.guards.length > 0 || g.envelope.dispatchesOn.length > 0));
|
|
469
519
|
const entries = routes.map((r) => {
|
|
470
520
|
const key = `route:${r.path}`;
|
|
471
521
|
const ee = engEntry.get(key);
|
|
472
522
|
const rs = shapes.get(r.path);
|
|
473
|
-
const wrapper = rs.shapedByGenerator;
|
|
523
|
+
const wrapper = wraps(gens.get(rs.shapedByGenerator)) ? rs.shapedByGenerator : null;
|
|
474
524
|
const inst = wrapper ? insts.find((i) => i.callerId === rs.handlerId && i.generatorId === wrapper) : null;
|
|
475
525
|
const passed = (inst ? inst.slotBindings : []).filter((sb) => ["function", "effect"].includes(sb.slotRole)).map((sb) => ({
|
|
476
526
|
slot: sb.slotName,
|
|
@@ -507,8 +557,9 @@ function buildSystemMap(engine, { program }) {
|
|
|
507
557
|
model: ee.resources.includes("model"),
|
|
508
558
|
net: ee.resources.includes("net"),
|
|
509
559
|
writes: reach.some((e) => isWrite(e.op)),
|
|
510
|
-
|
|
511
|
-
|
|
560
|
+
// every branch of the entry's own where a path refuses (a wrapper's are
|
|
561
|
+
// its own, and a function handed to it keeps its own slot)
|
|
562
|
+
...trees.has(key) ? ((forks) => ({ guards: forks.map((n) => forkLabel(n, ownDefs)), guardIds: forks.map((n) => forkId(n, ownDefs)), guardParts: forks.map((n) => forkParts(n, ownDefs)) }))(uniqBy(trees.get(key).filter((n) => n.concern?.layer !== "cross" && !(inst ? inst.slotBindings : []).some((sb) => (sb.guards || []).includes(forkId(n, ownDefs)))), forkKey(ownDefs))) : { guards: rs.handlerGuards.map(bare), guardIds: rs.handlerGuards },
|
|
512
563
|
wrapper,
|
|
513
564
|
via: rs.viaSpecialization ? bare(rs.viaSpecialization) : null,
|
|
514
565
|
policy: (inst ? inst.slotBindings : []).filter((s) => s.slotRole === "semantic-object").map((s) => s.value).filter(Boolean),
|
|
@@ -598,7 +649,12 @@ function buildSystemMap(engine, { program }) {
|
|
|
598
649
|
}
|
|
599
650
|
const present = uniq(st.ioKinds.flatMap((b) => b.resources));
|
|
600
651
|
const tables = uniq(entries.flatMap((e) => [...e.reach, ...e.beside ? e.beside.effects : []].map((r) => r.table))).sort();
|
|
601
|
-
const
|
|
652
|
+
const policies = engine.access ? Object.fromEntries(engine.access.report.resources.filter((r) => tables.includes(r.resource)).map((r) => [r.resource, policyWord(r)])) : null;
|
|
653
|
+
const resources = ["db", "model", "net"].filter((k) => present.includes(k)).map((k) => ({
|
|
654
|
+
kind: k,
|
|
655
|
+
...k === "db" ? { tables, ...policies ? { policies, policyFrom: "access.report.resources[].policyKind, guards, roles" } : {} } : {},
|
|
656
|
+
from: "structure.ioKinds[].resources"
|
|
657
|
+
}));
|
|
602
658
|
const ghostResources = ["db", "model", "net"].filter((k) => !present.includes(k)).map((k) => ({ kind: k, from: "structure.ioKinds[] checked: no site of this kind" }));
|
|
603
659
|
const transports = uniq(st.entries.map((e) => e.transport));
|
|
604
660
|
const callers = [];
|
|
@@ -712,13 +768,22 @@ function bundlesOf(entries, uses, keyed) {
|
|
|
712
768
|
}
|
|
713
769
|
function pathPattern(paths) {
|
|
714
770
|
if (paths.length === 1) return paths[0];
|
|
771
|
+
const sorted = [...paths].sort();
|
|
715
772
|
const segs = paths.map((p) => p.split("/"));
|
|
716
773
|
const n = Math.min(...segs.map((s) => s.length));
|
|
717
774
|
const out = [];
|
|
775
|
+
const vary = [];
|
|
718
776
|
for (let i = 0; i < n; i++) {
|
|
719
777
|
const v = uniq(segs.map((s) => s[i]));
|
|
720
778
|
out.push(v.length === 1 ? v[0] : "*");
|
|
779
|
+
if (v.length > 1) vary.push([i, v]);
|
|
780
|
+
}
|
|
781
|
+
if (segs.every((s) => s.length === n) && vary.length === 1 && vary[0][1].length <= 6) {
|
|
782
|
+
const [[i, v]] = vary;
|
|
783
|
+
out[i] = `{${[...v].sort().join(", ")}}`;
|
|
721
784
|
}
|
|
785
|
+
if (segs.every((s) => s.length === n) && vary.length <= 1) return out.join("/");
|
|
786
|
+
if (paths.length <= 4) return sorted.join(", ");
|
|
722
787
|
if (segs.every((s) => s.length === n)) return out.join("/");
|
|
723
788
|
let k = 0;
|
|
724
789
|
while (k < n && out[k] !== "*") k++;
|
|
@@ -750,6 +815,7 @@ var STRUCTURAL = /* @__PURE__ */ new Set([
|
|
|
750
815
|
"vars",
|
|
751
816
|
"id"
|
|
752
817
|
]);
|
|
818
|
+
var DESCRIBED = /* @__PURE__ */ new Set(["guards", "policies", "policyFrom"]);
|
|
753
819
|
var WHAT = {
|
|
754
820
|
path: "an action the app answers",
|
|
755
821
|
routes: "an action the app answers",
|
|
@@ -758,7 +824,8 @@ var WHAT = {
|
|
|
758
824
|
roles: "a permission an action requires, held by a person's role or by the app's own server",
|
|
759
825
|
policy: "a setting an action passes to shared code",
|
|
760
826
|
tokens: "a setting an action passes to shared code",
|
|
761
|
-
fields: "an operation a pluggable part offers"
|
|
827
|
+
fields: "an operation a pluggable part offers",
|
|
828
|
+
field: "a field of a kind of record the app keeps"
|
|
762
829
|
};
|
|
763
830
|
var PACKAGE = "a section of the program";
|
|
764
831
|
var DEFINITION = "a piece of the program";
|
|
@@ -781,7 +848,7 @@ var leaves = (record, visit) => {
|
|
|
781
848
|
if (typeof v === "string") visit(v, key);
|
|
782
849
|
else if (Array.isArray(v)) v.forEach((x) => walk3(x, key));
|
|
783
850
|
else if (v && typeof v === "object") {
|
|
784
|
-
for (const [k, x] of Object.entries(v)) if (!skipped(k, x)) walk3(x, k);
|
|
851
|
+
for (const [k, x] of Object.entries(v)) if (!skipped(k, x) && !DESCRIBED.has(k)) walk3(x, k);
|
|
785
852
|
}
|
|
786
853
|
};
|
|
787
854
|
walk3(record, null);
|
|
@@ -799,6 +866,7 @@ function nameableParts(map, own) {
|
|
|
799
866
|
add(id.slice(i + 2), DEFINITION);
|
|
800
867
|
};
|
|
801
868
|
leaves(map, (s, key) => {
|
|
869
|
+
if (key === "guardIds" && s.startsWith("route:")) return;
|
|
802
870
|
if (s.startsWith("route:")) return add(s.slice(6), WHAT.path);
|
|
803
871
|
if (TYPES.has(key)) {
|
|
804
872
|
for (const w of s.match(WORD) ?? []) if (w.includes("::")) idParts(w);
|
|
@@ -863,19 +931,22 @@ var VOCABULARY2 = {
|
|
|
863
931
|
version: MARKS_VERSION,
|
|
864
932
|
added: ["outcome"],
|
|
865
933
|
marks: {
|
|
866
|
-
entryHead:
|
|
934
|
+
entryHead: 'arrowhead where a request comes in; the rack above the tree has one per described entry, then one dashed line counting the entries no scenario describes, which opens to draw each of them struck (when no entry is described, that one line is all the rack draws, and it only counts them: "8 entries"; a scenario is a requirement someone wrote, so its absence says nothing about tests); under the head, "same steps as \u2026" names the entries whose trees take the same steps on their own tables and checks <- entries[], ghosts[], entries[].sameShape',
|
|
867
935
|
guard: "red diamond: the declared sign-in or role check, the first branch of every entry that has one; its failure stem ends in a refusal <- tree node with from.kind=guard",
|
|
868
|
-
fork:
|
|
869
|
-
|
|
936
|
+
fork: 'three prongs: a branch the code takes on the request, the caller, a value read from a table, or a call result, with "\u2190 request", "\u2190 caller" or "\u2190 <table> as read" saying where what it tests comes from; the arm each line takes is written where it leaves <- tree nodes, nodes[].from.sources',
|
|
937
|
+
docs: "under the names line, in italic, the comment the author wrote above each named check, marked unchecked: what the author says it decides, which the engine does not confirm <- docs (structure.docs)",
|
|
938
|
+
rows: `under the tree, for each table its stations touch (and those of the entries named in "same steps as"), what one row holds, by its checked type: "posts holds List[Post(id: String, \u2026, edited: Bool)]"; a field that is a record gives only its type's name, a choice of bare cases its cases ("visibility: Open | Members") <- rows (structure.rows)`,
|
|
939
|
+
station: "ring on a line: an effect on that path, its verb and table beside it; filled = it writes; mint = the platform hands out a fresh row id. Under it, in grey, the row it touches (the caller's row; the row named by a request field; one fixed row; a new row; every row, and the guard that keeps them) and, for a write, whether the row is replaced whole or updated from what it read, then where what it writes comes from, field by field (\u2190 caller, \u2190 request.<field>, \u2190 a literal, \u2190 new id, \u2190 a call and what it was computed from) <- arms[].effects[].key, value, kept (access report.findings[operation=keep_visible].provenBy)",
|
|
870
940
|
modelRing: "violet ring: the path reaches the model <- effects[resource=model]",
|
|
871
941
|
netRing: "orange ring: the path reaches the network <- effects[resource=net]",
|
|
872
942
|
outcome: "the end of a path: a cap with what the request gets back (a constructor, or a refusal with its status and code); red when a claim fails there or a check reports a witness that ends there, green when every obligation that failed there at the base is checked here and holds, hollow and dashed (unchecked) when a claim there is inconclusive or an obligation that failed at the base is no longer checked, grey otherwise <- leaves",
|
|
873
943
|
unchecked: "a hollow dashed cap and a ? tick: the engine cannot say whether the claim holds on this path (a step it could not open, a table it could not name, an answer it could not decide), or what failed here at the base is not checked here; never counted as holding <- claims[verdict=inconclusive], leaves[].gaps, diff.unresolved",
|
|
874
944
|
struck: "a struck ghost: something checked and found absent (an entry nobody described, a write a claim says a path must not make) <- ghosts[], claims holding with not",
|
|
875
945
|
bundle: "a cable with a count: a subtree too large for this view; opening it draws that part with a breadcrumb <- derived from the tree",
|
|
876
|
-
compared: 'a line under the rack
|
|
946
|
+
compared: 'a line under the rack when a base was compared: "compared with <base> \xB7 N fixed \xB7 M still failing"; with no base, no line <- diff',
|
|
877
947
|
hull: `a shaded box on the line: a step that is not the handler's own work, folded (a wrapper's guard or short-circuit, the platform's declared check or the transaction it runs the plan in, work beside the wrapper, a wrapper's work after the handler); it keeps what the step answers instead of running the handler: the effects it makes before its test above the arms, then one row per arm read as a side stub (the arm, the effects that arm makes, its struck writes, what the request gets, ticks), then "continue"; opening it draws the step's own tree; drawn inline, the same hull is a light band behind the step and its exits, with its role and where it comes from above its test, so either way it reads as not the handler's own work <- tree nodes[concern.role], entries[].steps`
|
|
878
948
|
},
|
|
949
|
+
names: "every check, call and table name is a label the code's author (often an AI) chose; the picture proves where a check runs, what it is given and what happens when it fails, never what it decides: a reader who doubts the author reads that rule itself.",
|
|
879
950
|
rule: "every entry, branch, arm, effect and outcome the engine computes is drawn, a folded step's inside its box and an undescribed entry behind the rack's one line; the author's words appear only on ids the engine computed, in italic, with the code name beside them.",
|
|
880
951
|
concerns: "where each branch and effect comes from in value flow (concern.layer, effects[].layer): core = the handler's own work, a function it hands its wrapper or its body when it has none; cross = the wrapper its route shape names, shared with other entries; beside = the handler's code outside the wrapper call; platform = the route's declared authority, or the transaction the platform runs a plan in (its arms that give up or cannot confirm skip the handler's work). Roles come from which arms reach the handler, never from names."
|
|
881
952
|
};
|
|
@@ -953,6 +1024,11 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
953
1024
|
const claims = st.claims || { scenarios: [], undescribed: (st.scenarios || []).map((s) => s.entry) };
|
|
954
1025
|
const byEntry = new Map(claims.scenarios.filter((s) => s.status === "matched").map((s) => [s.entry, s]));
|
|
955
1026
|
const routes = new Map((st.entries || []).map((e) => [e.entry, e]));
|
|
1027
|
+
const kept = new Map((engine.access?.report?.findings || []).filter((f) => f.operation === "keep_visible" && f.kind === "GuardProven" && f.status === "holds").map((f) => [`${f.package}::${f.binding}\0${f.resource}`, `${(f.provenBy || []).join(" and ")} passes for the caller and the row`]));
|
|
1028
|
+
const withKept = (es) => es.map((e) => {
|
|
1029
|
+
const by = e.kind === "DbQuery" && e.tables.length === 1 ? kept.get(`${e.site}\0${e.tables[0]}`) : null;
|
|
1030
|
+
return by ? { ...e, kept: by } : e;
|
|
1031
|
+
});
|
|
956
1032
|
const entries = st.scenarios.map((s) => {
|
|
957
1033
|
const meta = routes.get(s.entry) || {};
|
|
958
1034
|
const sc = byEntry.get(s.entry);
|
|
@@ -960,7 +1036,7 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
960
1036
|
const copy = (t) => {
|
|
961
1037
|
if (!t) return null;
|
|
962
1038
|
if (t.kind === "leaf") return { ...t, words: names.get(t.id) || null, claims: [], witnesses: [], status: "grey" };
|
|
963
|
-
return { ...t, words: names.get(t.id) || null, arms: t.arms.map((a) => ({ ...a, words: names.get(a.id) || null, next: copy(a.next) })) };
|
|
1039
|
+
return { ...t, words: names.get(t.id) || null, arms: t.arms.map((a) => ({ ...a, effects: withKept(a.effects), words: names.get(a.id) || null, next: copy(a.next) })) };
|
|
964
1040
|
};
|
|
965
1041
|
return {
|
|
966
1042
|
entry: s.entry,
|
|
@@ -970,7 +1046,7 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
970
1046
|
transport: meta.transport || null,
|
|
971
1047
|
title: sc?.title || null,
|
|
972
1048
|
described: !!sc,
|
|
973
|
-
effects: s.effects,
|
|
1049
|
+
effects: withKept(s.effects),
|
|
974
1050
|
tree: copy(s.tree),
|
|
975
1051
|
leafCount: Number(s.leafCount),
|
|
976
1052
|
truncated: s.truncated,
|
|
@@ -980,6 +1056,17 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
980
1056
|
from: "structure.scenarios[]"
|
|
981
1057
|
};
|
|
982
1058
|
});
|
|
1059
|
+
const shapeOf = (e) => {
|
|
1060
|
+
const eff = (x) => `${x.op}${x.key ? `@${x.key.kind}` : ""}${x.value ? `<${x.value.kind}` : ""}${x.kept ? "|kept" : ""}`;
|
|
1061
|
+
const go = (t) => !t ? "" : t.kind === "leaf" ? `(${t.outcome.kind}:${(t.outcome.statuses || (t.outcome.status ? [t.outcome.status] : [])).join("/")})` : `[${t.from.kind}${t.arms.map((a) => `{${a.effects.map(eff).join(",")}${go(a.next)}}`).join("")}]`;
|
|
1062
|
+
return `${(e.effects || []).map(eff).join(",")}${go(e.tree)}`;
|
|
1063
|
+
};
|
|
1064
|
+
const byShape = /* @__PURE__ */ new Map();
|
|
1065
|
+
for (const e of entries) byShape.set(shapeOf(e), [...byShape.get(shapeOf(e)) || [], e.entry]);
|
|
1066
|
+
for (const e of entries) {
|
|
1067
|
+
const same = byShape.get(shapeOf(e)).filter((x) => x !== e.entry);
|
|
1068
|
+
if (same.length) e.sameShape = { entries: same, from: "derived: entries[] trees equal in node kinds, arms, effect verbs, row keys and value kinds, and outcomes" };
|
|
1069
|
+
}
|
|
983
1070
|
const entryOf = new Map(entries.map((e) => [e.entry, e]));
|
|
984
1071
|
const leafById = new Map(entries.flatMap((e) => leavesOf(e.tree).map((l) => [l.id, l])));
|
|
985
1072
|
const checked = [], notes = [], refused = [], mismatches = [];
|
|
@@ -1085,6 +1172,18 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
1085
1172
|
refused,
|
|
1086
1173
|
mismatches,
|
|
1087
1174
|
diff,
|
|
1175
|
+
// the comment above each definition, as its author wrote it (structure.docs); never checked
|
|
1176
|
+
docs: ((callees) => Object.fromEntries(Object.entries(st.docs || {}).filter(([id]) => callees.has(id))))(
|
|
1177
|
+
new Set(entries.flatMap((e) => {
|
|
1178
|
+
const ids = [];
|
|
1179
|
+
walk(e.tree, (n) => {
|
|
1180
|
+
if (n.kind === "node" && n.from?.kind === "call") ids.push(n.from.callee);
|
|
1181
|
+
});
|
|
1182
|
+
return ids;
|
|
1183
|
+
}))
|
|
1184
|
+
),
|
|
1185
|
+
// what each table holds, by its checked row type (structure.rows)
|
|
1186
|
+
rows: Object.fromEntries(Object.entries(st.rows || {}).map(([table, schema]) => [table, rowShape(schema)])),
|
|
1088
1187
|
counts: {
|
|
1089
1188
|
entries: entries.length,
|
|
1090
1189
|
described: entries.length - ghosts.length,
|
|
@@ -1098,6 +1197,14 @@ function buildWhatHappens(engine, { program, base = null, baseLabel = "base" } =
|
|
|
1098
1197
|
evidence: { tools: ["api_organize:structure", ...engine.access ? ["api_access_check"] : []], derived: ["diff: base red leaves matched by id, their failed obligations by identity"] }
|
|
1099
1198
|
};
|
|
1100
1199
|
}
|
|
1200
|
+
var typeName = (s) => s.type ? s.type.split("::").pop() : null;
|
|
1201
|
+
var fieldType = (s) => ({ string: "String", int: "Int", float64: "Float64", bool: "Bool", unit: "Unit", json: "Json" })[s.kind] || (s.kind === "list" ? `List[${fieldType(s.element)}]` : s.kind === "unsupported" ? s.rendered : s.kind === "union" && s.variants.length && s.variants.every((v) => !v.fields.length) ? s.variants.map((v) => v.tag).join(" | ") : typeName(s));
|
|
1202
|
+
function rowShape(s) {
|
|
1203
|
+
if (s.kind === "list") return `List[${rowShape(s.element)}]`;
|
|
1204
|
+
if (s.kind === "struct") return `${typeName(s)}(${s.fields.map((f) => `${f.name}: ${fieldType(f)}`).join(", ")})`;
|
|
1205
|
+
if (s.kind === "union") return s.variants.map((v) => v.fields.length ? `${v.tag}(${v.fields.map((f) => `${f.name}: ${fieldType(f)}`).join(", ")})` : v.tag).join(" | ");
|
|
1206
|
+
return fieldType(s);
|
|
1207
|
+
}
|
|
1101
1208
|
function stationLabel(e) {
|
|
1102
1209
|
const verb = e.op === "mint id" ? "mint" : e.op;
|
|
1103
1210
|
if (e.tables.length) return `${verb} \xB7 ${e.tables.join(", ")}`;
|
|
@@ -2004,9 +2111,10 @@ async function direct({ root, engine, requirements, withMap = true, previous = n
|
|
|
2004
2111
|
base.lawsError = checked.error;
|
|
2005
2112
|
base.rules = (requirements.rules ?? []).map((r) => judgeRule(r, base.laws));
|
|
2006
2113
|
base.hasActions = report.surface.routes.length > 0;
|
|
2114
|
+
let access = null;
|
|
2007
2115
|
if (base.hasActions || requirements.scenarios?.length) {
|
|
2008
2116
|
try {
|
|
2009
|
-
|
|
2117
|
+
access = await call(engine, "api_access_check", { sources });
|
|
2010
2118
|
const record = WhatHappens.parse(buildWhatHappens({ structure: structureRaw, access }, { program: base.project }));
|
|
2011
2119
|
base.whatHappens = base.hasActions ? record : { ...record, entries: [], ghosts: [], claims: [], witnesses: [], refused: [], notes: [] };
|
|
2012
2120
|
} catch (error) {
|
|
@@ -2023,7 +2131,7 @@ async function direct({ root, engine, requirements, withMap = true, previous = n
|
|
|
2023
2131
|
await call(engine, "api_organize", { sources, lens: "abstractions" }),
|
|
2024
2132
|
await call(engine, "api_organize", { sources, lens: "layers" })
|
|
2025
2133
|
];
|
|
2026
|
-
base.map = buildSystemMap({ report, structure: structureRaw, abstractions, layers }, { program: base.project });
|
|
2134
|
+
base.map = buildSystemMap({ report, structure: structureRaw, abstractions, layers, ...access ? { access } : {} }, { program: base.project });
|
|
2027
2135
|
base.layers = layers;
|
|
2028
2136
|
} catch (error) {
|
|
2029
2137
|
base.mapError = error instanceof Error ? error.message : String(error);
|
|
@@ -2419,7 +2527,8 @@ async function refresh() {
|
|
|
2419
2527
|
const { state, requirements } = await (await fetch('/state')).json();
|
|
2420
2528
|
const savedOf = id => requirements.answers.find(a => a.question === id);
|
|
2421
2529
|
if (requirements.description) document.getElementById('description').textContent = requirements.description;
|
|
2422
|
-
|
|
2530
|
+
// Before the first check there is nothing drawn, but the person may already be looking.
|
|
2531
|
+
if (!state) { viewing(null); return; }
|
|
2423
2532
|
document.getElementById('project').textContent = ': ' + state.project;
|
|
2424
2533
|
document.getElementById('updated').textContent = 'Last checked ' + new Date(state.updatedAt).toLocaleTimeString() + '.';
|
|
2425
2534
|
document.getElementById('summary').textContent = state.summary;
|
|
@@ -2488,7 +2597,14 @@ var Report2 = z9.object({
|
|
|
2488
2597
|
/** The drawing the anchors belong to (person.ts), or null before one is drawn. */
|
|
2489
2598
|
drawing: z9.string().max(64).nullable()
|
|
2490
2599
|
}).strict();
|
|
2491
|
-
var Viewing = Report2.extend({
|
|
2600
|
+
var Viewing = Report2.extend({
|
|
2601
|
+
schemaVersion: z9.literal("yichus-viewing-1"),
|
|
2602
|
+
at: z9.string(),
|
|
2603
|
+
/** The page server the report came through: only it can tell that its page went. */
|
|
2604
|
+
server: z9.string().optional(),
|
|
2605
|
+
/** When the last copy of the page open on that server went away, until a page connects again. */
|
|
2606
|
+
closed: z9.string().optional()
|
|
2607
|
+
}).strict();
|
|
2492
2608
|
var viewingPath = (root) => path5.join(studioDir(root), "viewing.json");
|
|
2493
2609
|
function loadViewing(root) {
|
|
2494
2610
|
const file = viewingPath(root);
|
|
@@ -2497,14 +2613,28 @@ function loadViewing(root) {
|
|
|
2497
2613
|
if (!parsed.success) throw new Error(`${file} is not a yichus-viewing-1 file: ${z9.prettifyError(parsed.error)}`);
|
|
2498
2614
|
return parsed.data;
|
|
2499
2615
|
}
|
|
2500
|
-
function
|
|
2501
|
-
const parsed = Report2.safeParse(report);
|
|
2502
|
-
if (!parsed.success) return { ok: false, error: `The page's report is not one this studio reads. ${z9.prettifyError(parsed.error)}` };
|
|
2616
|
+
function writeViewing(root, viewing) {
|
|
2503
2617
|
ensureStudioDir(root);
|
|
2504
2618
|
const file = viewingPath(root), written = `${file}.${process.pid}.tmp`;
|
|
2505
|
-
fs5.writeFileSync(written, `${JSON.stringify(Viewing.parse(
|
|
2619
|
+
fs5.writeFileSync(written, `${JSON.stringify(Viewing.parse(viewing), null, 2)}
|
|
2506
2620
|
`);
|
|
2507
2621
|
fs5.renameSync(written, file);
|
|
2622
|
+
}
|
|
2623
|
+
function recordClosed(root, server, now) {
|
|
2624
|
+
const last = loadViewing(root);
|
|
2625
|
+
if (last?.server === server) writeViewing(root, { ...last, closed: now.toISOString() });
|
|
2626
|
+
}
|
|
2627
|
+
function recordOpened(root, server) {
|
|
2628
|
+
const last = loadViewing(root);
|
|
2629
|
+
if (last?.server === server && last.closed) {
|
|
2630
|
+
const { closed: _, ...open } = last;
|
|
2631
|
+
writeViewing(root, open);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
function recordViewing(root, server, report, now) {
|
|
2635
|
+
const parsed = Report2.safeParse(report);
|
|
2636
|
+
if (!parsed.success) return { ok: false, error: `The page's report is not one this studio reads. ${z9.prettifyError(parsed.error)}` };
|
|
2637
|
+
writeViewing(root, { schemaVersion: "yichus-viewing-1", at: now.toISOString(), server, ...parsed.data });
|
|
2508
2638
|
return { ok: true };
|
|
2509
2639
|
}
|
|
2510
2640
|
|
|
@@ -2611,7 +2741,10 @@ function pageText(root, pageUrl, now, surface) {
|
|
|
2611
2741
|
`;
|
|
2612
2742
|
}
|
|
2613
2743
|
function inViewLines(viewing, codeAt, said2, now, pageUrl) {
|
|
2614
|
-
|
|
2744
|
+
const link = pageUrl ? `Give the person its link: ${pageUrl}` : "The page is not running: start `yichus studio --page` in the background in this folder, then run this again for its link.";
|
|
2745
|
+
if (!viewing) return [`The page has not said what is on screen: it may not be open. ${link}`];
|
|
2746
|
+
if (viewing.closed) return [`The person closed the page ${ago(viewing.closed, now)}, so it is not open now. ${pageUrl ? `If you want them to look at it, give them its link again: ${pageUrl}` : link}`];
|
|
2747
|
+
if (!pageUrl) return [`The page is not open: its server stopped after it last said what was on screen. ${link}`];
|
|
2615
2748
|
const parts = (anchors) => !viewing.current ? [] : anchors.flatMap((a) => codeAt.has(a) ? [`[${codeAt.get(a)}] ${said2.get(codeAt.get(a))}`] : []);
|
|
2616
2749
|
const list = (what, items) => items.length ? [`${what}:`, ...items.map((i) => ` ${i}`)] : [];
|
|
2617
2750
|
const { touched } = viewing;
|
|
@@ -2953,7 +3086,7 @@ function pageNote(pageUrl, say2) {
|
|
|
2953
3086
|
].join("\n");
|
|
2954
3087
|
}
|
|
2955
3088
|
function createStudioMcpServer({ root, engine, catalog, pageUrl, now = () => /* @__PURE__ */ new Date(), locate = builtCopy }) {
|
|
2956
|
-
const server = new Server({ name: "yichus-studio", version: "0.3.
|
|
3089
|
+
const server = new Server({ name: "yichus-studio", version: "0.3.3" }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
|
|
2957
3090
|
const active = /* @__PURE__ */ new Set();
|
|
2958
3091
|
const track = (task) => {
|
|
2959
3092
|
active.add(task);
|
|
@@ -3222,7 +3355,7 @@ ${said2}
|
|
|
3222
3355
|
var HELP = `yichus \u2014 the Yichus WebMCP analysis engine, locally
|
|
3223
3356
|
|
|
3224
3357
|
yichus tools
|
|
3225
|
-
yichus call TOOL [--args JSON_FILE|-] [--source FILE ... | --project DIR]
|
|
3358
|
+
yichus call TOOL [--args JSON_FILE|-] [--arg-file NAME=JSON_FILE ...] [--source FILE ... | --project DIR]
|
|
3226
3359
|
yichus verify FILE ... [--instances 1|many] [--block-on-warning ID ...]
|
|
3227
3360
|
yichus lean FILE ... --lean-wasm-root DIRECTORY [--timeout-ms N]
|
|
3228
3361
|
${STUDIO_HELP}
|
|
@@ -3234,6 +3367,8 @@ Options:
|
|
|
3234
3367
|
(default the current folder), as the studio reads
|
|
3235
3368
|
it (call only).
|
|
3236
3369
|
--args FILE|- Tool arguments as a JSON object; - reads stdin.
|
|
3370
|
+
--arg-file NAME=FILE Set one argument to the JSON in FILE, such as a
|
|
3371
|
+
saved snapshot (repeatable; call only).
|
|
3237
3372
|
--block-on-warning ID A property id whose warning blocks the verdict, as a
|
|
3238
3373
|
violation would (repeatable; all names every property;
|
|
3239
3374
|
verify only). An id the verdict lacks exits 2.
|
|
@@ -3297,6 +3432,7 @@ var objectJson = (raw, label2) => objectValue(JSON.parse(raw), label2);
|
|
|
3297
3432
|
var FLAGS = {
|
|
3298
3433
|
source: { type: "string", multiple: true },
|
|
3299
3434
|
args: { type: "string" },
|
|
3435
|
+
"arg-file": { type: "string", multiple: true },
|
|
3300
3436
|
project: { type: "string" },
|
|
3301
3437
|
instances: { type: "string" },
|
|
3302
3438
|
"block-on-warning": { type: "string", multiple: true },
|
|
@@ -3317,7 +3453,7 @@ function parse(argv2) {
|
|
|
3317
3453
|
if (command !== "tools" && command !== "call" && command !== "verify" && command !== "lean") {
|
|
3318
3454
|
throw new Error("Expected tools, call TOOL, verify FILE, or lean FILE; use --help for usage.");
|
|
3319
3455
|
}
|
|
3320
|
-
const opts = { command, sources: [], expects: [], blockOnWarnings: values["block-on-warning"] ?? [] };
|
|
3456
|
+
const opts = { command, sources: [], argFiles: [], expects: [], blockOnWarnings: values["block-on-warning"] ?? [] };
|
|
3321
3457
|
if (command === "call") {
|
|
3322
3458
|
opts.tool = rest.shift();
|
|
3323
3459
|
if (!opts.tool) throw new Error("call requires a tool name");
|
|
@@ -3325,6 +3461,14 @@ function parse(argv2) {
|
|
|
3325
3461
|
if ((command === "tools" || command === "call") && rest.length) throw new Error(`Unexpected argument: ${rest[0]}`);
|
|
3326
3462
|
opts.sources = [...rest, ...values.source ?? []];
|
|
3327
3463
|
opts.args = values.args;
|
|
3464
|
+
for (const value of values["arg-file"] ?? []) {
|
|
3465
|
+
const equal = value.indexOf("=");
|
|
3466
|
+
if (equal <= 0 || equal === value.length - 1) throw new Error("--arg-file requires NAME=FILE");
|
|
3467
|
+
const name = value.slice(0, equal);
|
|
3468
|
+
if (opts.argFiles.some((a) => a.name === name)) throw new Error(`--arg-file names ${name} twice`);
|
|
3469
|
+
opts.argFiles.push({ name, file: value.slice(equal + 1) });
|
|
3470
|
+
}
|
|
3471
|
+
if (opts.argFiles.length && command !== "call") throw new Error("--arg-file is for call");
|
|
3328
3472
|
opts.project = values.project;
|
|
3329
3473
|
if (opts.project !== void 0 && command !== "call") throw new Error("--project is for call");
|
|
3330
3474
|
if (opts.project !== void 0 && opts.sources.length) throw new Error("Use either --source files or --project, not both");
|
|
@@ -3430,6 +3574,15 @@ function sourceNames(paths) {
|
|
|
3430
3574
|
}
|
|
3431
3575
|
async function argumentsFor(opts, io) {
|
|
3432
3576
|
const args = opts.args ? objectJson(opts.args === "-" ? await io.readStdin() : readInput(opts.args), "Arguments") : {};
|
|
3577
|
+
for (const { name, file } of opts.argFiles) {
|
|
3578
|
+
if (Object.hasOwn(args, name)) throw new Error(`Use either --arg-file ${name}=... or args.${name}, not both`);
|
|
3579
|
+
try {
|
|
3580
|
+
args[name] = JSON.parse(readInput(file));
|
|
3581
|
+
} catch (error) {
|
|
3582
|
+
if (error instanceof SyntaxError) throw new Error(`--arg-file ${name}: ${file} is not JSON (${error.message})`);
|
|
3583
|
+
throw error;
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3433
3586
|
if (opts.sources.length) {
|
|
3434
3587
|
if (Object.hasOwn(args, "sources")) throw new Error("Use either --source files or args.sources, not both");
|
|
3435
3588
|
const names = sourceNames(opts.sources);
|
|
@@ -3457,7 +3610,7 @@ function clearOutput(opts) {
|
|
|
3457
3610
|
if (!opts.output) return;
|
|
3458
3611
|
const output = path9.resolve(opts.output);
|
|
3459
3612
|
const outputStat = fs9.existsSync(output) ? fs9.statSync(output) : void 0;
|
|
3460
|
-
for (const input of [...opts.sources, opts.args, opts.bundle].filter((p) => !!p && p !== "-")) {
|
|
3613
|
+
for (const input of [...opts.sources, ...opts.argFiles.map((a) => a.file), opts.args, opts.bundle].filter((p) => !!p && p !== "-")) {
|
|
3461
3614
|
const inputStat = fs9.existsSync(input) ? fs9.statSync(input) : void 0;
|
|
3462
3615
|
if (path9.resolve(input) === output || outputStat && inputStat && inputStat.dev === outputStat.dev && inputStat.ino === outputStat.ino) {
|
|
3463
3616
|
throw new Error("--output must not overwrite an input file");
|
|
@@ -3601,9 +3754,10 @@ async function startPageServer(root, assetsDir, port = null, now = () => /* @__P
|
|
|
3601
3754
|
};
|
|
3602
3755
|
for (const file of watched) fs10.watchFile(file, { interval: 250 }, changed);
|
|
3603
3756
|
const hosts = /* @__PURE__ */ new Set();
|
|
3757
|
+
let self = "", closing = false;
|
|
3604
3758
|
const takes = /* @__PURE__ */ new Map([
|
|
3605
3759
|
["/answer", (args) => recordAnswer(root, args, now())],
|
|
3606
|
-
["/viewing", (args) => recordViewing(root, args, now())]
|
|
3760
|
+
["/viewing", (args) => recordViewing(root, self, args, now())]
|
|
3607
3761
|
]);
|
|
3608
3762
|
const server = http.createServer((req, res) => {
|
|
3609
3763
|
const send = (status, type, body) => {
|
|
@@ -3624,7 +3778,11 @@ async function startPageServer(root, assetsDir, port = null, now = () => /* @__P
|
|
|
3624
3778
|
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
|
|
3625
3779
|
res.write(": connected\n\n");
|
|
3626
3780
|
clients.add(res);
|
|
3627
|
-
|
|
3781
|
+
recordOpened(root, self);
|
|
3782
|
+
req.on("close", () => {
|
|
3783
|
+
clients.delete(res);
|
|
3784
|
+
if (!clients.size && !closing) recordClosed(root, self, now());
|
|
3785
|
+
});
|
|
3628
3786
|
return;
|
|
3629
3787
|
}
|
|
3630
3788
|
const asset = PAGE_ASSETS.find((a) => url.pathname === `/${a}`);
|
|
@@ -3667,9 +3825,11 @@ async function startPageServer(root, assetsDir, port = null, now = () => /* @__P
|
|
|
3667
3825
|
}
|
|
3668
3826
|
const bound = server.address().port;
|
|
3669
3827
|
hosts.add(`127.0.0.1:${bound}`).add(`localhost:${bound}`);
|
|
3828
|
+
self = `http://127.0.0.1:${bound}/`;
|
|
3670
3829
|
return {
|
|
3671
|
-
url:
|
|
3830
|
+
url: self,
|
|
3672
3831
|
async close() {
|
|
3832
|
+
closing = true;
|
|
3673
3833
|
for (const file of watched) fs10.unwatchFile(file, changed);
|
|
3674
3834
|
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
3675
3835
|
server.closeAllConnections();
|