yarramate 1.24.0 → 1.25.1

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.
Files changed (36) hide show
  1. package/dist/adapters/visual/layout-sidecar.d.ts +28 -0
  2. package/dist/adapters/visual/layout-sidecar.js +61 -0
  3. package/dist/adapters/visual/protocol-contract.d.ts +19 -0
  4. package/dist/adapters/visual/session-server.js +28 -35
  5. package/dist/adapters/visual/wire.d.ts +9 -1
  6. package/dist/notation/archimate.d.ts +6 -1
  7. package/dist/notation/archimate.js +9 -4
  8. package/dist/schema-validation.d.ts +1 -0
  9. package/dist/schema-validation.js +1 -0
  10. package/dist/schema-validators.generated.d.ts +6 -0
  11. package/dist/schema-validators.generated.js +592 -1
  12. package/dist/visual-app/assets/elk-worker.min-D8OVqK8T.js +22 -0
  13. package/dist/visual-app/assets/elk.bundled-CLux5E_P.js +24 -0
  14. package/dist/visual-app/assets/index-DGkU2CSB.js +369 -0
  15. package/dist/visual-app/index.html +1 -1
  16. package/dist/visual-app-lib/editor.js +31036 -29678
  17. package/dist/visual-app-lib/types/adapters/visual/layout-sidecar.d.ts +28 -0
  18. package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +19 -0
  19. package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +9 -1
  20. package/dist/visual-app-lib/types/notation/archimate.d.ts +6 -1
  21. package/dist/visual-app-lib/types/schema-validation.d.ts +1 -0
  22. package/dist/visual-app-lib/types/schema-validators.generated.d.ts +6 -0
  23. package/dist/visual-app-lib/types/visual-app/edge-routes.d.ts +17 -1
  24. package/dist/visual-app-lib/types/visual-app/elk-layout.d.ts +42 -5
  25. package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +35 -5
  26. package/dist/visual-app-lib/types/visual-app/kind-icons.d.ts +1 -1
  27. package/dist/visual-app-lib/types/visual-app/layout-controls.d.ts +12 -6
  28. package/dist/visual-app-lib/types/visual-app/mount.d.ts +12 -0
  29. package/dist/visual-app-lib/types/visual-app/save-view.d.ts +15 -6
  30. package/dist/visual-app-lib/types/visual-app/style-presets.d.ts +39 -0
  31. package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +9 -0
  32. package/docs/CONSUMING-YARRAMATE.md +16 -0
  33. package/package.json +1 -1
  34. package/schema/yarramate-visual-event.schema.json +10 -0
  35. package/schema/yarramate-visual-layout.schema.json +31 -0
  36. package/dist/visual-app/assets/index-wKXrkA2V.js +0 -392
@@ -0,0 +1,28 @@
1
+ import type { VisualLayoutPositions, VisualLayoutRoutes } from './protocol-contract.js';
2
+ /** Where every host writes a projection's sidecar, and reads it back from. */
3
+ export declare const LAYOUT_SIDECAR_DIR = ".yarramate/visual-layout";
4
+ export declare const layoutSidecarPath: (projectionId: string) => string;
5
+ /** A path a store or directory holds that could be a sidecar. */
6
+ export declare const isLayoutSidecarPath: (path: string) => boolean;
7
+ export interface LayoutSidecars {
8
+ readonly layouts: Record<string, VisualLayoutPositions>;
9
+ readonly folds: Record<string, {
10
+ folded: string[];
11
+ unfolded: string[];
12
+ }>;
13
+ readonly routes: Record<string, VisualLayoutRoutes>;
14
+ }
15
+ /**
16
+ * What the sidecars say, keyed by the projection id each one names.
17
+ *
18
+ * Presentation state must never fail a session (ADR 0023): a source that does
19
+ * not parse or does not validate is skipped, not reported. A sidecar written
20
+ * before #473 has neither fold list and says nothing about folding rather
21
+ * than "fold nothing" - the view's own default decides - so only a sidecar
22
+ * that STATES a fold yields a `folds` entry. A sidecar written before the
23
+ * routes says nothing about them, and the layout recomputes.
24
+ */
25
+ export declare const readLayoutSidecars: (sources: Iterable<{
26
+ readonly path: string;
27
+ readonly source: string;
28
+ }>) => LayoutSidecars;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The layout sidecar, read back the one way (#503).
3
+ *
4
+ * A drag saves `.yarramate/visual-layout/<projectionId>.yaml` (ADR 0085), and
5
+ * since 1.25.0 the routes and the fold state ride in it beside the positions
6
+ * (ADR 0147, #473). Two hosts write that file and two hosts have to read it:
7
+ * the session server from disk, the local host a product mounts from its
8
+ * store. Until this module the server had a reader and the local host had
9
+ * none, so on a mounted host a saved drag was written faithfully and never
10
+ * applied again - found by ApertureX on 1.25.0 (#503). One reader here, and
11
+ * the two cannot drift apart a second time.
12
+ *
13
+ * Pure: it takes sources and returns what the model carries. Whoever can read
14
+ * a directory or a store hands the bytes over.
15
+ */
16
+ import { parse } from 'yaml';
17
+ import { validateVisualLayout } from '../../schema-validation.js';
18
+ /** Where every host writes a projection's sidecar, and reads it back from. */
19
+ export const LAYOUT_SIDECAR_DIR = '.yarramate/visual-layout';
20
+ export const layoutSidecarPath = (projectionId) => `${LAYOUT_SIDECAR_DIR}/${projectionId}.yaml`;
21
+ /** A path a store or directory holds that could be a sidecar. */
22
+ export const isLayoutSidecarPath = (path) => path.startsWith(`${LAYOUT_SIDECAR_DIR}/`) &&
23
+ !path.slice(LAYOUT_SIDECAR_DIR.length + 1).includes('/') &&
24
+ (path.endsWith('.yaml') || path.endsWith('.yml'));
25
+ /**
26
+ * What the sidecars say, keyed by the projection id each one names.
27
+ *
28
+ * Presentation state must never fail a session (ADR 0023): a source that does
29
+ * not parse or does not validate is skipped, not reported. A sidecar written
30
+ * before #473 has neither fold list and says nothing about folding rather
31
+ * than "fold nothing" - the view's own default decides - so only a sidecar
32
+ * that STATES a fold yields a `folds` entry. A sidecar written before the
33
+ * routes says nothing about them, and the layout recomputes.
34
+ */
35
+ export const readLayoutSidecars = (sources) => {
36
+ const layouts = {};
37
+ const folds = {};
38
+ const routes = {};
39
+ for (const { source } of sources) {
40
+ let parsed;
41
+ try {
42
+ parsed = parse(source);
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ if (!validateVisualLayout(parsed))
48
+ continue;
49
+ const sidecar = parsed;
50
+ layouts[sidecar.projectionId] = sidecar.positions;
51
+ if (sidecar.routes !== undefined)
52
+ routes[sidecar.projectionId] = sidecar.routes;
53
+ if (sidecar.folded !== undefined || sidecar.unfolded !== undefined) {
54
+ folds[sidecar.projectionId] = {
55
+ folded: [...(sidecar.folded ?? [])],
56
+ unfolded: [...(sidecar.unfolded ?? [])],
57
+ };
58
+ }
59
+ }
60
+ return { layouts, folds, routes };
61
+ };
@@ -267,6 +267,23 @@ export interface VisualLayoutPositions {
267
267
  readonly y: number;
268
268
  };
269
269
  }
270
+ /**
271
+ * The routes the canvas was drawing when a layout was saved (ADR 0147), keyed
272
+ * by relationship id: absolute canvas coordinates from the source end, both
273
+ * endpoints included, and where along the route the label sits. Saved WITH
274
+ * the positions, because a route is only right for the positions it was
275
+ * computed for: a reader who moved one subject keeps every other edge's
276
+ * route, and only the moved subject's edges fall back to a straight line.
277
+ */
278
+ export interface VisualLayoutRoutes {
279
+ readonly [relationshipId: string]: {
280
+ readonly points: readonly {
281
+ readonly x: number;
282
+ readonly y: number;
283
+ }[];
284
+ readonly labelAt: number | null;
285
+ };
286
+ }
270
287
  /**
271
288
  * A change to a projection document, staged rather than written (ADR 0103).
272
289
  *
@@ -332,6 +349,8 @@ export interface VisualLayoutSavePayload {
332
349
  */
333
350
  readonly folded?: readonly string[];
334
351
  readonly unfolded?: readonly string[];
352
+ /** The routes in force at save time (ADR 0147); absent when nothing was routed. */
353
+ readonly routes?: VisualLayoutRoutes;
335
354
  }
336
355
  /**
337
356
  * Terminal event payload. Every reason is the runtime's to choose: only it
@@ -6,7 +6,6 @@ import { basename, extname, join, relative, resolve, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import Ajv2020Module from "ajv/dist/2020.js";
8
8
  import { WebSocketServer } from "ws";
9
- import { parse } from "yaml";
10
9
  import { VISUAL_LIMITS, VISUAL_PROTOCOL_VERSION, digestOf, parseVisualBrowserInput, parseVisualResponse, parseVisualSessionStarted, parseVisualStatus, toWireFileUri, visualBrowserInputType, } from "./protocol.js";
11
10
  import { appendTerminalEvent, appendVisualEvent, appendVisualResponse, createVisualSession, isActionableVisualEvent, recoverVisualSession, removeVisualSession, writeVisualSessionDescriptor, } from "./session-store.js";
12
11
  import { loadProjection, } from "../../projection.js";
@@ -17,17 +16,13 @@ import { planOperations, posixDirectoryOf, writeConflictDiagnostics, } from "../
17
16
  import { createFileSystemStore } from "../../source-store.js";
18
17
  import { emitYaml } from "../../yaml-emission.js";
19
18
  import { loadWorkspaceManifest, } from "../../workspace.js";
20
- import visualLayoutSchema from "../../../schema/yarramate-visual-layout.schema.json" with { type: "json" };
21
- import visualProjectionSchema from "../../../schema/yarramate-projection.schema.json" with { type: "json" };
19
+ import { LAYOUT_SIDECAR_DIR, isLayoutSidecarPath, layoutSidecarPath, readLayoutSidecars, } from "./layout-sidecar.js";
22
20
  // The layout sidecar is adapter-owned presentation state (ADR 0023) that
23
21
  // protocol.ts's validators never touch — the browser's own `layout.save`
24
22
  // payload is already schema-validated by `parseVisualBrowserInput` before it
25
23
  // reaches this module — so this file compiles its own single-purpose
26
24
  // validator for the sidecar files it reads directly off disk.
27
25
  const Ajv2020 = Ajv2020Module.default;
28
- const layoutAjv = new Ajv2020({ allErrors: true });
29
- layoutAjv.addSchema(visualProjectionSchema);
30
- const validateVisualLayout = layoutAjv.compile(visualLayoutSchema);
31
26
  /**
32
27
  * Returned on every response. The policy admits no external origin at all, so
33
28
  * neither a compromised model nor a hostile chat message can reach the network,
@@ -454,44 +449,30 @@ export const startVisualServer = async (options) => {
454
449
  // Drag positions are adapter-owned presentation state (ADR 0023): never
455
450
  // validated by Core. An invalid or unreadable sidecar is skipped exactly
456
451
  // like a broken saved view above — presentation state must never fail a
457
- // session.
458
- const layoutDir = resolve(options.cwd, ".yarramate/visual-layout");
459
- const { layouts, folds } = (() => {
460
- const layouts = {};
461
- const folds = {};
452
+ // session. The reading itself is shared with the local host (#503): this
453
+ // host can read a directory, so it hands the bytes over.
454
+ const layoutDir = resolve(options.cwd, LAYOUT_SIDECAR_DIR);
455
+ const { layouts, folds, routes } = readLayoutSidecars((() => {
462
456
  let entries;
463
457
  try {
464
458
  entries = readdirSync(layoutDir);
465
459
  }
466
460
  catch {
467
- return { layouts, folds };
461
+ return [];
468
462
  }
469
- for (const entry of entries) {
470
- if (extname(entry) !== ".yaml" && extname(entry) !== ".yml")
471
- continue;
463
+ return entries.flatMap((entry) => {
464
+ const path = `${LAYOUT_SIDECAR_DIR}/${entry}`;
465
+ if (!isLayoutSidecarPath(path))
466
+ return [];
472
467
  try {
473
- const source = readFileSync(join(layoutDir, entry), "utf8");
474
- const parsed = parse(source);
475
- if (!validateVisualLayout(parsed))
476
- continue;
477
- const sidecar = parsed;
478
- layouts[sidecar.projectionId] = sidecar.positions;
479
- // A sidecar written before #473 has neither list, and says nothing
480
- // about folding rather than saying "fold nothing" - the view's own
481
- // default decides for it. Only a sidecar that STATES a fold overrides.
482
- if (sidecar.folded !== undefined || sidecar.unfolded !== undefined) {
483
- folds[sidecar.projectionId] = {
484
- folded: [...(sidecar.folded ?? [])],
485
- unfolded: [...(sidecar.unfolded ?? [])],
486
- };
487
- }
468
+ return [{ path, source: readFileSync(join(layoutDir, entry), "utf8") }];
488
469
  }
489
470
  catch {
490
471
  // Skipped sidecar: presentation state must never fail a session.
472
+ return [];
491
473
  }
492
- }
493
- return { layouts, folds };
494
- })();
474
+ });
475
+ })());
495
476
  // `request.initialModel.graph` is the caller's compile (`buildVisualModelGraph`,
496
477
  // before invoking `yarramate-visual start`) and is only the fallback below:
497
478
  // `recompileWorkspace` immediately below is the one path that actually fills
@@ -505,6 +486,7 @@ export const startVisualServer = async (options) => {
505
486
  vocabulary: { conceptKinds: [], relationshipKinds: [] },
506
487
  layouts,
507
488
  ...(Object.keys(folds).length === 0 ? {} : { folds }),
489
+ ...(Object.keys(routes).length === 0 ? {} : { routes }),
508
490
  sourceDigests: request.initialModel.sourceDigests,
509
491
  // The request's model has no projections in it - `visual-model/v1` carries
510
492
  // a graph, not a workspace - so the fallback states nothing rather than
@@ -643,6 +625,11 @@ export const startVisualServer = async (options) => {
643
625
  initialView: rendered.initialView,
644
626
  documents: resolvedWorkspace.documents,
645
627
  layouts: rendered.layouts,
628
+ // The fold state and the routes the sidecars said, or a save set,
629
+ // carried across the recompile like the positions are (#503): a
630
+ // commit is not a reason to forget how the reviewer arranged things.
631
+ ...(rendered.folds === undefined ? {} : { folds: rendered.folds }),
632
+ ...(rendered.routes === undefined ? {} : { routes: rendered.routes }),
646
633
  // Minted from the bytes this compile just read, so what the browser
647
634
  // renders and what it can later claim it rendered are the same read.
648
635
  sourceDigests: Object.fromEntries(sources.map(({ path, source }) => [path, digestOf(source)])),
@@ -1424,7 +1411,7 @@ export const startVisualServer = async (options) => {
1424
1411
  // never `git commit`ed. It never asks the agent anything, so it is
1425
1412
  // answered here directly rather than through the pending queue a
1426
1413
  // poll would drain.
1427
- const { projectionId, positions, folded, unfolded } = event.payload;
1414
+ const { projectionId, positions, folded, unfolded, routes: savedRoutes } = event.payload;
1428
1415
  if (!views.some((view) => view.id === projectionId)) {
1429
1416
  sendFrame(socket, {
1430
1417
  kind: "layout-save-result",
@@ -1435,7 +1422,7 @@ export const startVisualServer = async (options) => {
1435
1422
  });
1436
1423
  return;
1437
1424
  }
1438
- const path = `.yarramate/visual-layout/${projectionId}.yaml`;
1425
+ const path = layoutSidecarPath(projectionId);
1439
1426
  mkdirSync(layoutDir, { recursive: true });
1440
1427
  writeFileSync(resolve(options.cwd, path), emitYaml({
1441
1428
  format: "yarramate/visual-layout/v1",
@@ -1446,10 +1433,16 @@ export const startVisualServer = async (options) => {
1446
1433
  // sidecar written by one host is read by the other.
1447
1434
  ...(folded === undefined ? {} : { folded }),
1448
1435
  ...(unfolded === undefined ? {} : { unfolded }),
1436
+ // The routes in force, beside the positions they were computed
1437
+ // for (ADR 0147); a save with nothing routed writes none.
1438
+ ...(savedRoutes === undefined ? {} : { routes: savedRoutes }),
1449
1439
  }), "utf8");
1450
1440
  rendered = {
1451
1441
  ...rendered,
1452
1442
  layouts: { ...rendered.layouts, [projectionId]: positions },
1443
+ ...(savedRoutes === undefined
1444
+ ? {}
1445
+ : { routes: { ...rendered.routes, [projectionId]: savedRoutes } }),
1453
1446
  ...(folded === undefined && unfolded === undefined
1454
1447
  ? {}
1455
1448
  : {
@@ -1,6 +1,6 @@
1
1
  import type { CanvasGraph } from '../../graph-projection.js';
2
2
  import type { PatternMembership, PatternVacancy } from '../../compiler.js';
3
- import type { VISUAL_PROTOCOL_VERSION, VisualApplyResultPayload, VisualAuthority, VisualBrowserInput, VisualCapabilities, VisualChoicePresentPayload, VisualDiagnostic, VisualFilterResultPayload, VisualFreezeReason, VisualKindOption, VisualPatternOption, VisualLayoutPositions, VisualLayoutSaveResultPayload, VisualResponse, VisualTerminationReason, VisualViewSummary } from './protocol-contract.js';
3
+ import type { VISUAL_PROTOCOL_VERSION, VisualApplyResultPayload, VisualAuthority, VisualBrowserInput, VisualCapabilities, VisualChoicePresentPayload, VisualDiagnostic, VisualFilterResultPayload, VisualFreezeReason, VisualKindOption, VisualPatternOption, VisualLayoutPositions, VisualLayoutRoutes, VisualLayoutSaveResultPayload, VisualResponse, VisualTerminationReason, VisualViewSummary } from './protocol-contract.js';
4
4
  /**
5
5
  * Transport shapes the session server and the browser application both speak.
6
6
  *
@@ -77,6 +77,14 @@ export interface VisualRenderedModel {
77
77
  readonly unfolded: readonly string[];
78
78
  };
79
79
  };
80
+ /**
81
+ * The routes each saved layout was drawing (ADR 0147), keyed by projection
82
+ * id. A sibling of `layouts` for the reason `folds` is: a layout entry is
83
+ * positions, and widening it would reach every reader of it.
84
+ */
85
+ readonly routes?: {
86
+ readonly [projectionId: string]: VisualLayoutRoutes;
87
+ };
80
88
  /**
81
89
  * Which subject fills which slot of which instance (ADR 0131), and which
82
90
  * slots nothing fills (#447), forwarded so the browser can draw containment
@@ -55,7 +55,12 @@ export interface ConceptNotation extends ShapeMeta {
55
55
  }
56
56
  export declare const CONCEPT_NOTATION: readonly ConceptNotation[];
57
57
  export declare function conceptNotationOf(kindLabel: string): ConceptNotation | null;
58
- export declare function kindGlyphDataUriOf(kindLabel: string): string | null;
58
+ /**
59
+ * The glyph as a data URI, drawn in `ink` - the notation's own by default, or
60
+ * the light stroke a dark ground needs (ADR 0148). The stroke is the only
61
+ * thing that varies; the strokes themselves are the notation's.
62
+ */
63
+ export declare function kindGlyphDataUriOf(kindLabel: string, ink?: string): string | null;
59
64
  export interface ArrowNotation {
60
65
  readonly shape: 'none' | 'diamond' | 'triangle' | 'circle' | 'vee';
61
66
  readonly fill?: 'filled' | 'hollow';
@@ -38,10 +38,10 @@ export const ASPECT_SHAPES = {
38
38
  const KIND_SHAPE_OVERRIDES = {
39
39
  grouping: { borderStyle: 'dashed' },
40
40
  };
41
- function svg(body) {
41
+ function svg(body, ink = INK) {
42
42
  return (`<svg xmlns="http://www.w3.org/2000/svg" width="${ICON_SIZE}" height="${ICON_SIZE}" ` +
43
43
  `viewBox="0 0 ${ICON_SIZE} ${ICON_SIZE}">` +
44
- `<g fill="none" stroke="${INK}" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">${body}</g>` +
44
+ `<g fill="none" stroke="${ink}" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">${body}</g>` +
45
45
  `</svg>`);
46
46
  }
47
47
  function toDataUri(svgMarkup) {
@@ -205,9 +205,14 @@ const CONCEPT_NOTATION_BY_ID = Object.fromEntries(CONCEPT_NOTATION.map((row) =>
205
205
  export function conceptNotationOf(kindLabel) {
206
206
  return CONCEPT_NOTATION_BY_ID[kindLabel] ?? null;
207
207
  }
208
- export function kindGlyphDataUriOf(kindLabel) {
208
+ /**
209
+ * The glyph as a data URI, drawn in `ink` - the notation's own by default, or
210
+ * the light stroke a dark ground needs (ADR 0148). The stroke is the only
211
+ * thing that varies; the strokes themselves are the notation's.
212
+ */
213
+ export function kindGlyphDataUriOf(kindLabel, ink = INK) {
209
214
  const glyph = CONCEPT_NOTATION_BY_ID[kindLabel]?.glyph;
210
- return glyph == null ? null : toDataUri(svg(glyph));
215
+ return glyph == null ? null : toDataUri(svg(glyph, ink));
211
216
  }
212
217
  // The 11 rows from `graph-canvas.tsx`'s ArchiMate edge selectors (Task 11).
213
218
  const RELATIONSHIP_STYLE = {
@@ -39,4 +39,5 @@ export declare const validateEvidence: ValidateFunction;
39
39
  export declare const validateCoreContract: ValidateFunction;
40
40
  export declare const validateAdapterMapping: ValidateFunction;
41
41
  export declare const validateCatalogue: ValidateFunction;
42
+ export declare const validateVisualLayout: ValidateFunction;
42
43
  export declare const validateOperations: ValidateFunction;
@@ -40,4 +40,5 @@ export const validateEvidence = generated.validateEvidence;
40
40
  export const validateCoreContract = generated.validateCoreContract;
41
41
  export const validateAdapterMapping = generated.validateAdapterMapping;
42
42
  export const validateCatalogue = generated.validateCatalogue;
43
+ export const validateVisualLayout = generated.validateVisualLayout;
43
44
  export const validateOperations = generatedOperations.validateOperations;
@@ -52,4 +52,10 @@ declare function validate74(data: any, { instancePath, parentData, parentDataPro
52
52
  instancePath?: string | undefined;
53
53
  rootData?: any;
54
54
  }): boolean;
55
+ export declare const validateVisualLayout: typeof validate82;
56
+ declare function validate82(data: any, { instancePath, parentData, parentDataProperty, rootData, dynamicAnchors }?: {
57
+ dynamicAnchors?: {} | undefined;
58
+ instancePath?: string | undefined;
59
+ rootData?: any;
60
+ }): boolean;
55
61
  export {};