@memoarchitect/tools 0.11.2 → 0.12.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 +1 -1
- package/package.json +3 -3
- package/packages/tools/lib/browser/index.d.ts +1 -0
- package/packages/tools/lib/browser/index.d.ts.map +1 -1
- package/packages/tools/lib/browser/index.js +6 -0
- package/packages/tools/lib/browser/index.js.map +1 -1
- package/packages/tools/lib/model/builder.d.ts.map +1 -1
- package/packages/tools/lib/model/builder.js +95 -10
- package/packages/tools/lib/model/builder.js.map +1 -1
- package/packages/tools/lib/model/kind-registry.d.ts +12 -0
- package/packages/tools/lib/model/kind-registry.d.ts.map +1 -1
- package/packages/tools/lib/model/kind-registry.js +5 -0
- package/packages/tools/lib/model/kind-registry.js.map +1 -1
- package/packages/tools/lib/model/layer-resolver.d.ts.map +1 -1
- package/packages/tools/lib/model/layer-resolver.js +12 -4
- package/packages/tools/lib/model/layer-resolver.js.map +1 -1
- package/packages/tools/lib/model/ontology-loader.d.ts +27 -0
- package/packages/tools/lib/model/ontology-loader.d.ts.map +1 -1
- package/packages/tools/lib/model/ontology-loader.js +5 -3
- package/packages/tools/lib/model/ontology-loader.js.map +1 -1
- package/packages/tools/lib/model/relationship-legality.d.ts +2 -0
- package/packages/tools/lib/model/relationship-legality.d.ts.map +1 -1
- package/packages/tools/lib/model/relationship-legality.js +13 -3
- package/packages/tools/lib/model/relationship-legality.js.map +1 -1
- package/packages/tools/lib/model/relationship-registry.d.ts.map +1 -1
- package/packages/tools/lib/model/relationship-registry.js +15 -2
- package/packages/tools/lib/model/relationship-registry.js.map +1 -1
- package/packages/tools/lib/model/semantic.d.ts +0 -8
- package/packages/tools/lib/model/semantic.d.ts.map +1 -1
- package/packages/tools/lib/model/semantic.js.map +1 -1
- package/packages/tools/lib/model/view-deriver.d.ts +0 -9
- package/packages/tools/lib/model/view-deriver.d.ts.map +1 -1
- package/packages/tools/lib/model/view-deriver.js +121 -5
- package/packages/tools/lib/model/view-deriver.js.map +1 -1
- package/packages/tools/lib/protocol/messages.d.ts +20 -0
- package/packages/tools/lib/protocol/messages.d.ts.map +1 -1
- package/packages/tools/lib/server/dev-server.d.ts +1 -1
- package/packages/tools/lib/server/dev-server.d.ts.map +1 -1
- package/packages/tools/lib/server/dev-server.js +166 -44
- package/packages/tools/lib/server/dev-server.js.map +1 -1
- package/packages/tools/lib/server/persistor.d.ts.map +1 -1
- package/packages/tools/lib/server/persistor.js +156 -6
- package/packages/tools/lib/server/persistor.js.map +1 -1
- package/packages/tools/lib/server/view-writer.d.ts +44 -0
- package/packages/tools/lib/server/view-writer.d.ts.map +1 -0
- package/packages/tools/lib/server/view-writer.js +294 -0
- package/packages/tools/lib/server/view-writer.js.map +1 -0
- package/packages/tools/lib/sysml-ir/generated/sysml-metaclass-names.d.ts +4 -0
- package/packages/tools/lib/sysml-ir/generated/sysml-metaclass-names.d.ts.map +1 -0
- package/packages/tools/lib/sysml-ir/generated/sysml-metaclass-names.js +191 -0
- package/packages/tools/lib/sysml-ir/generated/sysml-metaclass-names.js.map +1 -0
|
@@ -20,6 +20,7 @@ import { assertSingleDomainMutation, MixedMutationDomainError } from './mutation
|
|
|
20
20
|
import { classifyConflict } from './conflict-policy.js';
|
|
21
21
|
import { checkRulePolicy, insertRulePolicy, renderRulePolicy } from './rule-policy-writer.js';
|
|
22
22
|
import { loadViewLayouts, saveViewLayout } from './view-layout-store.js';
|
|
23
|
+
import { updateViewExpose, writeViewDeclaration } from './view-writer.js';
|
|
23
24
|
import { usageKeyword } from '../model/semantic.js';
|
|
24
25
|
import { loadDhfDocs, saveDhfDoc, deleteDhfDoc, loadDhfSettings, saveDhfSettings, listRepoTemplates, readRepoTemplate, saveRepoTemplate, } from './dhf-doc-store.js';
|
|
25
26
|
const MODEL_MUTATION_MESSAGES = new Set([
|
|
@@ -92,7 +93,30 @@ function streamFile(res, fullPath, status = 200) {
|
|
|
92
93
|
res.writeHead(status, { 'Content-Type': mime, 'Content-Length': size });
|
|
93
94
|
createReadStream(fullPath).pipe(res);
|
|
94
95
|
}
|
|
95
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* The subtrees a project's screen captures may be served from.
|
|
98
|
+
*
|
|
99
|
+
* `model/assets` is where MEMO put them back when the whole model lived under
|
|
100
|
+
* `model/`. A project laid out with its source in `src/` has no `model/`
|
|
101
|
+
* directory at all and keeps its captures in `assets/` beside it — and those
|
|
102
|
+
* were unreachable, so its UI Screens workspace drew every region over a
|
|
103
|
+
* blank backdrop with no error to explain it. The request simply fell through
|
|
104
|
+
* to the SPA and the `<img>` got an HTML document back.
|
|
105
|
+
*
|
|
106
|
+
* Naming roots at all is the allow-list, and that part is not negotiable:
|
|
107
|
+
* this server hands project files to a browser, so it serves these two
|
|
108
|
+
* subtrees and nothing else — not `.env`, not `.git/`, not the source.
|
|
109
|
+
*/
|
|
110
|
+
const CAPTURE_ROOTS = ['model/assets', 'assets'];
|
|
111
|
+
/** True when `candidate` is a real descendant of `root` — the traversal guard. */
|
|
112
|
+
function isWithinRoot(root, candidate) {
|
|
113
|
+
const fromRoot = relative(root, candidate);
|
|
114
|
+
return Boolean(fromRoot)
|
|
115
|
+
&& fromRoot !== '..'
|
|
116
|
+
&& !fromRoot.startsWith('../')
|
|
117
|
+
&& !fromRoot.startsWith('..\\');
|
|
118
|
+
}
|
|
119
|
+
/** Resolve only project-owned screen captures, under either capture root. */
|
|
96
120
|
export function resolveProjectAssetRequest(projectRoot, requestUrl) {
|
|
97
121
|
let pathname;
|
|
98
122
|
try {
|
|
@@ -101,15 +125,45 @@ export function resolveProjectAssetRequest(projectRoot, requestUrl) {
|
|
|
101
125
|
catch {
|
|
102
126
|
return undefined;
|
|
103
127
|
}
|
|
104
|
-
if (!pathname.startsWith('/model/'))
|
|
128
|
+
if (!pathname.startsWith('/model/') && !pathname.startsWith('/assets/'))
|
|
105
129
|
return undefined;
|
|
106
|
-
const assetRoot = resolve(projectRoot, 'model', 'assets');
|
|
107
130
|
const requested = resolve(projectRoot, pathname.replace(/^\//, ''));
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
const within = CAPTURE_ROOTS.some(root => isWithinRoot(resolve(projectRoot, root), requested));
|
|
132
|
+
return within ? requested : undefined;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Report views that share an id.
|
|
136
|
+
*
|
|
137
|
+
* Kind collisions are already recorded this way (`KindNameCollision`) for the
|
|
138
|
+
* same reason: a `Map` keyed by a name silently keeps one writer, and which one
|
|
139
|
+
* survives then depends on file iteration order. A view is looked up by id from
|
|
140
|
+
* the URL, from the explorer and from every permalink, so a collision does not
|
|
141
|
+
* make views ambiguous — it makes the shadowed ones unreachable.
|
|
142
|
+
*/
|
|
143
|
+
function reportDuplicateViewIds(diagrams) {
|
|
144
|
+
const byId = new Map();
|
|
145
|
+
for (const diagram of diagrams) {
|
|
146
|
+
const list = byId.get(diagram.id);
|
|
147
|
+
if (list)
|
|
148
|
+
list.push(diagram);
|
|
149
|
+
else
|
|
150
|
+
byId.set(diagram.id, [diagram]);
|
|
151
|
+
}
|
|
152
|
+
const collisions = [...byId.values()].filter(group => group.length > 1);
|
|
153
|
+
if (collisions.length === 0)
|
|
154
|
+
return;
|
|
155
|
+
const shadowed = collisions.reduce((sum, group) => sum + group.length - 1, 0);
|
|
156
|
+
console.error(`[Views] ${shadowed} view(s) are UNREACHABLE: ${collisions.length} id(s) are `
|
|
157
|
+
+ 'declared by more than one view, and a view is addressed by its id. '
|
|
158
|
+
+ 'Give each view its own `providedId`:');
|
|
159
|
+
for (const group of collisions) {
|
|
160
|
+
const reachable = group[0].shortId ?? group[0].name ?? group[0].id;
|
|
161
|
+
console.error(` - ${group[0].id} is claimed by ${group.length} views; only ${reachable} can be opened`);
|
|
162
|
+
for (const shadow of group.slice(1)) {
|
|
163
|
+
console.error(` shadowed: ${shadow.shortId ?? shadow.name ?? shadow.id}`
|
|
164
|
+
+ `${shadow.sourceFile ? ` (${shadow.sourceFile})` : ''}`);
|
|
165
|
+
}
|
|
111
166
|
}
|
|
112
|
-
return requested;
|
|
113
167
|
}
|
|
114
168
|
export async function createDevServer(options) {
|
|
115
169
|
const { port, webPackagePath, initialMessages, transformClientHtml } = options;
|
|
@@ -219,10 +273,19 @@ export async function createDevServer(options) {
|
|
|
219
273
|
/** Serve captures referenced by project-relative ScreenCapture.imageUri. */
|
|
220
274
|
function serveProjectAsset(req, res) {
|
|
221
275
|
const requestUrl = req.url ?? '/';
|
|
222
|
-
|
|
276
|
+
const pathname = requestUrl.split('?')[0];
|
|
277
|
+
if (!pathname.startsWith('/model/') && !pathname.startsWith('/assets/'))
|
|
223
278
|
return false;
|
|
224
279
|
const requested = resolveProjectAssetRequest(options.projectRoot, requestUrl);
|
|
225
|
-
|
|
280
|
+
const owned = Boolean(requested) && existsSync(requested) && statSync(requested).isFile();
|
|
281
|
+
if (!owned) {
|
|
282
|
+
// `/assets/` is ALSO where the client's own hashed bundles live,
|
|
283
|
+
// and this handler runs before the web dist. Claiming every
|
|
284
|
+
// `/assets/` request would 404 the application's own JavaScript,
|
|
285
|
+
// so an unclaimed one falls through; `/model/` is unambiguous and
|
|
286
|
+
// still reports a missing capture as a missing capture.
|
|
287
|
+
if (pathname.startsWith('/assets/'))
|
|
288
|
+
return false;
|
|
226
289
|
res.writeHead(404);
|
|
227
290
|
res.end('Capture not found');
|
|
228
291
|
return true;
|
|
@@ -321,20 +384,39 @@ export async function createDevServer(options) {
|
|
|
321
384
|
const { WebSocketServer } = await import('ws');
|
|
322
385
|
const wss = new WebSocketServer({ server });
|
|
323
386
|
const clients = new Set();
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
387
|
+
// Every view is DECLARED in SysML — see `view-writer.ts`. The sidecar at
|
|
388
|
+
// .memo/user-diagrams.json used to be merged in here as a second class of
|
|
389
|
+
// view, which is how a view could exist for Architect and for nothing else:
|
|
390
|
+
// no source file, so invisible to `memo validate`, to `syside check`, to
|
|
391
|
+
// review, and — wherever `.memo/` is ignored — to git.
|
|
392
|
+
//
|
|
393
|
+
// Rows left in that file are therefore not views but UNDECLARED views: an
|
|
394
|
+
// error to be fixed by declaring them, not a state to render. They are
|
|
395
|
+
// reported by id and not merged. The file is never rewritten or deleted
|
|
396
|
+
// here — those rows are the only record of what still needs declaring.
|
|
397
|
+
const undeclared = loadUserDiagrams(options.projectRoot);
|
|
398
|
+
if (undeclared.length > 0) {
|
|
399
|
+
const initialModel = initialMessages.find(m => m.type === 'model:update');
|
|
400
|
+
const declaredIds = new Set((initialModel?.payload.diagrams ?? []).map(diagram => diagram.id));
|
|
401
|
+
const orphans = undeclared.filter(diagram => !declaredIds.has(diagram.id));
|
|
402
|
+
if (orphans.length > 0) {
|
|
403
|
+
console.error(`[Views] ${orphans.length} view(s) in .memo/user-diagrams.json have no SysML declaration `
|
|
404
|
+
+ 'and were NOT loaded. Declare each as a `view … { }` in source, then delete its row:');
|
|
405
|
+
for (const orphan of orphans) {
|
|
406
|
+
console.error(` - ${orphan.id} ${orphan.name ?? ''} (${orphan.viewKind ?? 'unknown kind'})`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
337
409
|
}
|
|
410
|
+
// A view is addressed by its id, so two views sharing one is not a
|
|
411
|
+
// duplicate label — it is one view the product can never open. Every lookup
|
|
412
|
+
// returns whichever loaded first and the rest are shadowed silently, with
|
|
413
|
+
// no error anywhere, which is the worst way for a model to be wrong.
|
|
414
|
+
//
|
|
415
|
+
// Found in affera as 11 of 95 views unreachable: ten clinical-step views
|
|
416
|
+
// all declaring VIEW-AFR-OP-SOS-004, and a CIU interconnect view claiming
|
|
417
|
+
// the id of the CIU decomposition view, so opening it showed the other.
|
|
418
|
+
reportDuplicateViewIds(initialMessages.find(m => m.type === 'model:update')
|
|
419
|
+
?.payload.diagrams ?? []);
|
|
338
420
|
const currentDiagrams = () => {
|
|
339
421
|
const model = initialMessages.find(m => m.type === 'model:update');
|
|
340
422
|
return model?.payload.diagrams ?? [];
|
|
@@ -918,8 +1000,10 @@ export async function createDevServer(options) {
|
|
|
918
1000
|
if (existsSync(path))
|
|
919
1001
|
recordWriteTransaction(sourceFile, readFileSync(path, 'utf8'));
|
|
920
1002
|
}
|
|
921
|
-
//
|
|
922
|
-
//
|
|
1003
|
+
// The only rows left in the sidecar are UNDECLARED views awaiting
|
|
1004
|
+
// migration; none is loaded as a view. They are still kept accurate,
|
|
1005
|
+
// because they are the record of what needs declaring and a dangling
|
|
1006
|
+
// element ID would carry straight into the declaration.
|
|
923
1007
|
const diagrams = loadUserDiagrams(options.projectRoot);
|
|
924
1008
|
let changed = false;
|
|
925
1009
|
const cleaned = diagrams.map(diagram => {
|
|
@@ -1254,10 +1338,17 @@ export async function createDevServer(options) {
|
|
|
1254
1338
|
const safeStem = rawStem
|
|
1255
1339
|
.normalize('NFKD').replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '')
|
|
1256
1340
|
|| 'capture';
|
|
1257
|
-
|
|
1341
|
+
// Write into the layout the project actually uses. Creating
|
|
1342
|
+
// `model/` under a project whose source lives in `src/`
|
|
1343
|
+
// resurrects a directory that layout deliberately does not
|
|
1344
|
+
// have, and the capture would then sit outside the tree the
|
|
1345
|
+
// rest of the model is kept in.
|
|
1346
|
+
const captureRoot = existsSync(resolve(options.projectRoot, 'model'))
|
|
1347
|
+
? resolve(options.projectRoot, 'model', 'assets')
|
|
1348
|
+
: resolve(options.projectRoot, 'assets');
|
|
1349
|
+
const assetDir = resolve(captureRoot, safeView);
|
|
1258
1350
|
const assetPath = resolve(assetDir, `${safeStem}${extension}`);
|
|
1259
|
-
|
|
1260
|
-
if (relative(modelRoot, assetPath).startsWith('..'))
|
|
1351
|
+
if (!isWithinRoot(captureRoot, assetPath))
|
|
1261
1352
|
throw new Error('Invalid capture path.');
|
|
1262
1353
|
mkdirSync(assetDir, { recursive: true });
|
|
1263
1354
|
writeFileSync(assetPath, bytes);
|
|
@@ -1276,29 +1367,60 @@ export async function createDevServer(options) {
|
|
|
1276
1367
|
else if (msg.type === 'diagram:create') {
|
|
1277
1368
|
const { projectRoot } = options;
|
|
1278
1369
|
const diagram = { ...msg.payload, auto: false };
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1370
|
+
// A view is model content, so it is DECLARED, not recorded in
|
|
1371
|
+
// a sidecar. Writing the SysML puts it where `memo validate`,
|
|
1372
|
+
// `syside check`, review and git can all see it, and — the
|
|
1373
|
+
// derived diagram then carrying a `sourceFile` — takes its
|
|
1374
|
+
// `.viewlayout` companion out of the hidden store too.
|
|
1375
|
+
const declared = await writeViewDeclaration(projectRoot, currentModel() ?? { elements: {}, diagrams: [] }, diagram);
|
|
1376
|
+
if (!declared.success) {
|
|
1377
|
+
// No fallback. A view that cannot be declared is not one
|
|
1378
|
+
// this project can hold, and recording it in a sidecar
|
|
1379
|
+
// would only hide that until something downstream needed
|
|
1380
|
+
// the declaration.
|
|
1381
|
+
const error = `The view "${diagram.name}" was not created: ${declared.reason}. `
|
|
1382
|
+
+ 'Every view must be declared in SysML.';
|
|
1383
|
+
console.error(`[Diagram] REFUSED: ${error}`);
|
|
1384
|
+
ws.send(JSON.stringify({ type: 'app:error', payload: { requestId: msg.payload?.requestId, error } }));
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
console.log(`[Diagram] Declared: ${diagram.name} (${diagram.id}) as ${declared.identifier} in ${declared.filePath}`);
|
|
1388
|
+
// The file watcher recompiles and `view-deriver` emits the
|
|
1389
|
+
// diagram; no broadcast here, or clients would briefly hold
|
|
1390
|
+
// a second, sidecar-shaped copy of it.
|
|
1391
|
+
await recompileAfterWrite();
|
|
1284
1392
|
}
|
|
1285
1393
|
else if (msg.type === 'diagram:update') {
|
|
1394
|
+
// What a view shows lives in its `expose` members, not in a
|
|
1395
|
+
// list of ids in a sidecar, so the update is applied to the
|
|
1396
|
+
// declaration. This used to edit the sidecar, which meant
|
|
1397
|
+
// that for a DECLARED view it silently did nothing at all.
|
|
1286
1398
|
const { projectRoot } = options;
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1399
|
+
const declared = currentDiagrams().find(d => d.id === msg.payload?.id);
|
|
1400
|
+
const result = declared
|
|
1401
|
+
? await updateViewExpose(projectRoot, currentModel() ?? { elements: {} }, declared, msg.payload?.elementIds)
|
|
1402
|
+
: { success: false, reason: `no view "${msg.payload?.id}" is declared` };
|
|
1403
|
+
if (result.success) {
|
|
1404
|
+
console.log(`[Diagram] Exposed ${result.exposed.join(', ')} on ${declared.name} (${result.filePath})`);
|
|
1405
|
+
await recompileAfterWrite();
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
const error = `The view "${declared?.name ?? msg.payload?.id}" was not changed: ${result.reason}.`;
|
|
1409
|
+
console.error(`[Diagram] REFUSED: ${error}`);
|
|
1410
|
+
ws.send(JSON.stringify({ type: 'app:error', payload: { requestId: msg.payload?.requestId, error } }));
|
|
1293
1411
|
}
|
|
1294
1412
|
}
|
|
1295
1413
|
else if (msg.type === 'diagram:delete') {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1414
|
+
// Deleting a view means deleting its declaration, which is
|
|
1415
|
+
// `element:delete` on the view element — not a sidecar edit.
|
|
1416
|
+
const declared = currentDiagrams().find(d => d.id === msg.payload?.id);
|
|
1417
|
+
const where = declared?.sourceFile
|
|
1418
|
+
? `Remove its \`view\` declaration from ${declared.sourceFile}.`
|
|
1419
|
+
: 'It is not declared in SysML.';
|
|
1420
|
+
const error = `The view "${declared?.name ?? msg.payload?.id}" cannot be deleted from here — `
|
|
1421
|
+
+ `a view exists because it is declared. ${where}`;
|
|
1422
|
+
console.error(`[Diagram] REFUSED: ${error}`);
|
|
1423
|
+
ws.send(JSON.stringify({ type: 'app:error', payload: { requestId: msg.payload?.requestId, error } }));
|
|
1302
1424
|
}
|
|
1303
1425
|
else if (msg.type === 'diagram:layout:update') {
|
|
1304
1426
|
const { diagramId, layout } = msg.payload;
|