visualproof 0.1.0 → 0.3.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 +40 -26
- package/dist/assets/webfonts/LICENSE-Jost.txt +93 -0
- package/dist/assets/webfonts/LICENSE-Noto.txt +95 -0
- package/dist/assets/webfonts/LICENSE-SourceSans3.txt +93 -0
- package/dist/assets/webfonts/LICENSE-SourceSerif4.txt +93 -0
- package/dist/assets/webfonts/jost-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/jost-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/metrics.json +1 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-normal.woff2 +0 -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/server/agent.js +125 -92
- package/dist/server/agentdocs.js +1 -1
- package/dist/server/artcmd.js +335 -0
- package/dist/server/history.js +39 -0
- package/dist/server/historyapi.js +2 -2
- package/dist/server/hover.js +3 -3
- package/dist/server/layoutthread.js +1 -1
- package/dist/server/live.js +6 -0
- package/dist/server/main.js +151 -11
- package/dist/server/mcptools.js +6 -4
- package/dist/server/pastdoc.js +1 -1
- package/dist/server/pipeline.js +18 -9
- package/dist/server/readonlygate.js +2 -2
- package/dist/server/review.js +4 -1
- package/dist/server/reviewapi.js +35 -1
- package/dist/server/snapshot.js +13 -3
- package/dist/server/usage.js +2 -2
- package/dist/server/views.js +25 -1
- package/dist/shared/artfonts.js +81 -0
- package/dist/shared/artrecipe.js +316 -0
- package/dist/shared/collab.js +99 -1
- package/dist/shared/declfind.js +95 -0
- package/dist/shared/doc/order.js +17 -0
- package/dist/shared/doc/outline.js +45 -0
- package/dist/shared/keys.js +1 -1
- package/dist/shared/lostfound.js +1 -0
- package/dist/shared/moves.js +2 -1
- package/dist/shared/pastgraph.js +9 -1
- package/dist/shared/query.js +21 -13
- package/dist/shared/review.js +62 -5
- package/dist/shared/sharesnap.js +2 -1
- package/dist/shared/titles.js +2 -0
- package/dist/shared/viewgraph.js +18 -0
- package/dist/web/bundle/app.js +7709 -3222
- package/dist/web/bundle/edges.js +4 -6
- package/dist/web/index.html +24 -6
- 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 +3694 -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/scene.js +17 -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 +255 -12
- package/docs/agent-api/README.md +1 -1
- package/docs/agent-api/export.md +41 -2
- package/docs/agent-api/following.md +2 -2
- package/docs/agent-api/history.md +2 -1
- package/docs/agent-api/mcp.md +1 -1
- package/docs/agent-api/reading.md +2 -1
- package/docs/agent-api/views.md +3 -2
- package/package.json +3 -1
- package/test/toolchains.json +2 -2
package/dist/server/agent.js
CHANGED
|
@@ -34,6 +34,7 @@ import { join } from "node:path";
|
|
|
34
34
|
import { gunzipSync } from "node:zlib";
|
|
35
35
|
import { ACTIVITY_KINDS, ACTIVITY_WHO, cleanView, MAX_PINNED, SCHEMES, validName } from "../shared/collab.js";
|
|
36
36
|
import { addressOf, readRegion, showing, writeSnapshot } from "../shared/sharesnap.js";
|
|
37
|
+
import { encodeRecipe, readRecipe, RECIPE_PARAM, resolveRecipe, SAVE_PARAM } from "../shared/artrecipe.js";
|
|
37
38
|
import { contextText, exactContext, readContext } from "../shared/context.js";
|
|
38
39
|
import { compiledExcerpt, excerptOf } from "../shared/excerpt.js";
|
|
39
40
|
import { tokenizeLean } from "../shared/lexer.js";
|
|
@@ -47,6 +48,8 @@ import { authorLine, authorsOf, reviewTraces, viewTraces } from "../shared/doc/a
|
|
|
47
48
|
import { itemsShownOn } from "../shared/noteplace.js";
|
|
48
49
|
import { buildDoc, DocInputError, docNotes } from "../shared/doc/build.js";
|
|
49
50
|
import { boardSpots } from "./boardspots.js";
|
|
51
|
+
import { outlineOf } from "../shared/doc/outline.js";
|
|
52
|
+
import { planDoc } from "../shared/doc/sections.js";
|
|
50
53
|
import { docFiles, figureFiles, figureNotes } from "./figures.js";
|
|
51
54
|
import { renderLatex } from "../shared/doc/latex.js";
|
|
52
55
|
import { readSettings, SETTINGS_HELP } from "../shared/doc/settings.js";
|
|
@@ -54,6 +57,7 @@ import { inTargets, nonStandardAxiom, offBoard, reviewQueue, TCB_SITES, whyChain
|
|
|
54
57
|
import { reviewThen } from "../shared/pastgraph.js";
|
|
55
58
|
import { contextAt, namespacesOf } from "../shared/scope.js";
|
|
56
59
|
import { PLACES, search, SearchIndex, snippet } from "../shared/search.js";
|
|
60
|
+
import { foldedAt, guessNames } from "../shared/declfind.js";
|
|
57
61
|
import { fullWhen } from "../shared/titles.js";
|
|
58
62
|
import { ALL_SITES, Graph } from "../shared/viewgraph.js";
|
|
59
63
|
import { layoutKey, wholeLayout } from "../shared/board.js";
|
|
@@ -170,9 +174,6 @@ const PARTS = ["source", "context", "uses", "users", "describe", "print"];
|
|
|
170
174
|
const WHERE = { statement: SITE_STMT, body: SITE_BODY, proof: SITE_PROOF, grammar: SITE_GRAMMAR };
|
|
171
175
|
/** what a trusted standard axiom is trusted by */
|
|
172
176
|
const STANDARD = "standard axiom";
|
|
173
|
-
/** the last components of names Lean generates beside a declaration (extractor/Extract.lean,
|
|
174
|
-
* `generatedSuffixes`, and the like) */
|
|
175
|
-
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+|_.*)$/;
|
|
176
177
|
/** The graph on show, as the API reads it: built from the server's prepared view once per
|
|
177
178
|
* version. The page's review route reads it too (server/reviewapi.ts). */
|
|
178
179
|
export class View {
|
|
@@ -370,41 +371,9 @@ export class AgentApi {
|
|
|
370
371
|
this.view = new View(g.version, g.prep.metaGz, g.prep.edgesGz);
|
|
371
372
|
return this.view;
|
|
372
373
|
}
|
|
373
|
-
/** Up to five names like `name
|
|
374
|
-
* or one a typo or two away, in the same namespace first; then what the search finds. */
|
|
374
|
+
/** Up to five names like `name` (shared/declfind.ts, `guessNames`). */
|
|
375
375
|
guess(v, name) {
|
|
376
|
-
|
|
377
|
-
const dot = name.lastIndexOf(".");
|
|
378
|
-
const last = name.slice(dot + 1).toLowerCase();
|
|
379
|
-
const ns = dot < 0 ? "" : name.slice(0, dot);
|
|
380
|
-
const scored = new Map();
|
|
381
|
-
const offer = (n, score) => {
|
|
382
|
-
if ((scored.get(n) ?? Infinity) > score)
|
|
383
|
-
scored.set(n, score);
|
|
384
|
-
};
|
|
385
|
-
const lower = name.toLowerCase();
|
|
386
|
-
const most = last.length < 5 ? 1 : 2;
|
|
387
|
-
// the graph's declarations, and the names Lean generated from a command that they fold
|
|
388
|
-
for (const name of [...g.nodes.map((n) => n.name), ...g.foldedInto.keys()]) {
|
|
389
|
-
if (name.toLowerCase() === lower)
|
|
390
|
-
offer(name, -1);
|
|
391
|
-
const k = name.lastIndexOf(".");
|
|
392
|
-
const d = editDistance(name.slice(k + 1).toLowerCase(), last, most);
|
|
393
|
-
if (d <= most)
|
|
394
|
-
offer(name, 2 * d + (name.slice(0, Math.max(0, k)) === ns ? 0 : 1));
|
|
395
|
-
}
|
|
396
|
-
let rank = 0;
|
|
397
|
-
for (const q of [name.slice(dot + 1), name.replace(/\./g, " ")]) {
|
|
398
|
-
if (q.trim().length < 2)
|
|
399
|
-
continue;
|
|
400
|
-
const f = search(v.index, q);
|
|
401
|
-
for (const h of [...f.hits.name, ...f.hits.statement].slice(0, 10))
|
|
402
|
-
offer(g.nodes[h.i].name, 10 + rank++);
|
|
403
|
-
}
|
|
404
|
-
return [...scored]
|
|
405
|
-
.sort((a, b) => a[1] - b[1] || a[0].length - b[0].length || (a[0] < b[0] ? -1 : 1))
|
|
406
|
-
.slice(0, 5)
|
|
407
|
-
.map(([n]) => n);
|
|
376
|
+
return guessNames(v.g, v.index, name);
|
|
408
377
|
}
|
|
409
378
|
/** A declaration by its full name, or the answer that there is none (with what was meant). A
|
|
410
379
|
* name the graph folds into a declaration is that declaration (`folded` says so). */
|
|
@@ -417,35 +386,10 @@ export class AgentApi {
|
|
|
417
386
|
const guesses = this.guess(v, name.trim());
|
|
418
387
|
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 });
|
|
419
388
|
}
|
|
420
|
-
/** The declaration a name the graph has no node for is folded into
|
|
421
|
-
*
|
|
422
|
-
* declaration or of one so generated, which is a name one component below it. A name one
|
|
423
|
-
* component below a declaration that is a typo or two away from a declaration beside it is
|
|
424
|
-
* taken for that typo, not for something folded, unless its last component is one Lean
|
|
425
|
-
* generates. */
|
|
389
|
+
/** The declaration a name the graph has no node for is folded into (shared/declfind.ts,
|
|
390
|
+
* `foldedAt`). */
|
|
426
391
|
foldedAt(v, name) {
|
|
427
|
-
|
|
428
|
-
const direct = g.foldedInto.get(name);
|
|
429
|
-
if (direct !== undefined)
|
|
430
|
-
return direct;
|
|
431
|
-
const dot = name.lastIndexOf(".");
|
|
432
|
-
if (dot <= 0)
|
|
433
|
-
return undefined;
|
|
434
|
-
const head = name.slice(0, dot), last = name.slice(dot + 1);
|
|
435
|
-
const at = g.byName.get(head) ?? g.foldedInto.get(head);
|
|
436
|
-
if (at === undefined || !last)
|
|
437
|
-
return undefined;
|
|
438
|
-
if (!GENERATED_LAST.test(last)) {
|
|
439
|
-
const most = last.length < 5 ? 1 : 2;
|
|
440
|
-
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;
|
|
441
|
-
for (const n of g.nodes)
|
|
442
|
-
if (near(n.name))
|
|
443
|
-
return undefined;
|
|
444
|
-
for (const n of g.foldedInto.keys())
|
|
445
|
-
if (near(n))
|
|
446
|
-
return undefined;
|
|
447
|
-
}
|
|
448
|
-
return at;
|
|
392
|
+
return foldedAt(v.g, name);
|
|
449
393
|
}
|
|
450
394
|
/** What an answer about `asked` says when that name is folded into declaration `i`. */
|
|
451
395
|
folded(v, asked, i) {
|
|
@@ -512,8 +456,10 @@ export class AgentApi {
|
|
|
512
456
|
return {
|
|
513
457
|
by: o.by,
|
|
514
458
|
agent: !!o.agent,
|
|
515
|
-
// someone invited by a personal link: the name is theirs (CollabSpec decision 9)
|
|
516
|
-
|
|
459
|
+
// someone invited by a personal link: the name is theirs (CollabSpec decision 9), unless
|
|
460
|
+
// someone else recorded it for them at the same screen, who is named (ReviewSpec decision 8)
|
|
461
|
+
...(o.person && !o.recordedBy ? { verified: true } : {}),
|
|
462
|
+
...(o.recordedBy ? { recordedBy: o.recordedBy.by } : {}),
|
|
517
463
|
verdict: o.verdict,
|
|
518
464
|
...(o.comment ? { comment: o.comment } : {}),
|
|
519
465
|
at: o.at,
|
|
@@ -717,6 +663,39 @@ export class AgentApi {
|
|
|
717
663
|
const tv = this.titleVersion(rec, at, version);
|
|
718
664
|
return ok({ view: rec.id, title: docTitle({ ...base, ...(tv !== undefined ? { version: tv } : {}) }, titleContext(this.world)), author: authorLine(authors), authors });
|
|
719
665
|
}
|
|
666
|
+
/** The outline of the document of view `view` at the build `version` names (ExportSpec decision
|
|
667
|
+
* 18; left out, the view's own): its sections, declarations and notes in the document's order,
|
|
668
|
+
* without their text, from the board as its page draws it (as `document` reads it). What the
|
|
669
|
+
* page's linear form draws (GET /api/export/outline); a view too large to print has one too. */
|
|
670
|
+
exportOutline(view, version) {
|
|
671
|
+
const rec = view ? this.world.views?.book.get(view) : undefined;
|
|
672
|
+
if (!rec)
|
|
673
|
+
return fail(404, `There is no view ${view.slice(0, 200)}.`, "GET /api/agent/views lists them.");
|
|
674
|
+
const at = this.exportAt(rec, version);
|
|
675
|
+
if (at !== null && typeof at === "object")
|
|
676
|
+
return at;
|
|
677
|
+
const v = this.graph();
|
|
678
|
+
if (!(v instanceof View))
|
|
679
|
+
return v;
|
|
680
|
+
const notes = this.world.views?.book.notes ?? [];
|
|
681
|
+
const memory = this.world.views?.book.memoryOf(rec.id, rec.clustering) ?? null;
|
|
682
|
+
const whole = v.meta.layouts?.[rec.clustering];
|
|
683
|
+
let g = v.g;
|
|
684
|
+
let review = this.world.review.data;
|
|
685
|
+
let past;
|
|
686
|
+
if (at !== null) {
|
|
687
|
+
const b = this.pasts.get(at, v.g, v.version, (m) => v.modules.get(m)?.file ?? null);
|
|
688
|
+
if (!b)
|
|
689
|
+
return fail(404, `There is no snapshot ${at}.`, "GET /api/agent/history lists them.");
|
|
690
|
+
past = { snapshot: at, nowName: b.past.nowName, now: v.g, nowReview: review };
|
|
691
|
+
g = b.past.graph;
|
|
692
|
+
review = reviewThen(review, b.past.nowName);
|
|
693
|
+
}
|
|
694
|
+
const set = evaluate(rec.scope, { g, review });
|
|
695
|
+
const plan = planDoc(g, set, rec.clustering, rec.hidden[rec.clustering] ?? []);
|
|
696
|
+
const board = boardSpots({ g, review, rec, memory, whole, notes, ...(past ? { past } : {}) });
|
|
697
|
+
return ok({ view: rec.id, ...(at !== null ? { version: at } : {}), outline: outlineOf(plan, g, board) });
|
|
698
|
+
}
|
|
720
699
|
/** The exported document's model (ExportSpec decision 6) of view `view` with `settings` at the
|
|
721
700
|
* build `version` names (decision 16; left out, the view's own), as a request sends them: what
|
|
722
701
|
* the page's Export and an agent typeset (POST /api/export, POST /api/agent/export), and what
|
|
@@ -956,7 +935,7 @@ export class AgentApi {
|
|
|
956
935
|
people: out,
|
|
957
936
|
privateCount: P.privateCount(),
|
|
958
937
|
agents,
|
|
959
|
-
note: "privateCount: how many people are here privately — not listed, not followable, nothing known of where they are. Each person's place is that of their liveliest page: the view, the tool (how a click selects), what is selected (a query), the declaration their panel tells of, whether its source window is open (and collapsed), the source windows they pinned (pinned: declaration, collapsed), and, where their page says them, Activity (open, kind, who, mine: whose targets), the lost-and-found (lostAndFound.open), the colouring (kind or status), edges
|
|
938
|
+
note: "privateCount: how many people are here privately — not listed, not followable, nothing known of where they are. Each person's place is that of their liveliest page: the view, the tool (how a click selects), what is selected (a query), the declaration their panel tells of, whether its source window is open (and collapsed), the source windows they pinned (pinned: declaration, collapsed), and, where their page says them, Activity (open, kind, who, mine: whose targets), the lost-and-found (lostAndFound.open), the colouring (kind or status), edges — all of which those following them see too. idle: no input for five minutes, or the page in the background. verified: invited by a personal link, so the name is theirs. To look where someone looks, POST /api/agent/view with their view and declaration; ?wait=<s>&since=<rev> waits for the next change (at most 60 s).",
|
|
960
939
|
});
|
|
961
940
|
}
|
|
962
941
|
/** A page's or an agent's place, as the companion tells it (and a conversation's message, where
|
|
@@ -986,7 +965,7 @@ export class AgentApi {
|
|
|
986
965
|
: v.lostFound?.open
|
|
987
966
|
? ", with the lost-and-found open"
|
|
988
967
|
: "";
|
|
989
|
-
const drawn = (v.scheme ? `, coloured by ${v.scheme}` : "") + (v.edges === false ? ", edges hidden" : "")
|
|
968
|
+
const drawn = (v.scheme ? `, coloured by ${v.scheme}` : "") + (v.edges === false ? ", edges hidden" : "");
|
|
990
969
|
return {
|
|
991
970
|
view: v.view ?? PROJECT,
|
|
992
971
|
viewTitle: title,
|
|
@@ -1004,7 +983,6 @@ export class AgentApi {
|
|
|
1004
983
|
...(v.lostFound ? { lostAndFound: { open: v.lostFound.open } } : {}),
|
|
1005
984
|
...(v.scheme ? { colouring: v.scheme } : {}),
|
|
1006
985
|
...(v.edges !== undefined ? { edges: v.edges } : {}),
|
|
1007
|
-
...(v.proofEdges !== undefined ? { proofEdges: v.proofEdges } : {}),
|
|
1008
986
|
inWords: words + then + shownBy + drawn + ".",
|
|
1009
987
|
};
|
|
1010
988
|
}
|
|
@@ -2012,7 +1990,7 @@ export class AgentApi {
|
|
|
2012
1990
|
// what the review's logs record, as the page reads it
|
|
2013
1991
|
for (const [name, ops] of Object.entries(d.opinions))
|
|
2014
1992
|
for (const o of ops)
|
|
2015
|
-
out.push({ at: o.at, kind: "opinion", by: o.by, agent: !!o.agent, name, verdict: o.verdict, ...(o.comment ? { comment: o.comment } : {}) });
|
|
1993
|
+
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 } : {}) });
|
|
2016
1994
|
// what an agent did at level full is marked as an agent's, as its advice is
|
|
2017
1995
|
for (const [name, t] of Object.entries(d.targets))
|
|
2018
1996
|
out.push({ at: t.at, kind: "target", by: t.by, agent: !!t.agent, name });
|
|
@@ -2194,6 +2172,10 @@ export class AgentApi {
|
|
|
2194
2172
|
unit = v.g.nodes[i].name;
|
|
2195
2173
|
}
|
|
2196
2174
|
const verdict = b.verdict;
|
|
2175
|
+
// an agent's advice is its own: people record for one another at one screen, agents never
|
|
2176
|
+
// (ReviewSpec decision 8)
|
|
2177
|
+
if (b.for !== undefined)
|
|
2178
|
+
return fail(403, "An agent records no opinion for anyone else.", "Leave out for: your advice is your own.");
|
|
2197
2179
|
// "dispute" was the old name of a comment (renamed 2026-10-09): refused, with the new name
|
|
2198
2180
|
if (verdict === "dispute")
|
|
2199
2181
|
return fail(400, "verdict is approve or comment: \"dispute\" is now \"comment\".", "Use comment: send {name, verdict: \"comment\", comment}.");
|
|
@@ -2494,6 +2476,66 @@ export class AgentApi {
|
|
|
2494
2476
|
}
|
|
2495
2477
|
return { place: cleanView(next), temporary };
|
|
2496
2478
|
}
|
|
2479
|
+
/** A link to a picture of the board (ExportSpec decision 20, drafted 2026-10-15): a recipe
|
|
2480
|
+
* (shared/artrecipe.ts) checked, and resolved against the board of the view it names — the
|
|
2481
|
+
* Project's layout in the view's clustering — so that a name, a target or a cluster the board has
|
|
2482
|
+
* not is refused here rather than drawn as a picture unlike the one meant; the selection it gives
|
|
2483
|
+
* goes in the address, the recipe after it. The page that opens the link resolves it again
|
|
2484
|
+
* against the board it opens on, opens the artwork mode, and saves the files asked for. Nothing
|
|
2485
|
+
* changes here, and the link holds no key. */
|
|
2486
|
+
async artwork(a) {
|
|
2487
|
+
const no = needAgent(a.caller);
|
|
2488
|
+
if (no)
|
|
2489
|
+
return no;
|
|
2490
|
+
const b = body(a);
|
|
2491
|
+
const read = readRecipe(b.recipe);
|
|
2492
|
+
if ("error" in read)
|
|
2493
|
+
return fail(400, read.error, read.hint);
|
|
2494
|
+
const r = read.recipe;
|
|
2495
|
+
const save = b.save === undefined || b.save === null ? [] : typeof b.save === "string" ? b.save.split(",").map((k) => k.trim()) : null;
|
|
2496
|
+
if (!save || save.some((k) => k !== "png" && k !== "svg"))
|
|
2497
|
+
return fail(400, "save is png, svg or png,svg.", "The files the page saves once the board is ready; left out, none.");
|
|
2498
|
+
const v = this.graph();
|
|
2499
|
+
if (!(v instanceof View))
|
|
2500
|
+
return v;
|
|
2501
|
+
let sel = null;
|
|
2502
|
+
if (r.selection) {
|
|
2503
|
+
const i = this.find(v, r.selection.name, "selection.name");
|
|
2504
|
+
if (typeof i !== "number")
|
|
2505
|
+
return i;
|
|
2506
|
+
sel = v.g.nodes[i].name;
|
|
2507
|
+
}
|
|
2508
|
+
const rec = r.view && r.view !== PROJECT ? this.world.views?.book.get(r.view) : undefined;
|
|
2509
|
+
if (r.view && r.view !== PROJECT && !rec)
|
|
2510
|
+
return fail(404, `There is no view ${r.view}.`, "GET /api/agent/views lists them; left out, the Project.");
|
|
2511
|
+
const L = v.meta.layouts[rec?.clustering ?? "namespace"];
|
|
2512
|
+
const res = resolveRecipe(r, {
|
|
2513
|
+
clusters: L.clusters,
|
|
2514
|
+
clusterOf: (name) => {
|
|
2515
|
+
const i = v.g.byName.get(name);
|
|
2516
|
+
return i === undefined ? undefined : L.cluster[i];
|
|
2517
|
+
},
|
|
2518
|
+
targets: Object.keys(this.world.review.data.targets),
|
|
2519
|
+
projectCount: v.meta.projectCount,
|
|
2520
|
+
});
|
|
2521
|
+
if (res.problems.length)
|
|
2522
|
+
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 });
|
|
2523
|
+
const address = addressOf({ view: rec ? rec.id : null, scope: null, mode: r.selection?.mode ?? "tcb", selection: null, sel });
|
|
2524
|
+
const link = `${a.base ?? ""}/#${address}&${RECIPE_PARAM}=${encodeRecipe(r)}${save.length ? `&${SAVE_PARAM}=${[...new Set(save)].join(",")}` : ""}`;
|
|
2525
|
+
const look = res.look;
|
|
2526
|
+
return ok({
|
|
2527
|
+
ok: true,
|
|
2528
|
+
link,
|
|
2529
|
+
length: link.length,
|
|
2530
|
+
size: [look.w, look.h],
|
|
2531
|
+
names: look.labels === "chosen" ? res.named : look.labels,
|
|
2532
|
+
targets: look.targets === "chosen" ? look.shownTargets : look.targets,
|
|
2533
|
+
selection: sel ? { name: sel, mode: r.selection.mode ?? "tcb" } : null,
|
|
2534
|
+
// names and targets beyond what the artwork carries, left out: said, not refused
|
|
2535
|
+
left: res.left,
|
|
2536
|
+
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.",
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2497
2539
|
/** A link to what the agent looks at, or to a place it gives over its own, as a person's Share makes
|
|
2498
2540
|
* one with Exactly what I see (CollabSpec decision 7, the owner's decision of 2026-10-12): the
|
|
2499
2541
|
* address, and after it a snapshot of everything followed, which the page that opens it applies
|
|
@@ -2519,7 +2561,8 @@ export class AgentApi {
|
|
|
2519
2561
|
place[k] = b[k];
|
|
2520
2562
|
return null;
|
|
2521
2563
|
};
|
|
2522
|
-
|
|
2564
|
+
// (proofEdges, said until 2026-10-15, is no more: left as it is, as any word not known)
|
|
2565
|
+
const bad = bool("sourceCollapsed") ?? bool("edges");
|
|
2523
2566
|
if (bad)
|
|
2524
2567
|
return bad;
|
|
2525
2568
|
if (b.lostFound !== undefined) {
|
|
@@ -2744,7 +2787,7 @@ export class AgentApi {
|
|
|
2744
2787
|
{
|
|
2745
2788
|
method: "GET", path: "/api/agent/companion", brief: "who is on the page, and where each one looks", about: "Who is on the page now and where each one is — every person (the place of their liveliest page: view, tool, selection, the declaration in their panel, the source window and the pinned ones, Activity, the lost-and-found, the colouring and the edges, idle or not) and the agents present — to follow the people you help; privateCount says how many people are here privately, who are not listed and cannot be followed. With wait, the answer waits until someone comes, goes or moves.",
|
|
2746
2789
|
params: { wait: "optional: seconds to wait for a change (at most 60)", since: "optional: the rev of an earlier answer; with wait, the answer comes as soon as the revision is another" },
|
|
2747
|
-
returns: "{rev, changed?, now, sharedBy?: {name, here}, sharedByNote?, people: [{name, idle, pages, place: {view, viewTitle, targets?, scope?, mode, tool, selection, declaration, source, sourceCollapsed, pinned: [{declaration, collapsed}], history?, activity?: {open, kind, who, mine?}, lostAndFound?: {open}, colouring?, edges?,
|
|
2790
|
+
returns: "{rev, changed?, now, sharedBy?: {name, here}, sharedByNote?, people: [{name, idle, pages, place: {view, viewTitle, targets?, scope?, mode, tool, selection, declaration, source, sourceCollapsed, pinned: [{declaration, collapsed}], history?, activity?: {open, kind, who, mine?}, lostAndFound?: {open}, colouring?, edges?, inWords}}], privateCount, agents: [{name, idle, state?, status?, lastSeen?, you?, place}], note}",
|
|
2748
2791
|
run: (a) => this.companion(a),
|
|
2749
2792
|
},
|
|
2750
2793
|
{
|
|
@@ -2881,10 +2924,16 @@ export class AgentApi {
|
|
|
2881
2924
|
},
|
|
2882
2925
|
{
|
|
2883
2926
|
method: "POST", path: "/api/agent/view/link", about: "A link to what you look at, or to a place you give, that opens exactly that for whoever opens it, as a person's Share makes with Exactly what I see (the owner's decision of 2026-10-12): the address, and after it a snapshot of everything followed — the view, the build in the Time Machine, the region of the board, the tool, the selection, the focus, the panel, the source windows, the reference window, Activity, the lost-and-found, the colouring, the edges. Fields you send are taken over your own place (as POST view takes them); nothing moves, you neither. The page that opens it applies it once, and says in a line what is no longer there. It holds no key: it opens for whoever is let in already, the people on the page.",
|
|
2884
|
-
params: { "view, review, sel, selection, mode, source, focus, paper": "as POST view: over your own place", sourceCollapsed: "true: the source window following sel is collapsed to its pill", pinned: `source windows pinned, [{name, collapsed?}], at most ${MAX_PINNED}, the first pinned first`, history: "a snapshot's id (GET /api/agent/history): the Time Machine at that build; null for none", region: "[x, y, w, h]: the region of the board framed — its centre, width and height in the board's world — fitted to the opener's screen; left out, framed as for any link", activity: "{open, kind?, who?, mine?}: Activity open or closed, its filters", lostFound: "true: the lost-and-found open", scheme: "kind or status: the colouring", edges: "edges drawn",
|
|
2927
|
+
params: { "view, review, sel, selection, mode, source, focus, paper": "as POST view: over your own place", sourceCollapsed: "true: the source window following sel is collapsed to its pill", pinned: `source windows pinned, [{name, collapsed?}], at most ${MAX_PINNED}, the first pinned first`, history: "a snapshot's id (GET /api/agent/history): the Time Machine at that build; null for none", region: "[x, y, w, h]: the region of the board framed — its centre, width and height in the board's world — fitted to the opener's screen; left out, framed as for any link", activity: "{open, kind?, who?, mine?}: Activity open or closed, its filters", lostFound: "true: the lost-and-found open", scheme: "kind or status: the colouring", edges: "edges drawn", exact: "false: only the view, as Share's switch turned off makes it (the address alone)" },
|
|
2885
2928
|
returns: "{ok, link, exact, carries: what the link opens on, length, note, temporary?}",
|
|
2886
2929
|
run: (a) => this.viewLink(a),
|
|
2887
2930
|
},
|
|
2931
|
+
{
|
|
2932
|
+
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).",
|
|
2933
|
+
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" },
|
|
2934
|
+
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}",
|
|
2935
|
+
run: (a) => this.artwork(a),
|
|
2936
|
+
},
|
|
2888
2937
|
{
|
|
2889
2938
|
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.",
|
|
2890
2939
|
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}` },
|
|
@@ -3139,6 +3188,8 @@ export function doingOf(a) {
|
|
|
3139
3188
|
return of("going to", clip(b.sel)) ?? of("going to the view of", clip(b.review)) ?? "moving its view";
|
|
3140
3189
|
case "/api/agent/view/link":
|
|
3141
3190
|
return "making a link to what it looks at";
|
|
3191
|
+
case "/api/agent/artwork":
|
|
3192
|
+
return "making a link to a picture of the board";
|
|
3142
3193
|
case "/api/agent/ask-follow":
|
|
3143
3194
|
return clip(b.who) ? `asking ${clip(b.who)} to follow it` : "asking everyone to follow it";
|
|
3144
3195
|
case "/api/agent/opinion":
|
|
@@ -3191,24 +3242,6 @@ const str = (x) => (typeof x === "string" ? x : null);
|
|
|
3191
3242
|
export function whoOf(c) {
|
|
3192
3243
|
return { name: c.name ?? "", ...(c.token ? { token: c.token.id } : {}) };
|
|
3193
3244
|
}
|
|
3194
|
-
/** The edit distance between two short strings, or `most + 1` once it is more than `most`. */
|
|
3195
|
-
function editDistance(a, b, most) {
|
|
3196
|
-
if (Math.abs(a.length - b.length) > most)
|
|
3197
|
-
return most + 1;
|
|
3198
|
-
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
3199
|
-
for (let i = 1; i <= a.length; i++) {
|
|
3200
|
-
const row = [i];
|
|
3201
|
-
let best = i;
|
|
3202
|
-
for (let j = 1; j <= b.length; j++) {
|
|
3203
|
-
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
3204
|
-
best = Math.min(best, row[j]);
|
|
3205
|
-
}
|
|
3206
|
-
if (best > most)
|
|
3207
|
-
return most + 1;
|
|
3208
|
-
prev = row;
|
|
3209
|
-
}
|
|
3210
|
-
return Math.min(prev[b.length], most + 1);
|
|
3211
|
-
}
|
|
3212
3245
|
/** An integer parameter within bounds, `dflt` when absent or not a number. */
|
|
3213
3246
|
function clampInt(s, dflt, lo, hi) {
|
|
3214
3247
|
const n = s === null || s.trim() === "" ? NaN : Number(s);
|
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" },
|