yarramate 0.23.0 → 1.0.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 +13 -4
- package/catalogues/core-enrichment.yaml +79 -55
- package/dist/adapter-mapping.js +6 -1
- package/dist/adapters/likec4-cli.js +36 -7
- package/dist/adapters/visual/client.d.ts +2 -2
- package/dist/adapters/visual/client.js +79 -10
- package/dist/adapters/visual/protocol-contract.d.ts +124 -10
- package/dist/adapters/visual/protocol-contract.js +12 -1
- package/dist/adapters/visual/protocol.d.ts +63 -0
- package/dist/adapters/visual/protocol.js +96 -5
- package/dist/adapters/visual/session-server.d.ts +1 -6
- package/dist/adapters/visual/session-server.js +243 -143
- package/dist/adapters/visual/session-store.d.ts +9 -0
- package/dist/adapters/visual/session-store.js +14 -5
- package/dist/adapters/visual/view-identity.d.ts +135 -0
- package/dist/adapters/visual/view-identity.js +200 -0
- package/dist/adapters/visual/wire.d.ts +19 -4
- package/dist/adapters/visual/workspace-model.d.ts +116 -0
- package/dist/adapters/visual/workspace-model.js +212 -0
- package/dist/adapters/visual-cli.d.ts +1 -1
- package/dist/adapters/visual-cli.js +30 -21
- package/dist/apply-cli.d.ts +2 -0
- package/dist/apply-cli.js +83 -0
- package/dist/apply-command.d.ts +84 -3
- package/dist/apply-command.js +218 -109
- package/dist/archimate-relationships.generated.d.ts +22 -0
- package/dist/archimate-relationships.generated.js +156 -0
- package/dist/ask-command.js +18 -6
- package/dist/attestation-staleness.js +2 -2
- package/dist/changed.js +1 -1
- package/dist/check-command.js +13 -5
- package/dist/cli-support.d.ts +1 -1
- package/dist/cli-support.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/compiler.d.ts +31 -3
- package/dist/compiler.js +321 -81
- package/dist/concept-drafting.d.ts +27 -0
- package/dist/concept-drafting.js +75 -0
- package/dist/core-contract.js +6 -1
- package/dist/deletion-drafting.d.ts +52 -0
- package/dist/deletion-drafting.js +100 -0
- package/dist/evidence.js +6 -1
- package/dist/graph-projection.d.ts +14 -0
- package/dist/graph-projection.js +4 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +8 -1
- package/dist/interrogate-command.js +63 -17
- package/dist/kind-label.d.ts +4 -3
- package/dist/kind-label.js +4 -3
- package/dist/nesting.d.ts +15 -0
- package/dist/nesting.js +2 -0
- package/dist/notation/archimate.d.ts +1 -1
- package/dist/notation/archimate.js +108 -9
- package/dist/profile.d.ts +9 -12
- package/dist/profile.js +17 -26
- package/dist/projection.d.ts +62 -3
- package/dist/projection.js +127 -43
- package/dist/relationship-drafting.d.ts +51 -0
- package/dist/relationship-drafting.js +92 -0
- package/dist/relationship-matrix.d.ts +14 -0
- package/dist/relationship-matrix.js +74 -0
- package/dist/shipped-profile.d.ts +1 -1
- package/dist/shipped-profile.js +19 -5
- package/dist/source-store.d.ts +95 -0
- package/dist/source-store.js +170 -0
- package/dist/subject-references.js +1 -1
- package/dist/visual-app/assets/index-BLcpI8WN.js +394 -0
- package/dist/visual-app/assets/index-DLutIWES.css +1 -0
- package/dist/visual-app/index.html +2 -2
- package/dist/visual-app-lib/editor.d.ts +1 -0
- package/dist/visual-app-lib/editor.js +90968 -0
- package/dist/visual-app-lib/styles.css +2 -0
- package/dist/visual-app-lib/types/adapter-mapping.d.ts +40 -0
- package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +455 -0
- package/dist/visual-app-lib/types/adapters/visual/view-identity.d.ts +135 -0
- package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +141 -0
- package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +116 -0
- package/dist/visual-app-lib/types/apply-command.d.ts +93 -0
- package/dist/visual-app-lib/types/archimate-relationships.generated.d.ts +22 -0
- package/dist/visual-app-lib/types/brief.d.ts +5 -0
- package/dist/visual-app-lib/types/compiler.d.ts +155 -0
- package/dist/visual-app-lib/types/concept-drafting.d.ts +27 -0
- package/dist/visual-app-lib/types/deletion-drafting.d.ts +52 -0
- package/dist/visual-app-lib/types/evidence.d.ts +62 -0
- package/dist/visual-app-lib/types/graph-claims.d.ts +18 -0
- package/dist/visual-app-lib/types/graph-projection.d.ts +77 -0
- package/dist/visual-app-lib/types/kind-label.d.ts +9 -0
- package/dist/visual-app-lib/types/nesting.d.ts +15 -0
- package/dist/visual-app-lib/types/notation/archimate.d.ts +71 -0
- package/dist/visual-app-lib/types/operations.d.ts +157 -0
- package/dist/visual-app-lib/types/profile.d.ts +47 -0
- package/dist/visual-app-lib/types/projection.d.ts +113 -0
- package/dist/visual-app-lib/types/relationship-drafting.d.ts +51 -0
- package/dist/visual-app-lib/types/relationship-matrix.d.ts +14 -0
- package/dist/visual-app-lib/types/shipped-profile.d.ts +2 -0
- package/dist/visual-app-lib/types/source-document.d.ts +36 -0
- package/dist/visual-app-lib/types/source-store.d.ts +95 -0
- package/dist/visual-app-lib/types/subject-references.d.ts +92 -0
- package/dist/visual-app-lib/types/visual-app/App.d.ts +15 -0
- package/dist/visual-app-lib/types/visual-app/badges.d.ts +7 -0
- package/dist/visual-app-lib/types/visual-app/changeset-tray.d.ts +106 -0
- package/dist/visual-app-lib/types/visual-app/confirm-dialog.d.ts +14 -0
- package/dist/visual-app-lib/types/visual-app/connection-panel.d.ts +23 -0
- package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +187 -0
- package/dist/visual-app-lib/types/visual-app/context-menu.d.ts +24 -0
- package/dist/visual-app-lib/types/visual-app/describe-query.d.ts +9 -0
- package/dist/visual-app-lib/types/visual-app/editor-host.d.ts +70 -0
- package/dist/visual-app-lib/types/visual-app/faults.d.ts +26 -0
- package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +73 -0
- package/dist/visual-app-lib/types/visual-app/kind-icons.d.ts +3 -0
- package/dist/visual-app-lib/types/visual-app/local-host.d.ts +38 -0
- package/dist/visual-app-lib/types/visual-app/mount.d.ts +56 -0
- package/dist/visual-app-lib/types/visual-app/prompt-dialog.d.ts +24 -0
- package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +69 -0
- package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +142 -0
- package/dist/visual-app-lib/types/visual-app/quick-filter.d.ts +11 -0
- package/dist/visual-app-lib/types/visual-app/relationship-kind-options.d.ts +50 -0
- package/dist/visual-app-lib/types/visual-app/save-view.d.ts +86 -0
- package/dist/visual-app-lib/types/visual-app/section-stack.d.ts +59 -0
- package/dist/visual-app-lib/types/visual-app/session-client.d.ts +38 -0
- package/dist/visual-app-lib/types/visual-app/state.d.ts +328 -0
- package/dist/visual-app-lib/types/visual-app/subject-draft-panel.d.ts +22 -0
- package/dist/visual-app-lib/types/visual-app/subject-form.d.ts +42 -0
- package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +122 -0
- package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +32 -0
- package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +268 -0
- package/dist/visual-app-lib/types/workspace.d.ts +29 -0
- package/dist/workspace.js +7 -1
- package/docs/CONSUMING-YARRAMATE.md +51 -2
- package/package.json +9 -2
- package/schema/yarramate-adapter-mapping.schema.json +1 -1
- package/schema/yarramate-ask-result.schema.json +24 -3
- package/schema/yarramate-check-result.schema.json +9 -0
- package/schema/yarramate-diagnostic-result.schema.json +9 -0
- package/schema/yarramate-document.schema.json +6 -0
- package/schema/yarramate-evidence-report.schema.json +1 -1
- package/schema/yarramate-evidence.schema.json +1 -1
- package/schema/yarramate-graph-v2.schema.json +34 -9
- package/schema/yarramate-likec4-generated-project-v2.schema.json +1 -1
- package/schema/yarramate-likec4-generated-project.schema.json +1 -1
- package/schema/yarramate-likec4-project.schema.json +1 -1
- package/schema/yarramate-projection-result.schema.json +3 -7
- package/schema/yarramate-projection.schema.json +16 -16
- package/schema/yarramate-reconciliation-report.schema.json +2 -2
- package/schema/yarramate-rtm.schema.json +1 -1
- package/schema/yarramate-state-comparison.schema.json +1 -1
- package/schema/yarramate-visual-diagnostic-result.schema.json +9 -0
- package/schema/yarramate-visual-event.schema.json +40 -41
- package/schema/yarramate-visual-graph.schema.json +8 -0
- package/schema/yarramate-visual-handoff.schema.json +3 -3
- package/schema/yarramate-visual-session-descriptor.schema.json +6 -6
- package/schema/yarramate-visual-session-started.schema.json +10 -9
- package/schema/yarramate-visual-status.schema.json +3 -3
- package/skills/yarramate-architecture/SKILL.md +3 -3
- package/skills/yarramate-architecture/references/modelling-patterns.md +23 -44
- package/skills/yarramate-architecture/references/native-authoring.md +32 -49
- package/skills/yarramate-architecture/references/visual-conversations.md +13 -10
- package/dist/visual-app/assets/index-CRd1Khma.js +0 -354
- package/dist/visual-app/assets/index-tZSHlU-a.css +0 -1
|
@@ -2,18 +2,19 @@ import { createHash, createHmac, randomBytes as randomSource, timingSafeEqual, }
|
|
|
2
2
|
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { lstat, readFile } from "node:fs/promises";
|
|
4
4
|
import { createServer, } from "node:http";
|
|
5
|
-
import { basename, extname, join, resolve, sep } from "node:path";
|
|
5
|
+
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
9
|
import { parse, stringify } from "yaml";
|
|
10
|
-
import { VISUAL_LIMITS, VISUAL_PROTOCOL_VERSION, digestOf, parseVisualBrowserInput, parseVisualResponse, parseVisualSessionStarted, parseVisualStatus, visualBrowserInputType, } from "./protocol.js";
|
|
10
|
+
import { VISUAL_LIMITS, VISUAL_PROTOCOL_VERSION, digestOf, parseVisualBrowserInput, parseVisualResponse, parseVisualSessionStarted, parseVisualStatus, toWireFileUri, visualBrowserInputType, } from "./protocol.js";
|
|
11
11
|
import { appendTerminalEvent, appendVisualEvent, appendVisualResponse, createVisualSession, isActionableVisualEvent, recoverVisualSession, removeVisualSession, writeVisualSessionDescriptor, } from "./session-store.js";
|
|
12
|
-
import { loadProjection,
|
|
12
|
+
import { loadProjection, } from "../../projection.js";
|
|
13
|
+
export { VISUAL_SERVER_DOCUMENT } from "./workspace-model.js";
|
|
14
|
+
import { adoptLandedViews, exclusionsOf, matchedIdsOf, planViewWrites, published, renderedWorkspaceOf, serverDiagnostic, viewSummaryOf, } from "./workspace-model.js";
|
|
13
15
|
import { compileWorkspaceWithProfileContext, } from "../../compiler.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { kindLabelOf } from "../../kind-label.js";
|
|
16
|
+
import { planOperations, posixDirectoryOf, writeConflictDiagnostics, } from "../../apply-command.js";
|
|
17
|
+
import { createFileSystemStore } from "../../source-store.js";
|
|
17
18
|
import { loadWorkspaceManifest, } from "../../workspace.js";
|
|
18
19
|
import visualLayoutSchema from "../../../schema/yarramate-visual-layout.schema.json" with { type: "json" };
|
|
19
20
|
import visualProjectionSchema from "../../../schema/yarramate-projection.schema.json" with { type: "json" };
|
|
@@ -26,12 +27,6 @@ const Ajv2020 = Ajv2020Module.default;
|
|
|
26
27
|
const layoutAjv = new Ajv2020({ allErrors: true });
|
|
27
28
|
layoutAjv.addSchema(visualProjectionSchema);
|
|
28
29
|
const validateVisualLayout = layoutAjv.compile(visualLayoutSchema);
|
|
29
|
-
/**
|
|
30
|
-
* Document name reported by diagnostics the server itself raises — a refused
|
|
31
|
-
* frame, a frozen queue, or a transport violation that no protocol document
|
|
32
|
-
* owns.
|
|
33
|
-
*/
|
|
34
|
-
export const VISUAL_SERVER_DOCUMENT = "visual-session-server";
|
|
35
30
|
/**
|
|
36
31
|
* Returned on every response. The policy admits no external origin at all, so
|
|
37
32
|
* neither a compromised model nor a hostile chat message can reach the network,
|
|
@@ -152,15 +147,17 @@ const serverError = (code, message) => new Error(`${code}: ${message}`);
|
|
|
152
147
|
* `/`, because these diagnostics are published in `visual-diagnostic-result/v1`
|
|
153
148
|
* documents and read back by the one-shot agent clients.
|
|
154
149
|
*/
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Names the subjects a Core diagnostic is about, so the canvas can mark the
|
|
152
|
+
* element a rule refused rather than reporting a failure with nothing on
|
|
153
|
+
* screen changed.
|
|
154
|
+
*
|
|
155
|
+
* Core derives this at the boundary that publishes a result rather than inside
|
|
156
|
+
* `compileWorkspace`, which keeps compiler diagnostics a pure function of the
|
|
157
|
+
* model. `check` has always done it. This adapter is the consumer the
|
|
158
|
+
* derivation was written for and never did, so every diagnostic it sent to a
|
|
159
|
+
* browser arrived anchored to a byte offset the browser cannot use.
|
|
160
|
+
*/
|
|
164
161
|
/**
|
|
165
162
|
* Compares two capabilities without leaking where they differ, or how long the
|
|
166
163
|
* presented one is: both sides are reduced to a fixed-width digest first, so
|
|
@@ -248,8 +245,6 @@ const eventFrom = (input, envelope) => {
|
|
|
248
245
|
return { ...envelope, type: input.type, payload: input.payload };
|
|
249
246
|
case "filter.query":
|
|
250
247
|
return { ...envelope, type: input.type, payload: input.payload };
|
|
251
|
-
case "view.save":
|
|
252
|
-
return { ...envelope, type: input.type, payload: input.payload };
|
|
253
248
|
case "changeset.commit":
|
|
254
249
|
return { ...envelope, type: input.type, payload: input.payload };
|
|
255
250
|
case "layout.save":
|
|
@@ -257,27 +252,70 @@ const eventFrom = (input, envelope) => {
|
|
|
257
252
|
}
|
|
258
253
|
};
|
|
259
254
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
255
|
+
* What a batch that changes no subject reports.
|
|
256
|
+
*
|
|
257
|
+
* A commit carrying only view operations never reaches Core: an operations
|
|
258
|
+
* document with an empty list is refused by the operations schema (`YM201`),
|
|
259
|
+
* and rightly - "apply nothing" is not a batch. The receipt still has to name
|
|
260
|
+
* the documents that changed, which here are the projections.
|
|
263
261
|
*/
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
262
|
+
const noModelChange = (workspace, documents) => ({
|
|
263
|
+
format: "yarramate/apply-result/v1",
|
|
264
|
+
workspace,
|
|
265
|
+
applied: {
|
|
266
|
+
addedConcepts: 0,
|
|
267
|
+
addedRelationships: 0,
|
|
268
|
+
updatedConcepts: 0,
|
|
269
|
+
updatedRelationships: 0,
|
|
270
|
+
deletedConcepts: 0,
|
|
271
|
+
deletedRelationships: 0,
|
|
272
|
+
renamedConcepts: 0,
|
|
273
|
+
renamedRelationships: 0,
|
|
274
|
+
addedObservations: 0,
|
|
275
|
+
updatedObservations: 0,
|
|
276
|
+
deletedObservations: 0,
|
|
277
|
+
},
|
|
278
|
+
documents,
|
|
279
|
+
});
|
|
280
|
+
/**
|
|
281
|
+
* Lands a planned batch plus the view writes beside it, as one `writeAll`.
|
|
282
|
+
*
|
|
283
|
+
* `landOperations` would write the model's half on its own, which is the one
|
|
284
|
+
* thing this must not do: two calls are two batches, and a view removed in one
|
|
285
|
+
* and a subject edited in the other can half land.
|
|
286
|
+
*/
|
|
287
|
+
const landPlanned = (store, planned, viewWrites, operationsPath, workspaceId) => {
|
|
288
|
+
if (planned !== null && !planned.ok)
|
|
289
|
+
return planned.outcome;
|
|
290
|
+
const writes = [...(planned?.writes ?? []), ...viewWrites];
|
|
291
|
+
const viewPaths = viewWrites.map((write) => write.path);
|
|
292
|
+
const outcome = planned === null
|
|
293
|
+
? {
|
|
294
|
+
ok: true,
|
|
295
|
+
sources: [],
|
|
296
|
+
result: noModelChange(workspaceId, viewPaths),
|
|
297
|
+
}
|
|
298
|
+
: {
|
|
299
|
+
...planned.outcome,
|
|
300
|
+
// Core names the documents IT rewrote, which is the model's half.
|
|
301
|
+
// The projections went out in the same `writeAll`, so a receipt that
|
|
302
|
+
// named only Core's half would report one file where two landed -
|
|
303
|
+
// and a mixed batch is the ordinary case now that creating a subject
|
|
304
|
+
// also puts it in the view that created it (#255).
|
|
305
|
+
result: {
|
|
306
|
+
...planned.outcome.result,
|
|
307
|
+
documents: [...planned.outcome.result.documents, ...viewPaths],
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
if (writes.length === 0)
|
|
311
|
+
return outcome;
|
|
312
|
+
const written = store.writeAll(writes);
|
|
313
|
+
if (written.ok)
|
|
314
|
+
return outcome;
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
diagnostics: writeConflictDiagnostics(written.conflicts, operationsPath),
|
|
318
|
+
};
|
|
281
319
|
};
|
|
282
320
|
/**
|
|
283
321
|
* Serves one authenticated visual session over loopback. The handle owns the
|
|
@@ -344,10 +382,16 @@ export const startVisualServer = async (options) => {
|
|
|
344
382
|
}
|
|
345
383
|
return abandon(serverError("YMVS132", `Session requires a resolvable workspace manifest at ${manifestPath}`));
|
|
346
384
|
})();
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
385
|
+
/**
|
|
386
|
+
* One view, read off disk. `undefined` for a projection that is unreadable
|
|
387
|
+
* or that the schema refuses — a broken saved view skips rather than failing
|
|
388
|
+
* the session, the same way a broken layout sidecar does.
|
|
389
|
+
*
|
|
390
|
+
* `subjectCount` is left at zero here and refreshed by
|
|
391
|
+
* `renderedWorkspaceOf`: counting needs the compiled graph, which does not
|
|
392
|
+
* exist yet when the session builds its first list.
|
|
393
|
+
*/
|
|
394
|
+
const readViewSummary = (projectionPath) => {
|
|
351
395
|
try {
|
|
352
396
|
const projectionSource = readFileSync(resolve(options.cwd, projectionPath), "utf8");
|
|
353
397
|
const loaded = loadProjection({
|
|
@@ -355,22 +399,19 @@ export const startVisualServer = async (options) => {
|
|
|
355
399
|
source: projectionSource,
|
|
356
400
|
});
|
|
357
401
|
if (!loaded.ok)
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
return [
|
|
361
|
-
{
|
|
362
|
-
id: projection.id,
|
|
363
|
-
title: projection.presentation?.title ?? projection.id,
|
|
364
|
-
description: projection.presentation?.description ?? "",
|
|
365
|
-
query: projection.query,
|
|
366
|
-
presentation: projection.presentation,
|
|
367
|
-
},
|
|
368
|
-
];
|
|
402
|
+
return undefined;
|
|
403
|
+
return viewSummaryOf(loaded.projection, projectionPath);
|
|
369
404
|
}
|
|
370
405
|
catch {
|
|
371
|
-
|
|
372
|
-
return [];
|
|
406
|
+
return undefined;
|
|
373
407
|
}
|
|
408
|
+
};
|
|
409
|
+
// Saved views join this list as a commit lands them (see `adoptLandedViews`),
|
|
410
|
+
// because a reconnecting browser is handed it in its snapshot and
|
|
411
|
+
// `layout.save` checks projection ids against it.
|
|
412
|
+
const views = resolvedWorkspace.projections.flatMap((projectionPath) => {
|
|
413
|
+
const summary = readViewSummary(projectionPath);
|
|
414
|
+
return summary === undefined ? [] : [summary];
|
|
374
415
|
});
|
|
375
416
|
// Drag positions are adapter-owned presentation state (ADR 0023): never
|
|
376
417
|
// validated by Core. An invalid or unreadable sidecar is skipped exactly
|
|
@@ -416,6 +457,10 @@ export const startVisualServer = async (options) => {
|
|
|
416
457
|
vocabulary: { conceptKinds: [], relationshipKinds: [] },
|
|
417
458
|
layouts,
|
|
418
459
|
sourceDigests: request.initialModel.sourceDigests,
|
|
460
|
+
// The request's model has no projections in it - `visual-model/v1` carries
|
|
461
|
+
// a graph, not a workspace - so the fallback states nothing rather than
|
|
462
|
+
// guessing. `recompileWorkspace` below fills it from disk immediately.
|
|
463
|
+
projectionDigests: {},
|
|
419
464
|
};
|
|
420
465
|
const capabilities = {
|
|
421
466
|
chat: request.chatEnabled,
|
|
@@ -440,15 +485,49 @@ export const startVisualServer = async (options) => {
|
|
|
440
485
|
* Returns whether it compiled, so a post-write failure can freeze the
|
|
441
486
|
* session instead of serving a stale graph.
|
|
442
487
|
*/
|
|
443
|
-
const
|
|
488
|
+
const workspaceSources = () => {
|
|
444
489
|
try {
|
|
445
|
-
|
|
490
|
+
return [
|
|
446
491
|
...resolvedWorkspace.profiles,
|
|
447
492
|
...resolvedWorkspace.documents,
|
|
448
493
|
].map((path) => ({
|
|
449
494
|
path,
|
|
450
495
|
source: readFileSync(resolve(options.cwd, path), "utf8"),
|
|
451
496
|
}));
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// A source that cannot be read is one whose diagnostics belong to no
|
|
500
|
+
// subject anyway, so an empty list is the right answer rather than a
|
|
501
|
+
// throw from a path that is only trying to add detail.
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
/**
|
|
506
|
+
* The digest of every projection the session knows about, read now.
|
|
507
|
+
*
|
|
508
|
+
* Read from `views` rather than from `resolvedWorkspace.projections`,
|
|
509
|
+
* because a view created mid-session joins the former and never the latter:
|
|
510
|
+
* the manifest is resolved once at startup and never again. One that has
|
|
511
|
+
* been removed is simply absent, which is the same thing the map says about
|
|
512
|
+
* one that never existed — and the browser reads both as "no pin", which is
|
|
513
|
+
* correct for a create and refused by the store for a delete.
|
|
514
|
+
*/
|
|
515
|
+
const projectionDigestsNow = () => {
|
|
516
|
+
const digests = {};
|
|
517
|
+
for (const view of views) {
|
|
518
|
+
try {
|
|
519
|
+
digests[view.path] = digestOf(readFileSync(resolve(options.cwd, view.path), "utf8"));
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
// Gone from disk: nothing to pin against, and the commit's own
|
|
523
|
+
// conflict check is what reports it rather than a guess made here.
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return digests;
|
|
527
|
+
};
|
|
528
|
+
const recompileWorkspace = () => {
|
|
529
|
+
try {
|
|
530
|
+
const sources = workspaceSources();
|
|
452
531
|
const compiled = compileWorkspaceWithProfileContext(sources);
|
|
453
532
|
if (!compiled.ok) {
|
|
454
533
|
compiledWorkspace = undefined;
|
|
@@ -458,23 +537,24 @@ export const startVisualServer = async (options) => {
|
|
|
458
537
|
graph: compiled.graph,
|
|
459
538
|
profileContext: compiled.profileContext,
|
|
460
539
|
};
|
|
461
|
-
const
|
|
462
|
-
...compiled.profileContext.conceptKindLineages.keys(),
|
|
463
|
-
].map((id) => ({ id, label: kindLabelOf(id) }));
|
|
464
|
-
const relationshipKinds = [
|
|
465
|
-
...compiled.profileContext.relationshipKindLineages.keys(),
|
|
466
|
-
].map((id) => ({ id, label: kindLabelOf(id) }));
|
|
467
|
-
rendered = {
|
|
540
|
+
const workspaceModel = renderedWorkspaceOf(compiledWorkspace, views, {
|
|
468
541
|
authority: rendered.authority,
|
|
469
542
|
initialView: rendered.initialView,
|
|
470
|
-
graph: projectGraphForCanvas(compiled.graph, compiled.profileContext),
|
|
471
543
|
documents: resolvedWorkspace.documents,
|
|
472
|
-
vocabulary: { conceptKinds, relationshipKinds },
|
|
473
544
|
layouts: rendered.layouts,
|
|
474
545
|
// Minted from the bytes this compile just read, so what the browser
|
|
475
546
|
// renders and what it can later claim it rendered are the same read.
|
|
476
547
|
sourceDigests: Object.fromEntries(sources.map(({ path, source }) => [path, digestOf(source)])),
|
|
477
|
-
|
|
548
|
+
// Kept apart from `sourceDigests`, which means what this graph was
|
|
549
|
+
// compiled from and is what `YMVS112` checks — a projection is not
|
|
550
|
+
// that. A staged view operation pins against these (ADR 0103), and a
|
|
551
|
+
// projection missing from the map is one the commit will create.
|
|
552
|
+
projectionDigests: projectionDigestsNow(),
|
|
553
|
+
});
|
|
554
|
+
// Closures below retain this array, so refresh its contents without
|
|
555
|
+
// replacing the identity the session started with.
|
|
556
|
+
views.splice(0, views.length, ...workspaceModel.views);
|
|
557
|
+
rendered = workspaceModel.model;
|
|
478
558
|
return true;
|
|
479
559
|
}
|
|
480
560
|
catch {
|
|
@@ -483,12 +563,23 @@ export const startVisualServer = async (options) => {
|
|
|
483
563
|
}
|
|
484
564
|
};
|
|
485
565
|
recompileWorkspace();
|
|
486
|
-
const filterMatchedIds = (query) =>
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
566
|
+
const filterMatchedIds = (query) => compiledWorkspace === undefined
|
|
567
|
+
? []
|
|
568
|
+
: matchedIdsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext);
|
|
569
|
+
/**
|
|
570
|
+
* Why a query dropped what it dropped, as the editor's "excluded, and why"
|
|
571
|
+
* list reads it (#248).
|
|
572
|
+
*
|
|
573
|
+
* Resolved here rather than in the browser for the same reason the match set
|
|
574
|
+
* is: a `ProjectionQuery` needs the semantic graph to answer, and the browser
|
|
575
|
+
* has the rendered model rather than the graph. One evaluator, one answer -
|
|
576
|
+
* `explainProjection` shares `conceptSelector` with `evaluateProjection`, so
|
|
577
|
+
* the reason the editor shows and the set the canvas draws cannot come from
|
|
578
|
+
* two readings of the same query.
|
|
579
|
+
*/
|
|
580
|
+
const filterExclusions = (query) => compiledWorkspace === undefined
|
|
581
|
+
? []
|
|
582
|
+
: exclusionsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext);
|
|
492
583
|
let listening = false;
|
|
493
584
|
let bootstrapSpent = false;
|
|
494
585
|
let agentAttached = false;
|
|
@@ -996,64 +1087,11 @@ export const startVisualServer = async (options) => {
|
|
|
996
1087
|
result: {
|
|
997
1088
|
query: event.payload.query,
|
|
998
1089
|
matchedIds: filterMatchedIds(event.payload.query),
|
|
1090
|
+
excluded: filterExclusions(event.payload.query),
|
|
999
1091
|
},
|
|
1000
1092
|
});
|
|
1001
1093
|
return;
|
|
1002
1094
|
}
|
|
1003
|
-
if (event.type === "view.save") {
|
|
1004
|
-
// Saving a view is a pure filesystem write plus schema validation: it
|
|
1005
|
-
// never asks the agent anything, so it is answered here directly
|
|
1006
|
-
// rather than through the pending queue a poll would drain.
|
|
1007
|
-
const existingIds = new Set(views.map((view) => view.id));
|
|
1008
|
-
const id = event.payload.id ??
|
|
1009
|
-
uniqueViewId(slugify(event.payload.title), existingIds);
|
|
1010
|
-
const presentation = {
|
|
1011
|
-
...(event.payload.presentation ?? {}),
|
|
1012
|
-
title: event.payload.title,
|
|
1013
|
-
description: event.payload.description,
|
|
1014
|
-
};
|
|
1015
|
-
const candidate = {
|
|
1016
|
-
format: "yarramate/projection/v1",
|
|
1017
|
-
id,
|
|
1018
|
-
version: "1.0",
|
|
1019
|
-
query: event.payload.query,
|
|
1020
|
-
presentation,
|
|
1021
|
-
};
|
|
1022
|
-
const path = `.yarramate/projections/${id}.yaml`;
|
|
1023
|
-
const source = stringify(candidate);
|
|
1024
|
-
const loaded = loadProjection({ path, source });
|
|
1025
|
-
if (!loaded.ok) {
|
|
1026
|
-
sendFrame(socket, {
|
|
1027
|
-
kind: "view-save-result",
|
|
1028
|
-
result: { ok: false, diagnostics: loaded.diagnostics },
|
|
1029
|
-
});
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
mkdirSync(resolve(options.cwd, ".yarramate/projections"), {
|
|
1033
|
-
recursive: true,
|
|
1034
|
-
});
|
|
1035
|
-
writeFileSync(resolve(options.cwd, path), source, "utf8");
|
|
1036
|
-
// On disk is not enough: the list handed to a reconnecting browser and
|
|
1037
|
-
// checked by `layout.save` lives here, so an overwrite replaces its
|
|
1038
|
-
// summary in place and a new view joins the end.
|
|
1039
|
-
const summary = {
|
|
1040
|
-
id,
|
|
1041
|
-
title: event.payload.title,
|
|
1042
|
-
description: event.payload.description,
|
|
1043
|
-
query: event.payload.query,
|
|
1044
|
-
presentation,
|
|
1045
|
-
};
|
|
1046
|
-
const saved = views.findIndex((view) => view.id === id);
|
|
1047
|
-
if (saved === -1)
|
|
1048
|
-
views.push(summary);
|
|
1049
|
-
else
|
|
1050
|
-
views[saved] = summary;
|
|
1051
|
-
sendFrame(socket, {
|
|
1052
|
-
kind: "view-save-result",
|
|
1053
|
-
result: { ok: true, id, path },
|
|
1054
|
-
});
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
1095
|
if (event.type === "changeset.commit") {
|
|
1058
1096
|
// Landing a batch is mechanical: Core's `applyOperations` never asks
|
|
1059
1097
|
// the agent anything, so it is answered here directly rather than
|
|
@@ -1073,7 +1111,14 @@ export const startVisualServer = async (options) => {
|
|
|
1073
1111
|
// state is decoration.
|
|
1074
1112
|
const pins = event.payload.sourceDigests;
|
|
1075
1113
|
const refused = [];
|
|
1076
|
-
for (const path of new Set(
|
|
1114
|
+
for (const path of new Set([
|
|
1115
|
+
...event.payload.operations.map((operation) => operation.document),
|
|
1116
|
+
// A staged view is a document this batch touches like any other, so
|
|
1117
|
+
// it answers to the same precondition (ADR 0103). Without this a
|
|
1118
|
+
// projection someone edited on disk would be overwritten by a
|
|
1119
|
+
// reviewer who never saw the edit.
|
|
1120
|
+
...event.payload.viewOperations.map((operation) => operation.path),
|
|
1121
|
+
])) {
|
|
1077
1122
|
let held;
|
|
1078
1123
|
try {
|
|
1079
1124
|
held = digestOf(readFileSync(resolve(options.cwd, path), "utf8"));
|
|
@@ -1106,29 +1151,84 @@ export const startVisualServer = async (options) => {
|
|
|
1106
1151
|
result: { ok: false, diagnostics: refused },
|
|
1107
1152
|
});
|
|
1108
1153
|
if (recompileWorkspace())
|
|
1109
|
-
broadcast({ kind: "model", model: rendered });
|
|
1154
|
+
broadcast({ kind: "model", model: rendered, views });
|
|
1110
1155
|
return;
|
|
1111
1156
|
}
|
|
1112
1157
|
const operationsSource = stringify({
|
|
1113
1158
|
format: "yarramate/operations/v1",
|
|
1114
1159
|
operations: event.payload.operations,
|
|
1115
1160
|
});
|
|
1116
|
-
|
|
1161
|
+
// The pin check above is about the reviewer's view: whether the value
|
|
1162
|
+
// on screen is still the value on disk. What follows is about the
|
|
1163
|
+
// bytes: Core is handed the sources, returns the ones it changed, and
|
|
1164
|
+
// the store writes them only if each still holds what Core was shown
|
|
1165
|
+
// (ADR 0100). That second check is what closes the window the pin
|
|
1166
|
+
// check alone left open, which used to span a whole workspace compile
|
|
1167
|
+
// between the digest that satisfied it and the write that acted on it.
|
|
1168
|
+
const loadedWorkspace = loadWorkspaceManifest({ path: manifestPath, source: readFileSync(manifestPath, "utf8") }, options.cwd);
|
|
1169
|
+
if (!loadedWorkspace.ok) {
|
|
1170
|
+
sendFrame(socket, {
|
|
1171
|
+
kind: "apply-result",
|
|
1172
|
+
result: {
|
|
1173
|
+
ok: false,
|
|
1174
|
+
diagnostics: published(loadedWorkspace.diagnostics, workspaceSources()),
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
const store = createFileSystemStore(options.cwd);
|
|
1180
|
+
// Core is asked nothing when nothing about the model changed: an
|
|
1181
|
+
// operations document with an empty list is refused by the operations
|
|
1182
|
+
// schema, and a batch of views only is a legitimate thing to commit.
|
|
1183
|
+
const planned = event.payload.operations.length === 0
|
|
1184
|
+
? null
|
|
1185
|
+
: planOperations(store, {
|
|
1186
|
+
workspace: loadedWorkspace.workspace,
|
|
1187
|
+
operations: {
|
|
1188
|
+
path: "changeset.yaml",
|
|
1189
|
+
source: operationsSource,
|
|
1190
|
+
},
|
|
1191
|
+
manifestDirectory: posixDirectoryOf(relative(options.cwd, manifestPath)),
|
|
1192
|
+
});
|
|
1193
|
+
// The views' own writes, composed and validated the same way
|
|
1194
|
+
// `view.save` used to compose one, but not yet written: both halves go
|
|
1195
|
+
// to the store in a single batch so a view and the subjects it shows
|
|
1196
|
+
// land together or not at all (ADR 0103).
|
|
1197
|
+
const viewWrites = planViewWrites(event.payload.viewOperations, loadedWorkspace.workspace, store);
|
|
1198
|
+
if (!viewWrites.ok) {
|
|
1199
|
+
sendFrame(socket, {
|
|
1200
|
+
kind: "apply-result",
|
|
1201
|
+
result: { ok: false, diagnostics: viewWrites.diagnostics },
|
|
1202
|
+
});
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const outcome = landPlanned(store, planned, viewWrites.writes, "changeset.yaml", loadedWorkspace.workspace.id);
|
|
1117
1206
|
if (!outcome.ok) {
|
|
1118
1207
|
// Nothing landed: forward Core's diagnostics verbatim (ADR 0062)
|
|
1119
1208
|
// rather than recompiling or broadcasting anything.
|
|
1120
1209
|
sendFrame(socket, {
|
|
1121
1210
|
kind: "apply-result",
|
|
1122
|
-
result: {
|
|
1211
|
+
result: {
|
|
1212
|
+
ok: false,
|
|
1213
|
+
diagnostics: published(outcome.diagnostics, workspaceSources()),
|
|
1214
|
+
},
|
|
1123
1215
|
});
|
|
1124
1216
|
return;
|
|
1125
1217
|
}
|
|
1218
|
+
// The views list is maintained by hand, because nothing else will:
|
|
1219
|
+
// `resolvedWorkspace` was resolved once at startup and never again, so
|
|
1220
|
+
// a recompile re-reads the documents it already knew about and learns
|
|
1221
|
+
// nothing about a projection this batch just created or removed. The
|
|
1222
|
+
// rail, `layout.save`'s id check, and a reconnecting browser all read
|
|
1223
|
+
// this list, so leaving it stale is how they come to disagree with the
|
|
1224
|
+
// disk. Preserve the mutable list identity that session closures hold.
|
|
1225
|
+
views.splice(0, views.length, ...adoptLandedViews(views, event.payload.viewOperations, readViewSummary));
|
|
1126
1226
|
sendFrame(socket, {
|
|
1127
1227
|
kind: "apply-result",
|
|
1128
1228
|
result: { ok: true, result: outcome.result },
|
|
1129
1229
|
});
|
|
1130
1230
|
if (recompileWorkspace()) {
|
|
1131
|
-
broadcast({ kind: "model", model: rendered });
|
|
1231
|
+
broadcast({ kind: "model", model: rendered, views });
|
|
1132
1232
|
return;
|
|
1133
1233
|
}
|
|
1134
1234
|
// A post-write compile failure is a bug, not a user error: the batch
|
|
@@ -1781,17 +1881,17 @@ export const startVisualServer = async (options) => {
|
|
|
1781
1881
|
let started;
|
|
1782
1882
|
try {
|
|
1783
1883
|
await writeVisualSessionDescriptor(paths, {
|
|
1784
|
-
format: "yarramate/visual-session-descriptor/
|
|
1884
|
+
format: "yarramate/visual-session-descriptor/v2",
|
|
1785
1885
|
protocolVersion: VISUAL_PROTOCOL_VERSION,
|
|
1786
1886
|
sessionId,
|
|
1787
1887
|
origin,
|
|
1788
1888
|
agentCapability: session.agentToken,
|
|
1789
|
-
sessionRoot: paths.root,
|
|
1790
|
-
journalPath: paths.journal,
|
|
1889
|
+
sessionRoot: toWireFileUri(paths.root),
|
|
1890
|
+
journalPath: toWireFileUri(paths.journal),
|
|
1791
1891
|
createdAt: stamp(),
|
|
1792
1892
|
});
|
|
1793
1893
|
started = {
|
|
1794
|
-
format: "yarramate/visual-session-started/
|
|
1894
|
+
format: "yarramate/visual-session-started/v2",
|
|
1795
1895
|
protocolVersion: VISUAL_PROTOCOL_VERSION,
|
|
1796
1896
|
sessionId,
|
|
1797
1897
|
authority: request.authority,
|
|
@@ -1800,8 +1900,8 @@ export const startVisualServer = async (options) => {
|
|
|
1800
1900
|
browserUrl: `${origin}/bootstrap?key=${session.browserToken}`,
|
|
1801
1901
|
webSocketUrl,
|
|
1802
1902
|
origin,
|
|
1803
|
-
descriptorPath: paths.descriptor,
|
|
1804
|
-
sessionRoot: paths.root,
|
|
1903
|
+
descriptorPath: toWireFileUri(paths.descriptor),
|
|
1904
|
+
sessionRoot: toWireFileUri(paths.root),
|
|
1805
1905
|
capabilities,
|
|
1806
1906
|
startedAt: stamp(),
|
|
1807
1907
|
};
|
|
@@ -61,6 +61,15 @@ export interface VisualAppendRejected {
|
|
|
61
61
|
readonly diagnostics: readonly VisualDiagnostic[];
|
|
62
62
|
}
|
|
63
63
|
export type VisualAppendResult = VisualAppendAccepted | VisualAppendRejected;
|
|
64
|
+
/**
|
|
65
|
+
* The one place `VisualSessionPaths` is constructed. Every field stays a
|
|
66
|
+
* native filesystem path — the form `fs`/`path` APIs, the in-process state
|
|
67
|
+
* maps keyed by `paths.journal`, and directory cleanup all need — because
|
|
68
|
+
* this is consumed as a filesystem path far more often than it is ever
|
|
69
|
+
* serialized. The few sites that write one of these fields into a
|
|
70
|
+
* schema-checked wire document encode it there, with `toWireFileUri`, rather
|
|
71
|
+
* than here.
|
|
72
|
+
*/
|
|
64
73
|
export declare const visualSessionPaths: (root: string) => VisualSessionPaths;
|
|
65
74
|
export declare const createVisualSession: (request: VisualSessionRequest, deps: SessionDependencies) => Promise<VisualSessionCreated>;
|
|
66
75
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
2
|
import { lstat, mkdir, open, readFile, readdir, rename, rm, stat, truncate, writeFile, } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
-
import { VISUAL_LIMITS, parseVisualEvent, parseVisualHandoff, parseVisualResponse, parseVisualSessionDescriptor, parseVisualSessionRequest, } from './protocol.js';
|
|
4
|
+
import { VISUAL_LIMITS, parseVisualEvent, parseVisualHandoff, parseVisualResponse, parseVisualSessionDescriptor, parseVisualSessionRequest, toWireFileUri, } from './protocol.js';
|
|
5
5
|
import { compileWorkspaceWithProfileContext, } from '../../compiler.js';
|
|
6
6
|
import { projectGraphForCanvas } from '../../graph-projection.js';
|
|
7
7
|
export const VISUAL_SESSION_MARKER_FORMAT = 'yarramate/visual-session-marker/v1';
|
|
@@ -105,6 +105,15 @@ const writePrivateJson = async (path, value) => {
|
|
|
105
105
|
await rename(temporary, path);
|
|
106
106
|
await syncDirectory(dirname(path));
|
|
107
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* The one place `VisualSessionPaths` is constructed. Every field stays a
|
|
110
|
+
* native filesystem path — the form `fs`/`path` APIs, the in-process state
|
|
111
|
+
* maps keyed by `paths.journal`, and directory cleanup all need — because
|
|
112
|
+
* this is consumed as a filesystem path far more often than it is ever
|
|
113
|
+
* serialized. The few sites that write one of these fields into a
|
|
114
|
+
* schema-checked wire document encode it there, with `toWireFileUri`, rather
|
|
115
|
+
* than here.
|
|
116
|
+
*/
|
|
108
117
|
export const visualSessionPaths = (root) => {
|
|
109
118
|
const base = resolve(root);
|
|
110
119
|
return {
|
|
@@ -298,8 +307,8 @@ export const writeVisualSessionDescriptor = async (paths, descriptor) => {
|
|
|
298
307
|
if (validated.value.sessionId !== marker.id) {
|
|
299
308
|
throw storeError('YMVS126', `Descriptor belongs to session "${validated.value.sessionId}", not "${marker.id}"`);
|
|
300
309
|
}
|
|
301
|
-
if (validated.value.sessionRoot !== paths.root ||
|
|
302
|
-
validated.value.journalPath !== paths.journal) {
|
|
310
|
+
if (validated.value.sessionRoot !== toWireFileUri(paths.root) ||
|
|
311
|
+
validated.value.journalPath !== toWireFileUri(paths.journal)) {
|
|
303
312
|
throw storeError('YMVS125', `Descriptor for session "${marker.id}" names artefacts outside "${paths.root}"`);
|
|
304
313
|
}
|
|
305
314
|
await writePrivateJson(paths.descriptor, validated.value);
|
|
@@ -521,7 +530,7 @@ export const recoverVisualSession = async (paths, includeTranscript = false) =>
|
|
|
521
530
|
: 'cancelled';
|
|
522
531
|
const last = journal.records.at(-1);
|
|
523
532
|
const handoff = {
|
|
524
|
-
format: 'yarramate/visual-handoff/
|
|
533
|
+
format: 'yarramate/visual-handoff/v2',
|
|
525
534
|
sessionId: marker.id,
|
|
526
535
|
authority: marker.authority,
|
|
527
536
|
decision,
|
|
@@ -533,7 +542,7 @@ export const recoverVisualSession = async (paths, includeTranscript = false) =>
|
|
|
533
542
|
requestedChanges: summary?.requestedChanges ?? [],
|
|
534
543
|
unresolvedQuestions: summary?.unresolvedQuestions ?? [],
|
|
535
544
|
finalViews: summary?.finalViews ?? visited.slice(-256),
|
|
536
|
-
transcriptPath: paths.journal,
|
|
545
|
+
transcriptPath: toWireFileUri(paths.journal),
|
|
537
546
|
completedAt: last?.timestamp ?? marker.createdAt,
|
|
538
547
|
// Present and undefined rather than absent, so a caller cannot mistake the
|
|
539
548
|
// summary-only handoff for one whose transcript it forgot to read.
|