yarramate 1.25.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.
@@ -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
+ };
@@ -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,49 +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, routes } = (() => {
460
- const layouts = {};
461
- const folds = {};
462
- const routes = {};
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((() => {
463
456
  let entries;
464
457
  try {
465
458
  entries = readdirSync(layoutDir);
466
459
  }
467
460
  catch {
468
- return { layouts, folds, routes };
461
+ return [];
469
462
  }
470
- for (const entry of entries) {
471
- if (extname(entry) !== ".yaml" && extname(entry) !== ".yml")
472
- continue;
463
+ return entries.flatMap((entry) => {
464
+ const path = `${LAYOUT_SIDECAR_DIR}/${entry}`;
465
+ if (!isLayoutSidecarPath(path))
466
+ return [];
473
467
  try {
474
- const source = readFileSync(join(layoutDir, entry), "utf8");
475
- const parsed = parse(source);
476
- if (!validateVisualLayout(parsed))
477
- continue;
478
- const sidecar = parsed;
479
- layouts[sidecar.projectionId] = sidecar.positions;
480
- // The routes the canvas was drawing when it saved (ADR 0147). A
481
- // sidecar written before them says nothing, and the layout recomputes.
482
- if (sidecar.routes !== undefined)
483
- routes[sidecar.projectionId] = sidecar.routes;
484
- // A sidecar written before #473 has neither list, and says nothing
485
- // about folding rather than saying "fold nothing" - the view's own
486
- // default decides for it. Only a sidecar that STATES a fold overrides.
487
- if (sidecar.folded !== undefined || sidecar.unfolded !== undefined) {
488
- folds[sidecar.projectionId] = {
489
- folded: [...(sidecar.folded ?? [])],
490
- unfolded: [...(sidecar.unfolded ?? [])],
491
- };
492
- }
468
+ return [{ path, source: readFileSync(join(layoutDir, entry), "utf8") }];
493
469
  }
494
470
  catch {
495
471
  // Skipped sidecar: presentation state must never fail a session.
472
+ return [];
496
473
  }
497
- }
498
- return { layouts, folds, routes };
499
- })();
474
+ });
475
+ })());
500
476
  // `request.initialModel.graph` is the caller's compile (`buildVisualModelGraph`,
501
477
  // before invoking `yarramate-visual start`) and is only the fallback below:
502
478
  // `recompileWorkspace` immediately below is the one path that actually fills
@@ -649,6 +625,11 @@ export const startVisualServer = async (options) => {
649
625
  initialView: rendered.initialView,
650
626
  documents: resolvedWorkspace.documents,
651
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 }),
652
633
  // Minted from the bytes this compile just read, so what the browser
653
634
  // renders and what it can later claim it rendered are the same read.
654
635
  sourceDigests: Object.fromEntries(sources.map(({ path, source }) => [path, digestOf(source)])),
@@ -1441,7 +1422,7 @@ export const startVisualServer = async (options) => {
1441
1422
  });
1442
1423
  return;
1443
1424
  }
1444
- const path = `.yarramate/visual-layout/${projectionId}.yaml`;
1425
+ const path = layoutSidecarPath(projectionId);
1445
1426
  mkdirSync(layoutDir, { recursive: true });
1446
1427
  writeFileSync(resolve(options.cwd, path), emitYaml({
1447
1428
  format: "yarramate/visual-layout/v1",
@@ -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 {};