visualproof 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -33
- package/dist/assets/tips/phone-fingers.light.mp4 +0 -0
- package/dist/assets/tips/phone-fingers.light.webp +0 -0
- package/dist/assets/tips/tour-agent.webp +0 -0
- package/dist/assets/tips/tour-arrange.webp +0 -0
- package/dist/assets/tips/tour-artwork.webp +0 -0
- package/dist/assets/tips/tour-checks.webp +0 -0
- package/dist/assets/tips/tour-export.webp +0 -0
- package/dist/assets/tips/tour-keys.webp +0 -0
- package/dist/assets/tips/tour-library.webp +0 -0
- package/dist/assets/tips/tour-project.webp +0 -0
- package/dist/assets/tips/tour-review.webp +0 -0
- package/dist/assets/tips/tour-share.webp +0 -0
- package/dist/assets/tips/tour-source.webp +0 -0
- package/dist/assets/tips/tour-target.webp +0 -0
- package/dist/assets/tips/tour-view.webp +0 -0
- package/dist/assets/tips/tour.json +289 -0
- package/dist/assets/tips/tour.webp +0 -0
- package/dist/assets/webfonts/LICENSE-Noto.txt +95 -0
- package/dist/assets/webfonts/metrics.json +1 -0
- package/dist/assets/webfonts/ttf/jost-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/jost-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans-math.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-600-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-700-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-600-italic.ttf +0 -0
- package/dist/mcp/visualproof-mcp.mjs +3 -1
- package/dist/server/agent.js +94 -91
- package/dist/server/agentdocs.js +1 -1
- package/dist/server/artcmd.js +335 -0
- package/dist/server/children.js +85 -0
- package/dist/server/doctor.js +105 -42
- package/dist/server/events.js +10 -0
- package/dist/server/export.js +11 -4
- package/dist/server/extract.js +3 -2
- package/dist/server/figures.js +3 -2
- package/dist/server/film.js +380 -0
- package/dist/server/history.js +11 -1
- package/dist/server/hover.js +8 -5
- package/dist/server/live.js +72 -9
- package/dist/server/main.js +352 -23
- package/dist/server/mcptools.js +4 -1
- package/dist/server/media.js +114 -0
- package/dist/server/pipeline.js +21 -10
- package/dist/server/poppler.js +3 -2
- package/dist/server/readonlygate.js +4 -4
- package/dist/server/review.js +4 -1
- package/dist/server/reviewapi.js +35 -1
- package/dist/server/snapshot.js +10 -1
- package/dist/server/sourcestore.js +3 -2
- package/dist/server/status.js +3 -2
- package/dist/server/supervise.js +219 -0
- package/dist/server/updates.js +951 -0
- package/dist/server/usage.js +3 -1
- package/dist/server/version.js +183 -0
- package/dist/server/view.js +0 -7
- package/dist/server/views.js +24 -0
- package/dist/shared/artfonts.js +81 -0
- package/dist/shared/artrecipe.js +316 -0
- package/dist/shared/collab.js +7 -2
- package/dist/shared/declfind.js +95 -0
- package/dist/shared/lostfound.js +1 -0
- package/dist/shared/pastgraph.js +6 -6
- package/dist/shared/review.js +62 -5
- package/dist/shared/timeline.js +5 -0
- package/dist/web/bundle/app.js +3006 -413
- package/dist/web/bundle/edges.js +1 -1
- package/dist/web/index.html +13 -1
- package/dist/web/src/agentmark.js +14 -0
- package/dist/web/src/artwork.js +1000 -0
- package/dist/web/src/bundles.js +151 -0
- package/dist/web/src/canvas.js +3716 -0
- package/dist/web/src/drag.js +61 -0
- package/dist/web/src/edgequeue.js +204 -0
- package/dist/web/src/gl/gl.js +123 -0
- package/dist/web/src/gl/renderer.js +451 -0
- package/dist/web/src/halo.js +95 -0
- package/dist/web/src/highlight.js +108 -0
- package/dist/web/src/overview.js +393 -0
- package/dist/web/src/reviewqueue.js +250 -0
- package/dist/web/src/selection.js +272 -0
- package/dist/web/src/status.js +80 -0
- package/dist/web/src/visitors.js +318 -0
- package/dist/web/src/visits.js +174 -0
- package/dist/web/style.css +196 -1
- package/docs/agent-api/README.md +1 -1
- package/docs/agent-api/export.md +41 -2
- package/docs/agent-api/history.md +5 -3
- package/docs/agent-api/mcp.md +1 -1
- package/docs/agent-api/reading.md +2 -1
- package/package.json +5 -1
package/dist/server/agent.js
CHANGED
|
@@ -29,11 +29,15 @@
|
|
|
29
29
|
import { historyEndpoints, historyWorld } from "./historyapi.js";
|
|
30
30
|
import { PastBuilds } from "./pastdoc.js";
|
|
31
31
|
import { spawnSync } from "node:child_process";
|
|
32
|
+
import { packageVersion } from "./version.js";
|
|
33
|
+
import { withoutReasons } from "../shared/timeline.js";
|
|
34
|
+
import { childEnv } from "./children.js";
|
|
32
35
|
import { readFileSync } from "node:fs";
|
|
33
36
|
import { join } from "node:path";
|
|
34
37
|
import { gunzipSync } from "node:zlib";
|
|
35
38
|
import { ACTIVITY_KINDS, ACTIVITY_WHO, cleanView, MAX_PINNED, SCHEMES, validName } from "../shared/collab.js";
|
|
36
39
|
import { addressOf, readRegion, showing, writeSnapshot } from "../shared/sharesnap.js";
|
|
40
|
+
import { encodeRecipe, readRecipe, RECIPE_PARAM, resolveRecipe, SAVE_PARAM } from "../shared/artrecipe.js";
|
|
37
41
|
import { contextText, exactContext, readContext } from "../shared/context.js";
|
|
38
42
|
import { compiledExcerpt, excerptOf } from "../shared/excerpt.js";
|
|
39
43
|
import { tokenizeLean } from "../shared/lexer.js";
|
|
@@ -56,6 +60,7 @@ import { inTargets, nonStandardAxiom, offBoard, reviewQueue, TCB_SITES, whyChain
|
|
|
56
60
|
import { reviewThen } from "../shared/pastgraph.js";
|
|
57
61
|
import { contextAt, namespacesOf } from "../shared/scope.js";
|
|
58
62
|
import { PLACES, search, SearchIndex, snippet } from "../shared/search.js";
|
|
63
|
+
import { foldedAt, guessNames } from "../shared/declfind.js";
|
|
59
64
|
import { fullWhen } from "../shared/titles.js";
|
|
60
65
|
import { ALL_SITES, Graph } from "../shared/viewgraph.js";
|
|
61
66
|
import { layoutKey, wholeLayout } from "../shared/board.js";
|
|
@@ -172,9 +177,6 @@ const PARTS = ["source", "context", "uses", "users", "describe", "print"];
|
|
|
172
177
|
const WHERE = { statement: SITE_STMT, body: SITE_BODY, proof: SITE_PROOF, grammar: SITE_GRAMMAR };
|
|
173
178
|
/** what a trusted standard axiom is trusted by */
|
|
174
179
|
const STANDARD = "standard axiom";
|
|
175
|
-
/** the last components of names Lean generates beside a declaration (extractor/Extract.lean,
|
|
176
|
-
* `generatedSuffixes`, and the like) */
|
|
177
|
-
const GENERATED_LAST = /^(?:rec|recOn|casesOn|noConfusion|noConfusionType|ctorElim|ctorElimType|below|brecOn|binductionOn|ibelow|ctorIdx|toCtorIdx|ofNat|ofNat_ctorIdx|sizeOf_spec|injEq|inj|eq_def|induct|mutual_induct|fun_cases|induct_unfolding|ext_iff|congr_simp|mk|elim|(?:eq|match|proof)_\d+|_.*)$/;
|
|
178
180
|
/** The graph on show, as the API reads it: built from the server's prepared view once per
|
|
179
181
|
* version. The page's review route reads it too (server/reviewapi.ts). */
|
|
180
182
|
export class View {
|
|
@@ -372,41 +374,9 @@ export class AgentApi {
|
|
|
372
374
|
this.view = new View(g.version, g.prep.metaGz, g.prep.edgesGz);
|
|
373
375
|
return this.view;
|
|
374
376
|
}
|
|
375
|
-
/** Up to five names like `name
|
|
376
|
-
* or one a typo or two away, in the same namespace first; then what the search finds. */
|
|
377
|
+
/** Up to five names like `name` (shared/declfind.ts, `guessNames`). */
|
|
377
378
|
guess(v, name) {
|
|
378
|
-
|
|
379
|
-
const dot = name.lastIndexOf(".");
|
|
380
|
-
const last = name.slice(dot + 1).toLowerCase();
|
|
381
|
-
const ns = dot < 0 ? "" : name.slice(0, dot);
|
|
382
|
-
const scored = new Map();
|
|
383
|
-
const offer = (n, score) => {
|
|
384
|
-
if ((scored.get(n) ?? Infinity) > score)
|
|
385
|
-
scored.set(n, score);
|
|
386
|
-
};
|
|
387
|
-
const lower = name.toLowerCase();
|
|
388
|
-
const most = last.length < 5 ? 1 : 2;
|
|
389
|
-
// the graph's declarations, and the names Lean generated from a command that they fold
|
|
390
|
-
for (const name of [...g.nodes.map((n) => n.name), ...g.foldedInto.keys()]) {
|
|
391
|
-
if (name.toLowerCase() === lower)
|
|
392
|
-
offer(name, -1);
|
|
393
|
-
const k = name.lastIndexOf(".");
|
|
394
|
-
const d = editDistance(name.slice(k + 1).toLowerCase(), last, most);
|
|
395
|
-
if (d <= most)
|
|
396
|
-
offer(name, 2 * d + (name.slice(0, Math.max(0, k)) === ns ? 0 : 1));
|
|
397
|
-
}
|
|
398
|
-
let rank = 0;
|
|
399
|
-
for (const q of [name.slice(dot + 1), name.replace(/\./g, " ")]) {
|
|
400
|
-
if (q.trim().length < 2)
|
|
401
|
-
continue;
|
|
402
|
-
const f = search(v.index, q);
|
|
403
|
-
for (const h of [...f.hits.name, ...f.hits.statement].slice(0, 10))
|
|
404
|
-
offer(g.nodes[h.i].name, 10 + rank++);
|
|
405
|
-
}
|
|
406
|
-
return [...scored]
|
|
407
|
-
.sort((a, b) => a[1] - b[1] || a[0].length - b[0].length || (a[0] < b[0] ? -1 : 1))
|
|
408
|
-
.slice(0, 5)
|
|
409
|
-
.map(([n]) => n);
|
|
379
|
+
return guessNames(v.g, v.index, name);
|
|
410
380
|
}
|
|
411
381
|
/** A declaration by its full name, or the answer that there is none (with what was meant). A
|
|
412
382
|
* name the graph folds into a declaration is that declaration (`folded` says so). */
|
|
@@ -419,35 +389,10 @@ export class AgentApi {
|
|
|
419
389
|
const guesses = this.guess(v, name.trim());
|
|
420
390
|
return fail(404, `There is no declaration ${name} in the graph.`, guesses.length ? `Did you mean ${guesses[0]}? Names are full, as the search gives them.` : "Search for it: GET /api/agent/search?q=<words>.", { didYouMean: guesses });
|
|
421
391
|
}
|
|
422
|
-
/** The declaration a name the graph has no node for is folded into
|
|
423
|
-
*
|
|
424
|
-
* declaration or of one so generated, which is a name one component below it. A name one
|
|
425
|
-
* component below a declaration that is a typo or two away from a declaration beside it is
|
|
426
|
-
* taken for that typo, not for something folded, unless its last component is one Lean
|
|
427
|
-
* generates. */
|
|
392
|
+
/** The declaration a name the graph has no node for is folded into (shared/declfind.ts,
|
|
393
|
+
* `foldedAt`). */
|
|
428
394
|
foldedAt(v, name) {
|
|
429
|
-
|
|
430
|
-
const direct = g.foldedInto.get(name);
|
|
431
|
-
if (direct !== undefined)
|
|
432
|
-
return direct;
|
|
433
|
-
const dot = name.lastIndexOf(".");
|
|
434
|
-
if (dot <= 0)
|
|
435
|
-
return undefined;
|
|
436
|
-
const head = name.slice(0, dot), last = name.slice(dot + 1);
|
|
437
|
-
const at = g.byName.get(head) ?? g.foldedInto.get(head);
|
|
438
|
-
if (at === undefined || !last)
|
|
439
|
-
return undefined;
|
|
440
|
-
if (!GENERATED_LAST.test(last)) {
|
|
441
|
-
const most = last.length < 5 ? 1 : 2;
|
|
442
|
-
const near = (n) => n.length > head.length + 1 && n.startsWith(head + ".") && !n.includes(".", head.length + 1) && editDistance(n.slice(head.length + 1).toLowerCase(), last.toLowerCase(), most) <= most;
|
|
443
|
-
for (const n of g.nodes)
|
|
444
|
-
if (near(n.name))
|
|
445
|
-
return undefined;
|
|
446
|
-
for (const n of g.foldedInto.keys())
|
|
447
|
-
if (near(n))
|
|
448
|
-
return undefined;
|
|
449
|
-
}
|
|
450
|
-
return at;
|
|
395
|
+
return foldedAt(v.g, name);
|
|
451
396
|
}
|
|
452
397
|
/** What an answer about `asked` says when that name is folded into declaration `i`. */
|
|
453
398
|
folded(v, asked, i) {
|
|
@@ -514,8 +459,10 @@ export class AgentApi {
|
|
|
514
459
|
return {
|
|
515
460
|
by: o.by,
|
|
516
461
|
agent: !!o.agent,
|
|
517
|
-
// someone invited by a personal link: the name is theirs (CollabSpec decision 9)
|
|
518
|
-
|
|
462
|
+
// someone invited by a personal link: the name is theirs (CollabSpec decision 9), unless
|
|
463
|
+
// someone else recorded it for them at the same screen, who is named (ReviewSpec decision 8)
|
|
464
|
+
...(o.person && !o.recordedBy ? { verified: true } : {}),
|
|
465
|
+
...(o.recordedBy ? { recordedBy: o.recordedBy.by } : {}),
|
|
519
466
|
verdict: o.verdict,
|
|
520
467
|
...(o.comment ? { comment: o.comment } : {}),
|
|
521
468
|
at: o.at,
|
|
@@ -873,7 +820,7 @@ export class AgentApi {
|
|
|
873
820
|
const advise = !this.refuse(me, "advise");
|
|
874
821
|
const byKey = new Map(this.endpoints.map((e) => [`${e.method} ${e.path}`, e]));
|
|
875
822
|
return ok({
|
|
876
|
-
about:
|
|
823
|
+
about: `VisualProof ${packageVersion()} shows a Lean project's declarations, the TCB of its theorems (what a theorem's statement rests on) and the review people make of it. This is its API for AI agents.`,
|
|
877
824
|
you: {
|
|
878
825
|
name: me.name,
|
|
879
826
|
kind: me.kind,
|
|
@@ -1216,7 +1163,9 @@ export class AgentApi {
|
|
|
1216
1163
|
if (typeof t !== "string")
|
|
1217
1164
|
return t;
|
|
1218
1165
|
try {
|
|
1219
|
-
|
|
1166
|
+
// the file carries the version of the server it came from (UpdateSpec decision 11)
|
|
1167
|
+
const body = readFileSync(MCP_FILE, "utf8").replace(/^const VERSION = "[^"\n]*";$/m, `const VERSION = ${JSON.stringify(packageVersion())};`);
|
|
1168
|
+
return { status: 200, body: null, text: { type: "text/javascript; charset=utf-8", body } };
|
|
1220
1169
|
}
|
|
1221
1170
|
catch {
|
|
1222
1171
|
return fail(404, "The MCP server is not beside this server.", "Use the HTTP API (GET /api/agent).");
|
|
@@ -2046,7 +1995,7 @@ export class AgentApi {
|
|
|
2046
1995
|
// what the review's logs record, as the page reads it
|
|
2047
1996
|
for (const [name, ops] of Object.entries(d.opinions))
|
|
2048
1997
|
for (const o of ops)
|
|
2049
|
-
out.push({ at: o.at, kind: "opinion", by: o.by, agent: !!o.agent, name, verdict: o.verdict, ...(o.comment ? { comment: o.comment } : {}) });
|
|
1998
|
+
out.push({ at: o.at, kind: "opinion", by: o.by, ...(o.recordedBy ? { recordedBy: o.recordedBy.by } : {}), agent: !!o.agent, name, verdict: o.verdict, ...(o.comment ? { comment: o.comment } : {}) });
|
|
2050
1999
|
// what an agent did at level full is marked as an agent's, as its advice is
|
|
2051
2000
|
for (const [name, t] of Object.entries(d.targets))
|
|
2052
2001
|
out.push({ at: t.at, kind: "target", by: t.by, agent: !!t.agent, name });
|
|
@@ -2077,7 +2026,7 @@ export class AgentApi {
|
|
|
2077
2026
|
out.push({ ...rest, ...who, moved: ops.flatMap((o) => (o.op === "clear" ? [] : [o.key])).slice(0, 100), undoneBy: undone.get(e.id) ?? null });
|
|
2078
2027
|
}
|
|
2079
2028
|
else
|
|
2080
|
-
out.push({ ...e, ...who });
|
|
2029
|
+
out.push({ ...withoutReasons(e), ...who });
|
|
2081
2030
|
}
|
|
2082
2031
|
const about = (a.query.get("name") ?? "").trim();
|
|
2083
2032
|
// a note is about the declarations it names; a link, about the declaration it is on, or was on
|
|
@@ -2228,6 +2177,10 @@ export class AgentApi {
|
|
|
2228
2177
|
unit = v.g.nodes[i].name;
|
|
2229
2178
|
}
|
|
2230
2179
|
const verdict = b.verdict;
|
|
2180
|
+
// an agent's advice is its own: people record for one another at one screen, agents never
|
|
2181
|
+
// (ReviewSpec decision 8)
|
|
2182
|
+
if (b.for !== undefined)
|
|
2183
|
+
return fail(403, "An agent records no opinion for anyone else.", "Leave out for: your advice is your own.");
|
|
2231
2184
|
// "dispute" was the old name of a comment (renamed 2026-10-09): refused, with the new name
|
|
2232
2185
|
if (verdict === "dispute")
|
|
2233
2186
|
return fail(400, "verdict is approve or comment: \"dispute\" is now \"comment\".", "Use comment: send {name, verdict: \"comment\", comment}.");
|
|
@@ -2528,6 +2481,66 @@ export class AgentApi {
|
|
|
2528
2481
|
}
|
|
2529
2482
|
return { place: cleanView(next), temporary };
|
|
2530
2483
|
}
|
|
2484
|
+
/** A link to a picture of the board (ExportSpec decision 20, drafted 2026-10-15): a recipe
|
|
2485
|
+
* (shared/artrecipe.ts) checked, and resolved against the board of the view it names — the
|
|
2486
|
+
* Project's layout in the view's clustering — so that a name, a target or a cluster the board has
|
|
2487
|
+
* not is refused here rather than drawn as a picture unlike the one meant; the selection it gives
|
|
2488
|
+
* goes in the address, the recipe after it. The page that opens the link resolves it again
|
|
2489
|
+
* against the board it opens on, opens the artwork mode, and saves the files asked for. Nothing
|
|
2490
|
+
* changes here, and the link holds no key. */
|
|
2491
|
+
async artwork(a) {
|
|
2492
|
+
const no = needAgent(a.caller);
|
|
2493
|
+
if (no)
|
|
2494
|
+
return no;
|
|
2495
|
+
const b = body(a);
|
|
2496
|
+
const read = readRecipe(b.recipe);
|
|
2497
|
+
if ("error" in read)
|
|
2498
|
+
return fail(400, read.error, read.hint);
|
|
2499
|
+
const r = read.recipe;
|
|
2500
|
+
const save = b.save === undefined || b.save === null ? [] : typeof b.save === "string" ? b.save.split(",").map((k) => k.trim()) : null;
|
|
2501
|
+
if (!save || save.some((k) => k !== "png" && k !== "svg"))
|
|
2502
|
+
return fail(400, "save is png, svg or png,svg.", "The files the page saves once the board is ready; left out, none.");
|
|
2503
|
+
const v = this.graph();
|
|
2504
|
+
if (!(v instanceof View))
|
|
2505
|
+
return v;
|
|
2506
|
+
let sel = null;
|
|
2507
|
+
if (r.selection) {
|
|
2508
|
+
const i = this.find(v, r.selection.name, "selection.name");
|
|
2509
|
+
if (typeof i !== "number")
|
|
2510
|
+
return i;
|
|
2511
|
+
sel = v.g.nodes[i].name;
|
|
2512
|
+
}
|
|
2513
|
+
const rec = r.view && r.view !== PROJECT ? this.world.views?.book.get(r.view) : undefined;
|
|
2514
|
+
if (r.view && r.view !== PROJECT && !rec)
|
|
2515
|
+
return fail(404, `There is no view ${r.view}.`, "GET /api/agent/views lists them; left out, the Project.");
|
|
2516
|
+
const L = v.meta.layouts[rec?.clustering ?? "namespace"];
|
|
2517
|
+
const res = resolveRecipe(r, {
|
|
2518
|
+
clusters: L.clusters,
|
|
2519
|
+
clusterOf: (name) => {
|
|
2520
|
+
const i = v.g.byName.get(name);
|
|
2521
|
+
return i === undefined ? undefined : L.cluster[i];
|
|
2522
|
+
},
|
|
2523
|
+
targets: Object.keys(this.world.review.data.targets),
|
|
2524
|
+
projectCount: v.meta.projectCount,
|
|
2525
|
+
});
|
|
2526
|
+
if (res.problems.length)
|
|
2527
|
+
return fail(404, `The recipe names what the board has not: ${res.problems.join("; ")}.`, "Clusters by key (GET /api/agent/views/view lists a view's), declarations and targets by full name (GET /api/agent/search; the targets: GET /api/agent/targets). A target of the review the graph no longer has is left out unsaid where the recipe does not name it.", { problems: res.problems });
|
|
2528
|
+
const address = addressOf({ view: rec ? rec.id : null, scope: null, mode: r.selection?.mode ?? "tcb", selection: null, sel });
|
|
2529
|
+
const link = `${a.base ?? ""}/#${address}&${RECIPE_PARAM}=${encodeRecipe(r)}${save.length ? `&${SAVE_PARAM}=${[...new Set(save)].join(",")}` : ""}`;
|
|
2530
|
+
const look = res.look;
|
|
2531
|
+
return ok({
|
|
2532
|
+
ok: true,
|
|
2533
|
+
link,
|
|
2534
|
+
length: link.length,
|
|
2535
|
+
size: [look.w, look.h],
|
|
2536
|
+
names: look.labels === "chosen" ? res.named : look.labels,
|
|
2537
|
+
targets: look.targets === "chosen" ? look.shownTargets : look.targets,
|
|
2538
|
+
selection: sel ? { name: sel, mode: r.selection.mode ?? "tcb" } : null,
|
|
2539
|
+
// names and targets beyond what the artwork carries, left out: said, not refused
|
|
2540
|
+
left: res.left,
|
|
2541
|
+
note: "It holds no key: it opens for whoever is let in already, or for a script that enters with the key (scripts/artwork-batch.mjs). The page that opens it resolves the recipe again against its board, opens the artwork mode fitted to the whole board, and saves the files asked for on that machine; nothing comes back here.",
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2531
2544
|
/** A link to what the agent looks at, or to a place it gives over its own, as a person's Share makes
|
|
2532
2545
|
* one with Exactly what I see (CollabSpec decision 7, the owner's decision of 2026-10-12): the
|
|
2533
2546
|
* address, and after it a snapshot of everything followed, which the page that opens it applies
|
|
@@ -2887,7 +2900,7 @@ export class AgentApi {
|
|
|
2887
2900
|
{
|
|
2888
2901
|
method: "GET", path: "/api/agent/activity", brief: "the timeline", about: "The timeline, oldest first: opinions and advice, targets and key marks, files of the library added, retitled, archived or restored and links made, edited or removed (all from the review's logs), builds, TCB changes, targets taken off, prefixes trusted or no longer, views saved, renamed, rescoped, deleted or laid out afresh, notes added to a view or taken out of it, notes, layouts and their undoing.",
|
|
2889
2902
|
params: { since: "an ISO time (e.g. the `now` of your last answer) or an event id: what came after", name: "optional: only what is about this declaration (opinions, advice, notes that name it, key marks, links on it; a target's own events)", limit: "default 100, at most 1000 (the newest)" },
|
|
2890
|
-
returns: "{now, events: [{at, kind, by?, agent?, …}] oldest first, more}; kinds: opinion, target, key (a key declaration as marked now: target, name), build, tcb, untarget, trust, untrust, view (a view saved, renamed, rescoped, deleted or laid out afresh: what, view, name, was?, scope; or a note of the project added to it or taken out of it: what items, note, text, shown), note (a note on a view written, edited or deleted: what, view, title, note, text), file (a file of the library: what is added, edited — its title, wasTitle the one before —, archived or restored; sha, title), link (what is made, edited or removed; id, subject, label, file, and an edit's changed (subject, label, place), wasSubject?, wasLabel?, auto? where VisualProof carried it with a renamed cluster), layout (with moved and undoneBy), undo; relayout only in a timeline kept from before views",
|
|
2903
|
+
returns: "{now, events: [{at, kind, by?, agent?, …}] oldest first, more}; kinds: opinion, target, key (a key declaration as marked now: target, name), build, tcb, untarget, trust, untrust, view (a view saved, renamed, rescoped, deleted or laid out afresh: what, view, name, was?, scope; or a note of the project added to it or taken out of it: what items, note, text, shown), note (a note on a view written, edited or deleted: what, view, title, note, text), file (a file of the library: what is added, edited — its title, wasTitle the one before —, archived or restored; sha, title), link (what is made, edited or removed; id, subject, label, file, and an edit's changed (subject, label, place), wasSubject?, wasLabel?, auto? where VisualProof carried it with a renamed cluster), layout (with moved and undoneBy), undo, tool (the VisualProof version serving the site changed: from, to, setAside?); relayout only in a timeline kept from before views",
|
|
2891
2904
|
run: (a) => this.activity(a),
|
|
2892
2905
|
},
|
|
2893
2906
|
{
|
|
@@ -2920,6 +2933,12 @@ export class AgentApi {
|
|
|
2920
2933
|
returns: "{ok, link, exact, carries: what the link opens on, length, note, temporary?}",
|
|
2921
2934
|
run: (a) => this.viewLink(a),
|
|
2922
2935
|
},
|
|
2936
|
+
{
|
|
2937
|
+
method: "POST", path: "/api/agent/artwork", about: "A link that opens the board as a picture, in the artwork mode, as the recipe says (ExportSpec decision 20): its size, its look, names by a rule, the targets marked, the selection — in words that hold on any project, so that one recipe makes the same kind of picture of every codebase; the page saves the PNG or SVG where asked, once the board is ready. It changes nothing, and holds no key. A name, a target or a cluster the board has not is refused (404, problems).",
|
|
2938
|
+
params: { recipe: "the picture: {size, orientation, style, theme, transparent, colouring, border, title, subtitle, titleSize, footSize, source, names, targets, keys, notes, edges, selection, lit, view}, each optional (GET /api/agent/docs/export, \"Artwork\")", save: "png, svg or png,svg: the files the page saves once the board is ready; left out, none" },
|
|
2939
|
+
returns: "{ok, link, length, size: [w, h], names: {clusters, decls: written wherever they fall; ruled: by the rule, each only where it covers no other name} | how the board writes them, targets, selection, left: names and targets left out for room, note}",
|
|
2940
|
+
run: (a) => this.artwork(a),
|
|
2941
|
+
},
|
|
2923
2942
|
{
|
|
2924
2943
|
method: "POST", path: "/api/agent/pins", needs: "full", about: "Drop or let go of pins on the Project view, as people drag (for another view: POST /api/agent/views/arrange): one batch, recorded in the timeline with what it replaced, undoable at once.",
|
|
2925
2944
|
params: { mode: "namespace (default) or module", pins: `[{name or cluster, at: [x, y] in the parent's coordinates (GET /api/agent/place), or null to let go}], at most ${MAX_PINS}` },
|
|
@@ -3174,6 +3193,8 @@ export function doingOf(a) {
|
|
|
3174
3193
|
return of("going to", clip(b.sel)) ?? of("going to the view of", clip(b.review)) ?? "moving its view";
|
|
3175
3194
|
case "/api/agent/view/link":
|
|
3176
3195
|
return "making a link to what it looks at";
|
|
3196
|
+
case "/api/agent/artwork":
|
|
3197
|
+
return "making a link to a picture of the board";
|
|
3177
3198
|
case "/api/agent/ask-follow":
|
|
3178
3199
|
return clip(b.who) ? `asking ${clip(b.who)} to follow it` : "asking everyone to follow it";
|
|
3179
3200
|
case "/api/agent/opinion":
|
|
@@ -3226,24 +3247,6 @@ const str = (x) => (typeof x === "string" ? x : null);
|
|
|
3226
3247
|
export function whoOf(c) {
|
|
3227
3248
|
return { name: c.name ?? "", ...(c.token ? { token: c.token.id } : {}) };
|
|
3228
3249
|
}
|
|
3229
|
-
/** The edit distance between two short strings, or `most + 1` once it is more than `most`. */
|
|
3230
|
-
function editDistance(a, b, most) {
|
|
3231
|
-
if (Math.abs(a.length - b.length) > most)
|
|
3232
|
-
return most + 1;
|
|
3233
|
-
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
3234
|
-
for (let i = 1; i <= a.length; i++) {
|
|
3235
|
-
const row = [i];
|
|
3236
|
-
let best = i;
|
|
3237
|
-
for (let j = 1; j <= b.length; j++) {
|
|
3238
|
-
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
3239
|
-
best = Math.min(best, row[j]);
|
|
3240
|
-
}
|
|
3241
|
-
if (best > most)
|
|
3242
|
-
return most + 1;
|
|
3243
|
-
prev = row;
|
|
3244
|
-
}
|
|
3245
|
-
return Math.min(prev[b.length], most + 1);
|
|
3246
|
-
}
|
|
3247
3250
|
/** An integer parameter within bounds, `dflt` when absent or not a number. */
|
|
3248
3251
|
function clampInt(s, dflt, lo, hi) {
|
|
3249
3252
|
const n = s === null || s.trim() === "" ? NaN : Number(s);
|
|
@@ -3258,7 +3261,7 @@ export function commitOf(root) {
|
|
|
3258
3261
|
if (was && now - was.at < 10_000)
|
|
3259
3262
|
return was.commit;
|
|
3260
3263
|
const git = (...args) => {
|
|
3261
|
-
const r = spawnSync("git", ["-C", root, ...args], { encoding: "utf8", timeout: 5000, env: {
|
|
3264
|
+
const r = spawnSync("git", ["-C", root, ...args], { encoding: "utf8", timeout: 5000, env: childEnv({ GIT_OPTIONAL_LOCKS: "0" }), stdio: ["ignore", "pipe", "ignore"] });
|
|
3262
3265
|
return r.status === 0 ? r.stdout.trim() : null;
|
|
3263
3266
|
};
|
|
3264
3267
|
const hash = git("rev-parse", "HEAD");
|
package/dist/server/agentdocs.js
CHANGED
|
@@ -16,7 +16,7 @@ export const DOC_TOPICS = [
|
|
|
16
16
|
{ topic: "following", title: "Following", about: "where people are, your view, links to it, what people see" },
|
|
17
17
|
{ topic: "views", title: "Views and the layout", about: "queries, views, places, pins" },
|
|
18
18
|
{ topic: "library", title: "The library and links", about: "papers and images, their pages, links to them" },
|
|
19
|
-
{ topic: "export", title: "Exported documents", about: "a view's document, typeset" },
|
|
19
|
+
{ topic: "export", title: "Exported documents", about: "a view's document, typeset; a picture" },
|
|
20
20
|
{ topic: "history", title: "History and activity", about: "the timeline, builds, renames, the lost-and-found" },
|
|
21
21
|
{ topic: "talking", title: "Talking with people", about: "your inbox, replies, your state" },
|
|
22
22
|
{ topic: "mcp", title: "VisualProof's MCP server", about: "installing it, its tools and their actions" },
|