yarramate 0.18.0 → 0.20.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 +9 -1
- package/catalogues/core-enrichment.yaml +52 -1
- package/dist/adapters/graphify-cli.js +0 -0
- package/dist/adapters/likec4-cli.js +0 -0
- package/dist/adapters/likec4-export.d.ts +1 -1
- package/dist/adapters/likec4-export.js +32 -0
- package/dist/adapters/mcp-cli.js +0 -0
- package/dist/adapters/visual/client.js +0 -1
- package/dist/adapters/visual/protocol-contract.d.ts +118 -35
- package/dist/adapters/visual/protocol-contract.js +1 -11
- package/dist/adapters/visual/protocol.d.ts +8 -1
- package/dist/adapters/visual/protocol.js +86 -46
- package/dist/adapters/visual/request.d.ts +37 -0
- package/dist/adapters/visual/request.js +131 -0
- package/dist/adapters/visual/session-server.d.ts +12 -14
- package/dist/adapters/visual/session-server.js +641 -283
- package/dist/adapters/visual/session-store.d.ts +31 -29
- package/dist/adapters/visual/session-store.js +94 -94
- package/dist/adapters/visual/wire.d.ts +36 -10
- package/dist/adapters/visual-cli.d.ts +3 -0
- package/dist/adapters/visual-cli.js +66 -1
- package/dist/apply-command.d.ts +10 -0
- package/dist/apply-command.js +399 -247
- package/dist/cli.js +0 -0
- package/dist/compiler.d.ts +66 -0
- package/dist/compiler.js +209 -89
- package/dist/graph-projection.d.ts +62 -0
- package/dist/graph-projection.js +222 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/interrogate-command.d.ts +2 -0
- package/dist/interrogate-command.js +22 -0
- package/dist/kind-label.d.ts +8 -0
- package/dist/kind-label.js +8 -0
- package/dist/operations.d.ts +139 -0
- package/dist/operations.js +11 -0
- package/dist/projection.d.ts +2 -0
- package/dist/projection.js +8 -2
- package/dist/reconciliation.js +8 -7
- package/dist/source-document.d.ts +10 -0
- package/dist/source-document.js +21 -1
- package/dist/visual-app/assets/index-6oSR9RGM.js +354 -0
- package/dist/visual-app/assets/index-dwA9AOr5.css +1 -0
- package/dist/visual-app/index.html +2 -2
- package/docs/CONSUMING-YARRAMATE.md +25 -19
- package/package.json +9 -4
- package/schema/yarramate-adapter-mapping.schema.json +1 -1
- package/schema/yarramate-apply-result.schema.json +8 -2
- package/schema/yarramate-document.schema.json +1 -1
- package/schema/yarramate-likec4-diagnostic-result.schema.json +1 -1
- package/schema/yarramate-operations.schema.json +203 -1
- package/schema/yarramate-projection.schema.json +12 -3
- package/schema/yarramate-question-catalogue.schema.json +15 -2
- package/schema/yarramate-visual-event.schema.json +143 -0
- package/schema/yarramate-visual-graph.schema.json +272 -0
- package/schema/yarramate-visual-layout.schema.json +41 -0
- package/schema/yarramate-visual-model.schema.json +6 -44
- package/schema/yarramate-visual-response.schema.json +170 -19
- package/schema/yarramate-visual-session-descriptor.schema.json +1 -1
- package/schema/yarramate-visual-session-request.schema.json +0 -70
- package/schema/yarramate-visual-session-started.schema.json +2 -9
- package/schema/yarramate-visual-status.schema.json +3 -2
- package/skills/yarramate-architecture/SKILL.md +20 -13
- package/skills/yarramate-architecture/references/journey-checklists.md +4 -2
- package/skills/yarramate-architecture/references/native-authoring.md +30 -0
- package/skills/yarramate-architecture/references/visual-conversations.md +149 -150
- package/dist/adapters/visual/likec4-compiler.d.ts +0 -55
- package/dist/adapters/visual/likec4-compiler.js +0 -460
- package/dist/visual-app/assets/index-CGvC8e3R.css +0 -1
- package/dist/visual-app/assets/index-CXuW1tOj.js +0 -209
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import Ajv2020Module from 'ajv/dist/2020.js';
|
|
3
|
-
import { describeSchemaViolation } from '../../source-document.js';
|
|
3
|
+
import { describeSchemaViolation, readableSchemaErrors, } from '../../source-document.js';
|
|
4
4
|
import visualDiagnosticResultSchema from '../../../schema/yarramate-visual-diagnostic-result.schema.json' with { type: 'json'
|
|
5
5
|
};
|
|
6
6
|
import visualEventSchema from '../../../schema/yarramate-visual-event.schema.json' with { type: 'json'
|
|
7
7
|
};
|
|
8
|
+
import visualGraphSchema from '../../../schema/yarramate-visual-graph.schema.json' with { type: 'json'
|
|
9
|
+
};
|
|
10
|
+
import applyResultSchema from '../../../schema/yarramate-apply-result.schema.json' with { type: 'json'
|
|
11
|
+
};
|
|
12
|
+
import operationsSchema from '../../../schema/yarramate-operations.schema.json' with { type: 'json'
|
|
13
|
+
};
|
|
14
|
+
import projectionSchema from '../../../schema/yarramate-projection.schema.json' with { type: 'json'
|
|
15
|
+
};
|
|
8
16
|
import visualHandoffSchema from '../../../schema/yarramate-visual-handoff.schema.json' with { type: 'json'
|
|
9
17
|
};
|
|
18
|
+
import visualLayoutSchema from '../../../schema/yarramate-visual-layout.schema.json' with { type: 'json'
|
|
19
|
+
};
|
|
10
20
|
import visualModelSchema from '../../../schema/yarramate-visual-model.schema.json' with { type: 'json'
|
|
11
21
|
};
|
|
12
22
|
import visualResponseSchema from '../../../schema/yarramate-visual-response.schema.json' with { type: 'json'
|
|
@@ -24,17 +34,28 @@ import { VISUAL_LIMITS, } from './protocol-contract.js';
|
|
|
24
34
|
// validates travel with the validators.
|
|
25
35
|
export * from './protocol-contract.js';
|
|
26
36
|
const Ajv2020 = Ajv2020Module.default;
|
|
27
|
-
|
|
37
|
+
// `discriminator` on Core's operation union routes a staged edit to the branch
|
|
38
|
+
// its `op` names, so a browser changeset reports the fault the reviewer made
|
|
39
|
+
// rather than one near-miss per operation kind.
|
|
40
|
+
const ajv = new Ajv2020({ allErrors: true, discriminator: true });
|
|
28
41
|
ajv.addSchema([
|
|
29
42
|
visualDiagnosticResultSchema,
|
|
30
|
-
visualModelSchema,
|
|
31
43
|
visualEventSchema,
|
|
44
|
+
visualGraphSchema,
|
|
45
|
+
visualHandoffSchema,
|
|
46
|
+
visualModelSchema,
|
|
32
47
|
visualResponseSchema,
|
|
48
|
+
visualSessionDescriptorSchema,
|
|
33
49
|
visualSessionRequestSchema,
|
|
34
50
|
visualSessionStartedSchema,
|
|
35
|
-
visualSessionDescriptorSchema,
|
|
36
|
-
visualHandoffSchema,
|
|
37
51
|
visualStatusSchema,
|
|
52
|
+
visualLayoutSchema,
|
|
53
|
+
projectionSchema,
|
|
54
|
+
// The commit path's event and response documents reference Core's operations
|
|
55
|
+
// and apply-result shapes rather than restating them, so Ajv needs both
|
|
56
|
+
// resolvable before it compiles the visual validators.
|
|
57
|
+
operationsSchema,
|
|
58
|
+
applyResultSchema,
|
|
38
59
|
]);
|
|
39
60
|
// Diagnostics report the document they came from rather than a source file,
|
|
40
61
|
// because visual protocol documents arrive as parsed JSON over the wire.
|
|
@@ -50,7 +71,22 @@ const validateVisualModel = visualValidator('visual-model/v1');
|
|
|
50
71
|
const validateVisualSessionRequest = visualValidator('visual-session-request/v1');
|
|
51
72
|
const validateVisualSessionStarted = visualValidator('visual-session-started/v1');
|
|
52
73
|
const validateVisualSessionDescriptor = visualValidator('visual-session-descriptor/v1');
|
|
53
|
-
const
|
|
74
|
+
const BROWSER_INPUT_REFERENCE = 'https://yarramate.org/schema/visual-event/v1#/$defs/browserInput';
|
|
75
|
+
const validateVisualBrowserInput = visualValidator('visual-browser-input/v1', BROWSER_INPUT_REFERENCE);
|
|
76
|
+
/**
|
|
77
|
+
* One validator per input type, keyed by the `type` its branch fixes.
|
|
78
|
+
*
|
|
79
|
+
* The union reports every branch it tried, so a `changeset.commit` that gets
|
|
80
|
+
* one field wrong arrives as that one violation buried under seven other
|
|
81
|
+
* types' missing properties - unreadable, and it is the reviewer who has to
|
|
82
|
+
* read it. An input that names a type the protocol knows is answered against
|
|
83
|
+
* that type alone. The vocabulary is read off the schema rather than restated
|
|
84
|
+
* here, so a new input type cannot be added without one.
|
|
85
|
+
*/
|
|
86
|
+
const browserInputBranches = new Map(visualEventSchema.$defs.browserInput.oneOf.map((branch, index) => [
|
|
87
|
+
branch.properties.type.const,
|
|
88
|
+
visualValidator('visual-browser-input/v1', `${BROWSER_INPUT_REFERENCE}/oneOf/${index}`),
|
|
89
|
+
]));
|
|
54
90
|
const validateVisualEvent = visualValidator('visual-event/v1');
|
|
55
91
|
const validateVisualResponse = visualValidator('visual-response/v1');
|
|
56
92
|
const validateVisualHandoff = visualValidator('visual-handoff/v1');
|
|
@@ -83,13 +119,12 @@ const jsonBytes = (value) => {
|
|
|
83
119
|
return 0;
|
|
84
120
|
return Buffer.byteLength(encoded, 'utf8');
|
|
85
121
|
};
|
|
86
|
-
const MODEL_ROOT_CONFIG = 'likec4.config.json';
|
|
87
|
-
const MODEL_SOURCE_EXTENSIONS = ['.c4', '.likec4'];
|
|
88
122
|
/**
|
|
89
123
|
* Relative POSIX path confined to the candidate root. Absolute paths, parent
|
|
90
|
-
* traversal, empty segments,
|
|
91
|
-
*
|
|
92
|
-
*
|
|
124
|
+
* traversal, empty segments, `.`/`..` segments, Windows separators, and NUL
|
|
125
|
+
* bytes are all rejected. A dot-prefixed directory is not an escape and is
|
|
126
|
+
* kept: every workspace keeps its documents under `.yarramate/`, so the
|
|
127
|
+
* digests of a canonical model are unrepresentable without it.
|
|
93
128
|
*/
|
|
94
129
|
const isConfinedRelativePath = (candidate) => {
|
|
95
130
|
if (candidate.length === 0 || candidate.length > 1024)
|
|
@@ -102,35 +137,11 @@ const isConfinedRelativePath = (candidate) => {
|
|
|
102
137
|
return false;
|
|
103
138
|
return candidate
|
|
104
139
|
.split('/')
|
|
105
|
-
.every((segment) => segment.length > 0 &&
|
|
106
|
-
};
|
|
107
|
-
export const isSafeVisualModelPath = (candidate) => {
|
|
108
|
-
if (!isConfinedRelativePath(candidate))
|
|
109
|
-
return false;
|
|
110
|
-
if (candidate === MODEL_ROOT_CONFIG)
|
|
111
|
-
return true;
|
|
112
|
-
const extension = posix.extname(candidate);
|
|
113
|
-
return MODEL_SOURCE_EXTENSIONS.some((allowed) => allowed === extension);
|
|
140
|
+
.every((segment) => segment.length > 0 && segment !== '.' && segment !== '..');
|
|
114
141
|
};
|
|
115
142
|
const modelSemantics = (input, path, prefix) => {
|
|
116
143
|
const fields = documentFields(input);
|
|
117
144
|
const diagnostics = [];
|
|
118
|
-
const files = documentFields(fields.files);
|
|
119
|
-
const fileKeys = Object.keys(files);
|
|
120
|
-
if (fileKeys.length > 0) {
|
|
121
|
-
let sources = 0;
|
|
122
|
-
for (const key of fileKeys) {
|
|
123
|
-
if (!isSafeVisualModelPath(key)) {
|
|
124
|
-
diagnostics.push(visualDiagnostic('YMVS113', `Model file "${key}" escapes the candidate root or is not a LikeC4 source`, path, `${prefix}/files/${pointerSegment(key)}`));
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (key !== MODEL_ROOT_CONFIG)
|
|
128
|
-
sources += 1;
|
|
129
|
-
}
|
|
130
|
-
if (sources === 0) {
|
|
131
|
-
diagnostics.push(visualDiagnostic('YMVS114', 'A visual model requires at least one .c4 or .likec4 source file', path, `${prefix}/files`));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
145
|
const digestKeys = Object.keys(documentFields(fields.sourceDigests));
|
|
135
146
|
for (const key of digestKeys) {
|
|
136
147
|
if (isConfinedRelativePath(key))
|
|
@@ -140,9 +151,6 @@ const modelSemantics = (input, path, prefix) => {
|
|
|
140
151
|
if (fields.authority === 'canonical' && digestKeys.length === 0) {
|
|
141
152
|
diagnostics.push(visualDiagnostic('YMVS112', 'A canonical visual model must record the source digests it was derived from', path, `${prefix}/sourceDigests`));
|
|
142
153
|
}
|
|
143
|
-
if (fields.authority === 'ad-hoc' && digestKeys.length > 0) {
|
|
144
|
-
diagnostics.push(visualDiagnostic('YMVS112', 'An ad-hoc visual model must not claim canonical source digests', path, `${prefix}/sourceDigests`));
|
|
145
|
-
}
|
|
146
154
|
if (jsonBytes(input) > VISUAL_LIMITS.modelBytes) {
|
|
147
155
|
diagnostics.push(visualDiagnostic('YMVS115', `A visual model must not exceed ${VISUAL_LIMITS.modelBytes} bytes`, path, prefix === '' ? '/' : prefix));
|
|
148
156
|
}
|
|
@@ -182,9 +190,6 @@ const responseSemantics = (input, path) => {
|
|
|
182
190
|
if (fields.type === 'chat.response') {
|
|
183
191
|
return chatTextSemantics(fields.payload, path, '/payload');
|
|
184
192
|
}
|
|
185
|
-
if (fields.type === 'model.replace') {
|
|
186
|
-
return modelSemantics(documentFields(fields.payload).model, path, '/payload/model');
|
|
187
|
-
}
|
|
188
193
|
return [];
|
|
189
194
|
};
|
|
190
195
|
/**
|
|
@@ -233,15 +238,30 @@ const schemaDiagnostics = (errors, code, path) => errors.map((error) => {
|
|
|
233
238
|
? `Property "${property}" is not allowed`
|
|
234
239
|
: `${path} schema violation: ${describeSchemaViolation(error)}`, path, pointer);
|
|
235
240
|
});
|
|
241
|
+
/**
|
|
242
|
+
* The same violation, reported once.
|
|
243
|
+
*
|
|
244
|
+
* A union reports the branches it tried, and sibling branches that share a
|
|
245
|
+
* requirement each raise it - the reviewer reads one document, not the
|
|
246
|
+
* validator's search. Diagnostics are already sorted, so identical neighbours
|
|
247
|
+
* are adjacent; nothing that differs in code, pointer, or message is lost.
|
|
248
|
+
*/
|
|
249
|
+
const distinct = (diagnostics) => diagnostics.filter((diagnostic, index) => {
|
|
250
|
+
if (index === 0)
|
|
251
|
+
return true;
|
|
252
|
+
const previous = diagnostics[index - 1];
|
|
253
|
+
return (previous === undefined ||
|
|
254
|
+
visualDiagnosticOrder(previous, diagnostic) !== 0);
|
|
255
|
+
});
|
|
236
256
|
const parseWith = (validate, input, code) => {
|
|
237
257
|
const path = documentPaths.get(validate) ?? 'visual-document';
|
|
238
258
|
const semantics = semanticsByDocument[path];
|
|
239
|
-
const diagnostics = [
|
|
259
|
+
const diagnostics = distinct([
|
|
240
260
|
...(validate(input)
|
|
241
261
|
? []
|
|
242
|
-
: schemaDiagnostics(validate.errors ?? [], code, path)),
|
|
262
|
+
: schemaDiagnostics(readableSchemaErrors(validate.errors ?? []), code, path)),
|
|
243
263
|
...(semantics ? semantics(input, path) : []),
|
|
244
|
-
].sort(visualDiagnosticOrder);
|
|
264
|
+
].sort(visualDiagnosticOrder));
|
|
245
265
|
if (diagnostics.length > 0)
|
|
246
266
|
return { ok: false, diagnostics };
|
|
247
267
|
return { ok: true, value: input };
|
|
@@ -250,7 +270,27 @@ export const parseVisualModel = (input) => parseWith(validateVisualModel, input,
|
|
|
250
270
|
export const parseVisualSessionRequest = (input) => parseWith(validateVisualSessionRequest, input, 'YMVS101');
|
|
251
271
|
export const parseVisualSessionStarted = (input) => parseWith(validateVisualSessionStarted, input, 'YMVS102');
|
|
252
272
|
export const parseVisualSessionDescriptor = (input) => parseWith(validateVisualSessionDescriptor, input, 'YMVS103');
|
|
253
|
-
|
|
273
|
+
/**
|
|
274
|
+
* The type an untrusted frame claims to be, when the protocol has that type.
|
|
275
|
+
*
|
|
276
|
+
* A refusal has to say what it refused, and the browser is holding a control
|
|
277
|
+
* open until it does. The claim is worth reporting before the document is
|
|
278
|
+
* known to be valid because the frame that named it is the one that failed.
|
|
279
|
+
*/
|
|
280
|
+
export const visualBrowserInputType = (input) => {
|
|
281
|
+
const claimed = documentFields(input).type;
|
|
282
|
+
return typeof claimed === 'string' && browserInputBranches.has(claimed)
|
|
283
|
+
? claimed
|
|
284
|
+
: undefined;
|
|
285
|
+
};
|
|
286
|
+
export const parseVisualBrowserInput = (input) => {
|
|
287
|
+
const claimed = visualBrowserInputType(input);
|
|
288
|
+
const validate = claimed === undefined
|
|
289
|
+
? validateVisualBrowserInput
|
|
290
|
+
: // The branch is in the map because `claimed` came out of it.
|
|
291
|
+
browserInputBranches.get(claimed);
|
|
292
|
+
return parseWith(validate, input, 'YMVS109');
|
|
293
|
+
};
|
|
254
294
|
export const parseVisualEvent = (input) => parseWith(validateVisualEvent, input, 'YMVS104');
|
|
255
295
|
export const parseVisualResponse = (input) => parseWith(validateVisualResponse, input, 'YMVS105');
|
|
256
296
|
export const parseVisualHandoff = (input) => parseWith(validateVisualHandoff, input, 'YMVS107');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { VisualDiagnostic, VisualSessionRequest } from './protocol-contract.js';
|
|
2
|
+
export interface VisualSessionRequestOptions {
|
|
3
|
+
readonly cwd: string;
|
|
4
|
+
/** Defaults to the workspace id. */
|
|
5
|
+
readonly title?: string;
|
|
6
|
+
/** Defaults to a line naming the workspace and its manifest. */
|
|
7
|
+
readonly description?: string;
|
|
8
|
+
/** Defaults to the first projection the workspace declares. */
|
|
9
|
+
readonly initialView?: string;
|
|
10
|
+
/** Defaults to `false`: a diagram-only session needs no model provider. */
|
|
11
|
+
readonly chatEnabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export type VisualSessionRequestResult = {
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
readonly request: VisualSessionRequest;
|
|
16
|
+
} | {
|
|
17
|
+
readonly ok: false;
|
|
18
|
+
readonly diagnostics: readonly VisualDiagnostic[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Builds the `yarramate/visual-session-request/v1` document that `start`
|
|
22
|
+
* consumes, from the workspace on disk.
|
|
23
|
+
*
|
|
24
|
+
* The request carries a whole compiled model — 247 nodes and 338 edges for this
|
|
25
|
+
* repository's own architecture — so it is a machine's transcription of the
|
|
26
|
+
* workspace, never something an agent can honestly hand-author. Every input the
|
|
27
|
+
* document needs is already derivable: the graph from the native compiler, the
|
|
28
|
+
* digests from the same sources that compiled, and the view list from the
|
|
29
|
+
* workspace's declared projections.
|
|
30
|
+
*
|
|
31
|
+
* The result is validated through `parseVisualSessionRequest` before it is
|
|
32
|
+
* returned, so the byte ceiling, digest confinement, and authority agreement
|
|
33
|
+
* are the protocol's own rules rather than a second set restated here. A
|
|
34
|
+
* builder that cannot produce a valid document refuses instead of handing a
|
|
35
|
+
* session something it would reject at start.
|
|
36
|
+
*/
|
|
37
|
+
export declare const buildVisualSessionRequest: (options: VisualSessionRequestOptions) => VisualSessionRequestResult;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { loadProjection } from '../../projection.js';
|
|
5
|
+
import { loadWorkspaceManifest } from '../../workspace.js';
|
|
6
|
+
import { parseVisualSessionRequest } from './protocol.js';
|
|
7
|
+
import { buildVisualModelGraph } from './session-store.js';
|
|
8
|
+
/**
|
|
9
|
+
* The one manifest a session can serve. `startVisualServer` resolves exactly
|
|
10
|
+
* this path relative to its own working directory, so the builder reads the
|
|
11
|
+
* same file rather than accepting a path that could name a different workspace
|
|
12
|
+
* than the session about to render it.
|
|
13
|
+
*/
|
|
14
|
+
const MANIFEST_PATH = '.yarramate/workspace.yaml';
|
|
15
|
+
const requestDiagnostic = (code, message, path = MANIFEST_PATH) => ({
|
|
16
|
+
severity: 'error',
|
|
17
|
+
code,
|
|
18
|
+
message,
|
|
19
|
+
path,
|
|
20
|
+
pointer: '/',
|
|
21
|
+
line: 1,
|
|
22
|
+
column: 1,
|
|
23
|
+
});
|
|
24
|
+
const digestOf = (source) => createHash('sha256').update(source).digest('hex');
|
|
25
|
+
/**
|
|
26
|
+
* Builds the `yarramate/visual-session-request/v1` document that `start`
|
|
27
|
+
* consumes, from the workspace on disk.
|
|
28
|
+
*
|
|
29
|
+
* The request carries a whole compiled model — 247 nodes and 338 edges for this
|
|
30
|
+
* repository's own architecture — so it is a machine's transcription of the
|
|
31
|
+
* workspace, never something an agent can honestly hand-author. Every input the
|
|
32
|
+
* document needs is already derivable: the graph from the native compiler, the
|
|
33
|
+
* digests from the same sources that compiled, and the view list from the
|
|
34
|
+
* workspace's declared projections.
|
|
35
|
+
*
|
|
36
|
+
* The result is validated through `parseVisualSessionRequest` before it is
|
|
37
|
+
* returned, so the byte ceiling, digest confinement, and authority agreement
|
|
38
|
+
* are the protocol's own rules rather than a second set restated here. A
|
|
39
|
+
* builder that cannot produce a valid document refuses instead of handing a
|
|
40
|
+
* session something it would reject at start.
|
|
41
|
+
*/
|
|
42
|
+
export const buildVisualSessionRequest = (options) => {
|
|
43
|
+
const manifestPath = resolve(options.cwd, MANIFEST_PATH);
|
|
44
|
+
let manifestSource;
|
|
45
|
+
try {
|
|
46
|
+
manifestSource = readFileSync(manifestPath, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
diagnostics: [
|
|
52
|
+
requestDiagnostic('YMVS410', `Workspace manifest ${manifestPath} cannot be read`),
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const loaded = loadWorkspaceManifest({ path: manifestPath, source: manifestSource }, options.cwd);
|
|
57
|
+
if (!loaded.ok) {
|
|
58
|
+
return { ok: false, diagnostics: loaded.diagnostics };
|
|
59
|
+
}
|
|
60
|
+
const workspace = loaded.workspace;
|
|
61
|
+
// Same order the session's own recompile uses, so the request's graph and
|
|
62
|
+
// every graph the session rebuilds after a commit come from one input list.
|
|
63
|
+
const sourcePaths = [...workspace.profiles, ...workspace.documents];
|
|
64
|
+
const sources = [];
|
|
65
|
+
for (const path of sourcePaths) {
|
|
66
|
+
try {
|
|
67
|
+
sources.push({ path, source: readFileSync(resolve(options.cwd, path), 'utf8') });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
diagnostics: [
|
|
73
|
+
requestDiagnostic('YMVS411', `Workspace source ${path} cannot be read`, path),
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const built = buildVisualModelGraph(sources);
|
|
79
|
+
if (!built.ok)
|
|
80
|
+
return { ok: false, diagnostics: built.diagnostics };
|
|
81
|
+
// A projection that does not load is skipped by the session too: the request
|
|
82
|
+
// names a view the browser will actually be able to open.
|
|
83
|
+
const viewIds = workspace.projections.flatMap((projectionPath) => {
|
|
84
|
+
try {
|
|
85
|
+
const projection = loadProjection({
|
|
86
|
+
path: projectionPath,
|
|
87
|
+
source: readFileSync(resolve(options.cwd, projectionPath), 'utf8'),
|
|
88
|
+
});
|
|
89
|
+
return projection.ok ? [projection.projection.id] : [];
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const initialView = options.initialView ?? viewIds[0];
|
|
96
|
+
if (initialView === undefined) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
diagnostics: [
|
|
100
|
+
requestDiagnostic('YMVS412', `Workspace ${workspace.id} declares no loadable projection, so a session has no view to open`),
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (!viewIds.includes(initialView)) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
diagnostics: [
|
|
108
|
+
requestDiagnostic('YMVS413', `Workspace ${workspace.id} declares no projection "${initialView}": ${viewIds.length === 0 ? 'it declares none' : viewIds.join(', ')}`),
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const request = {
|
|
113
|
+
format: 'yarramate/visual-session-request/v1',
|
|
114
|
+
authority: 'canonical',
|
|
115
|
+
title: options.title ?? workspace.id,
|
|
116
|
+
description: options.description ??
|
|
117
|
+
`Native rendering of workspace ${workspace.id} from ${MANIFEST_PATH}`,
|
|
118
|
+
chatEnabled: options.chatEnabled ?? false,
|
|
119
|
+
initialModel: {
|
|
120
|
+
format: 'yarramate/visual-model/v1',
|
|
121
|
+
authority: 'canonical',
|
|
122
|
+
initialView,
|
|
123
|
+
sourceDigests: Object.fromEntries(sources.map(({ path, source }) => [path, digestOf(source)])),
|
|
124
|
+
graph: built.graph,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const parsed = parseVisualSessionRequest(request);
|
|
128
|
+
if (!parsed.ok)
|
|
129
|
+
return { ok: false, diagnostics: parsed.diagnostics };
|
|
130
|
+
return { ok: true, request: parsed.value };
|
|
131
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type VisualDiagnostic, type VisualEvent, type VisualFreezeReason, type VisualHandoff, type VisualLifecycle, type VisualSessionRequest, type VisualSessionStarted, type VisualStatus, type VisualTerminationReason } from
|
|
2
|
-
import { type TerminalEventDependencies, type VisualSessionPaths } from
|
|
3
|
-
import type { VisualRenderedModel, VisualServerFrame, VisualSessionSnapshot, VisualTranscriptRecord } from
|
|
1
|
+
import { type VisualDiagnostic, type VisualEvent, type VisualFreezeReason, type VisualHandoff, type VisualLifecycle, type VisualSessionRequest, type VisualSessionStarted, type VisualStatus, type VisualTerminationReason } from "./protocol.js";
|
|
2
|
+
import { type TerminalEventDependencies, type VisualSessionPaths } from "./session-store.js";
|
|
3
|
+
import type { VisualRenderedModel, VisualServerFrame, VisualSessionSnapshot, VisualTranscriptRecord } from "./wire.js";
|
|
4
4
|
export type { VisualRenderedModel, VisualServerFrame, VisualSessionSnapshot, VisualTranscriptRecord, };
|
|
5
5
|
/**
|
|
6
6
|
* Document name reported by diagnostics the server itself raises — a refused
|
|
@@ -14,9 +14,9 @@ export declare const VISUAL_SERVER_DOCUMENT = "visual-session-server";
|
|
|
14
14
|
* and the page can be neither framed nor used as a base for relative fetches.
|
|
15
15
|
*/
|
|
16
16
|
export declare const VISUAL_BROWSER_HEADERS: {
|
|
17
|
-
readonly
|
|
18
|
-
readonly
|
|
19
|
-
readonly
|
|
17
|
+
readonly "Referrer-Policy": "no-referrer";
|
|
18
|
+
readonly "X-Content-Type-Options": "nosniff";
|
|
19
|
+
readonly "Cache-Control": "no-store";
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
22
|
* This session's content policy. Every source stays `'self'`; the one addition
|
|
@@ -52,7 +52,6 @@ export type VisualResponseAcceptance = {
|
|
|
52
52
|
readonly accepted: true;
|
|
53
53
|
readonly duplicate: boolean;
|
|
54
54
|
readonly lastSequence: number;
|
|
55
|
-
readonly model?: VisualRenderedModel;
|
|
56
55
|
readonly diagnostics: readonly VisualDiagnostic[];
|
|
57
56
|
} | {
|
|
58
57
|
readonly accepted: false;
|
|
@@ -67,6 +66,8 @@ export interface VisualServerOptions {
|
|
|
67
66
|
readonly request: VisualSessionRequest;
|
|
68
67
|
/** Directory that holds one directory per live session. */
|
|
69
68
|
readonly baseDir: string;
|
|
69
|
+
/** Working directory for resolving .yarramate/workspace.yaml and projections. */
|
|
70
|
+
readonly cwd: string;
|
|
70
71
|
/** Root of the self-contained browser application. */
|
|
71
72
|
readonly assetRoot?: string;
|
|
72
73
|
readonly now?: () => Date;
|
|
@@ -98,8 +99,6 @@ export interface VisualServerHandle {
|
|
|
98
99
|
export interface ActiveVisualSession {
|
|
99
100
|
readonly paths: VisualSessionPaths;
|
|
100
101
|
lifecycle: VisualLifecycle;
|
|
101
|
-
/** Cancels whatever the trusted compiler is doing for this session. */
|
|
102
|
-
readonly compilerAbort: AbortController;
|
|
103
102
|
/** Records why the browser stopped being able to speak. */
|
|
104
103
|
readonly freeze: (reason: VisualFreezeReason) => void;
|
|
105
104
|
/** Settles work already admitted, so recovery reads a quiet journal. */
|
|
@@ -115,11 +114,10 @@ export interface ActiveVisualSession {
|
|
|
115
114
|
* that failed, a browser that never came back, a cancelling main agent, or a
|
|
116
115
|
* runtime shutting itself down.
|
|
117
116
|
*
|
|
118
|
-
* The order is the invariant. Input freezes before anything is read;
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* removes the session directory.
|
|
117
|
+
* The order is the invariant. Input freezes before anything is read; work
|
|
118
|
+
* already admitted is still allowed to finish; exactly one terminal event is
|
|
119
|
+
* journaled; and the handoff is recovered without deleting anything, so
|
|
120
|
+
* `stop` — and only `stop` — is what removes the session directory.
|
|
123
121
|
*/
|
|
124
122
|
export declare const terminateVisualSession: (session: ActiveVisualSession, reason: VisualTerminationReason) => Promise<VisualHandoff>;
|
|
125
123
|
/**
|