yarramate 1.4.0 → 1.5.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/dist/adapters/visual/request.js +8 -1
- package/dist/adapters/visual/session-server.js +154 -45
- package/dist/apply-command.js +7 -0
- package/dist/ask-command.js +13 -5
- package/dist/check-command.js +10 -1
- package/dist/cli-support.d.ts +2 -0
- package/dist/cli-support.js +13 -0
- package/dist/cli.js +1 -0
- package/dist/design-command.js +28 -9
- package/dist/export-command.js +5 -1
- package/dist/interrogate-command.d.ts +1 -1
- package/dist/interrogate-command.js +103 -1
- package/dist/visual-app/assets/{index-yCHtmQUH.js → index-C42oLqXj.js} +2 -2
- package/dist/visual-app/index.html +1 -1
- package/dist/visual-app-lib/editor.js +26896 -26793
- package/dist/visual-app-lib/types/interrogate-command.d.ts +1 -1
- package/package.json +1 -1
|
@@ -58,7 +58,14 @@ export const buildVisualSessionRequest = (options) => {
|
|
|
58
58
|
const workspace = loaded.workspace;
|
|
59
59
|
// Same order the session's own recompile uses, so the request's graph and
|
|
60
60
|
// every graph the session rebuilds after a commit come from one input list.
|
|
61
|
-
|
|
61
|
+
// Patterns are compiler input like profiles (#268). Omitting them compiled a
|
|
62
|
+
// different workspace than the manifest describes, and an instance binding
|
|
63
|
+
// parts then failed YM419 for a pattern the manifest declared.
|
|
64
|
+
const sourcePaths = [
|
|
65
|
+
...workspace.profiles,
|
|
66
|
+
...workspace.patterns,
|
|
67
|
+
...workspace.documents,
|
|
68
|
+
];
|
|
62
69
|
const sources = [];
|
|
63
70
|
for (const path of sourcePaths) {
|
|
64
71
|
try {
|
|
@@ -501,22 +501,58 @@ export const startVisualServer = async (options) => {
|
|
|
501
501
|
* session instead of serving a stale graph.
|
|
502
502
|
*/
|
|
503
503
|
const workspaceSources = () => {
|
|
504
|
+
let reading = "";
|
|
504
505
|
try {
|
|
505
|
-
|
|
506
|
+
const paths = [
|
|
506
507
|
...resolvedWorkspace.profiles,
|
|
508
|
+
// Patterns are compiler input like profiles (#268). Without them a
|
|
509
|
+
// session recompiles a workspace the manifest does not describe: an
|
|
510
|
+
// instance binding parts fails YM419, `compiledWorkspace` becomes
|
|
511
|
+
// undefined, and every view's filter then matches nothing while the
|
|
512
|
+
// rail still shows the initial model - which reads as a filter bug
|
|
513
|
+
// rather than as a compile that failed.
|
|
514
|
+
...resolvedWorkspace.patterns,
|
|
507
515
|
...resolvedWorkspace.documents,
|
|
508
|
-
]
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
516
|
+
];
|
|
517
|
+
return {
|
|
518
|
+
ok: true,
|
|
519
|
+
sources: paths.map((path) => {
|
|
520
|
+
reading = path;
|
|
521
|
+
return {
|
|
522
|
+
path,
|
|
523
|
+
source: readFileSync(resolve(options.cwd, path), "utf8"),
|
|
524
|
+
};
|
|
525
|
+
}),
|
|
526
|
+
};
|
|
512
527
|
}
|
|
513
|
-
catch {
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
|
|
528
|
+
catch (cause) {
|
|
529
|
+
// Never an empty list. Compiling one SUCCEEDS - `compileWorkspaceResolved`
|
|
530
|
+
// over no sources returns an empty graph, not a failure - so swallowing
|
|
531
|
+
// the read error here made an unreadable workspace indistinguishable
|
|
532
|
+
// from an empty one, and the session then served "your model has nothing
|
|
533
|
+
// in it" as though it were an answer (#349). The older comment defended
|
|
534
|
+
// the empty list as belonging to a path "only trying to add detail";
|
|
535
|
+
// this feeds the single compile that produces what the browser draws.
|
|
536
|
+
return {
|
|
537
|
+
ok: false,
|
|
538
|
+
path: reading,
|
|
539
|
+
reason: cause instanceof Error ? cause.message : String(cause),
|
|
540
|
+
};
|
|
518
541
|
}
|
|
519
542
|
};
|
|
543
|
+
/**
|
|
544
|
+
* The sources, for attaching subjects to a diagnostic that already exists.
|
|
545
|
+
*
|
|
546
|
+
* Here an unreadable source really is nothing worth reporting: the caller is
|
|
547
|
+
* decorating a refusal it has already decided on, so a missing file costs a
|
|
548
|
+
* subject marker and nothing else. That was the whole of the old
|
|
549
|
+
* `workspaceSources` contract, and it stayed correct for exactly these two
|
|
550
|
+
* callers while being wrong for the compile (#349).
|
|
551
|
+
*/
|
|
552
|
+
const sourcesForDetail = () => {
|
|
553
|
+
const read = workspaceSources();
|
|
554
|
+
return read.ok ? read.sources : [];
|
|
555
|
+
};
|
|
520
556
|
/**
|
|
521
557
|
* The digest of every projection the session knows about, read now.
|
|
522
558
|
*
|
|
@@ -542,11 +578,22 @@ export const startVisualServer = async (options) => {
|
|
|
542
578
|
};
|
|
543
579
|
const recompileWorkspace = () => {
|
|
544
580
|
try {
|
|
545
|
-
const
|
|
581
|
+
const read = workspaceSources();
|
|
582
|
+
if (!read.ok) {
|
|
583
|
+
return {
|
|
584
|
+
ok: false,
|
|
585
|
+
diagnostics: [
|
|
586
|
+
serverDiagnostic("YMVS319", `Workspace source ${read.path} cannot be read, so the workspace was not recompiled; the last good model stays as it is: ${read.reason}`),
|
|
587
|
+
],
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const sources = read.sources;
|
|
546
591
|
const compiled = compileWorkspaceWithProfileContext(sources);
|
|
547
592
|
if (!compiled.ok) {
|
|
548
|
-
|
|
549
|
-
|
|
593
|
+
return {
|
|
594
|
+
ok: false,
|
|
595
|
+
diagnostics: published(compiled.diagnostics, sources),
|
|
596
|
+
};
|
|
550
597
|
}
|
|
551
598
|
compiledWorkspace = {
|
|
552
599
|
graph: compiled.graph,
|
|
@@ -570,14 +617,36 @@ export const startVisualServer = async (options) => {
|
|
|
570
617
|
// replacing the identity the session started with.
|
|
571
618
|
views.splice(0, views.length, ...workspaceModel.views);
|
|
572
619
|
rendered = workspaceModel.model;
|
|
573
|
-
|
|
620
|
+
// Recovered: a browser connecting now is not handed a fault that has
|
|
621
|
+
// been fixed since. The connected ones clear it on the `model` frame
|
|
622
|
+
// this success is about to broadcast.
|
|
623
|
+
standingDiagnostics = [];
|
|
624
|
+
return { ok: true };
|
|
574
625
|
}
|
|
575
|
-
catch {
|
|
576
|
-
|
|
577
|
-
|
|
626
|
+
catch (cause) {
|
|
627
|
+
return {
|
|
628
|
+
ok: false,
|
|
629
|
+
diagnostics: [
|
|
630
|
+
serverDiagnostic("YMVS319", `Workspace could not be recompiled, so the last good model stays as it is: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
631
|
+
],
|
|
632
|
+
};
|
|
578
633
|
}
|
|
579
634
|
};
|
|
580
|
-
|
|
635
|
+
/**
|
|
636
|
+
* What the last recompile could not do, held for a browser that is not
|
|
637
|
+
* connected yet.
|
|
638
|
+
*
|
|
639
|
+
* The startup recompile runs before any socket exists, so a failure there
|
|
640
|
+
* has nobody to tell. Holding it means the first browser to arrive is told
|
|
641
|
+
* what every later one would have heard, rather than opening onto a session
|
|
642
|
+
* that looks well and answers nothing.
|
|
643
|
+
*/
|
|
644
|
+
let standingDiagnostics = [];
|
|
645
|
+
{
|
|
646
|
+
const started = recompileWorkspace();
|
|
647
|
+
if (!started.ok)
|
|
648
|
+
standingDiagnostics = started.diagnostics;
|
|
649
|
+
}
|
|
581
650
|
const filterMatchedIds = (query) => compiledWorkspace === undefined
|
|
582
651
|
? []
|
|
583
652
|
: matchedIdsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext);
|
|
@@ -844,6 +913,36 @@ export const startVisualServer = async (options) => {
|
|
|
844
913
|
for (const socket of connections.values())
|
|
845
914
|
sendFrame(socket, frame);
|
|
846
915
|
};
|
|
916
|
+
/**
|
|
917
|
+
* Tells the browser the workspace no longer compiles, and what said so.
|
|
918
|
+
*
|
|
919
|
+
* Journalled as an ordinary `diagnostic` response rather than a new frame
|
|
920
|
+
* kind, because that is the one the browser already renders - `Faults`
|
|
921
|
+
* reads `state.diagnostics`, which a `model` frame clears, so a recovered
|
|
922
|
+
* recompile clears the banner without anyone clearing it (#349). Held in
|
|
923
|
+
* `standingDiagnostics` too, for the browser that has not connected yet.
|
|
924
|
+
*/
|
|
925
|
+
const reportRecompileFailure = async (diagnostics, eventId = drawHex(16)) => {
|
|
926
|
+
standingDiagnostics = diagnostics;
|
|
927
|
+
const response = {
|
|
928
|
+
format: "yarramate/visual-response/v1",
|
|
929
|
+
sessionId,
|
|
930
|
+
responseId: drawHex(16),
|
|
931
|
+
eventId,
|
|
932
|
+
type: "diagnostic",
|
|
933
|
+
timestamp: stamp(),
|
|
934
|
+
payload: { diagnostics },
|
|
935
|
+
};
|
|
936
|
+
const appended = await appendVisualResponse(paths, response);
|
|
937
|
+
if (appended.ok) {
|
|
938
|
+
transcriptBytes = appended.transcriptBytes;
|
|
939
|
+
recordResponse(response);
|
|
940
|
+
broadcast({ kind: "response", response });
|
|
941
|
+
}
|
|
942
|
+
else if (appended.freeze !== undefined) {
|
|
943
|
+
freeze(appended.freeze);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
847
946
|
const idleDelivery = () => ({
|
|
848
947
|
waiting: true,
|
|
849
948
|
lastSequence,
|
|
@@ -1165,8 +1264,17 @@ export const startVisualServer = async (options) => {
|
|
|
1165
1264
|
kind: "apply-result",
|
|
1166
1265
|
result: { ok: false, diagnostics: refused },
|
|
1167
1266
|
});
|
|
1168
|
-
|
|
1267
|
+
const refreshed = recompileWorkspace();
|
|
1268
|
+
if (refreshed.ok) {
|
|
1169
1269
|
broadcast({ kind: "model", model: rendered, views });
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
// Nothing at all reached the browser here before (#349): a refused
|
|
1273
|
+
// apply whose refresh also failed left the reviewer with a
|
|
1274
|
+
// refusal about their rows and no word that the workspace itself
|
|
1275
|
+
// had stopped compiling.
|
|
1276
|
+
await reportRecompileFailure(refreshed.diagnostics);
|
|
1277
|
+
}
|
|
1170
1278
|
return;
|
|
1171
1279
|
}
|
|
1172
1280
|
const operationsSource = stringify({
|
|
@@ -1186,7 +1294,7 @@ export const startVisualServer = async (options) => {
|
|
|
1186
1294
|
kind: "apply-result",
|
|
1187
1295
|
result: {
|
|
1188
1296
|
ok: false,
|
|
1189
|
-
diagnostics: published(loadedWorkspace.diagnostics,
|
|
1297
|
+
diagnostics: published(loadedWorkspace.diagnostics, sourcesForDetail()),
|
|
1190
1298
|
},
|
|
1191
1299
|
});
|
|
1192
1300
|
return;
|
|
@@ -1225,7 +1333,7 @@ export const startVisualServer = async (options) => {
|
|
|
1225
1333
|
kind: "apply-result",
|
|
1226
1334
|
result: {
|
|
1227
1335
|
ok: false,
|
|
1228
|
-
diagnostics: published(outcome.diagnostics,
|
|
1336
|
+
diagnostics: published(outcome.diagnostics, sourcesForDetail()),
|
|
1229
1337
|
},
|
|
1230
1338
|
});
|
|
1231
1339
|
return;
|
|
@@ -1242,7 +1350,8 @@ export const startVisualServer = async (options) => {
|
|
|
1242
1350
|
kind: "apply-result",
|
|
1243
1351
|
result: { ok: true, result: outcome.result },
|
|
1244
1352
|
});
|
|
1245
|
-
|
|
1353
|
+
const recompiled = recompileWorkspace();
|
|
1354
|
+
if (recompiled.ok) {
|
|
1246
1355
|
broadcast({ kind: "model", model: rendered, views });
|
|
1247
1356
|
return;
|
|
1248
1357
|
}
|
|
@@ -1250,29 +1359,13 @@ export const startVisualServer = async (options) => {
|
|
|
1250
1359
|
// landed but produced a document Core itself can no longer parse.
|
|
1251
1360
|
// The freeze alone only tells the browser input is refused, not why
|
|
1252
1361
|
// the graph on screen has gone stale, so a diagnostic response is
|
|
1253
|
-
// journaled alongside it
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
timestamp: stamp(),
|
|
1261
|
-
payload: {
|
|
1262
|
-
diagnostics: [
|
|
1263
|
-
serverDiagnostic("YMVS310", "Workspace failed to recompile after a landed changeset"),
|
|
1264
|
-
],
|
|
1265
|
-
},
|
|
1266
|
-
};
|
|
1267
|
-
const appendedResponse = await appendVisualResponse(paths, diagnosticResponse);
|
|
1268
|
-
if (appendedResponse.ok) {
|
|
1269
|
-
transcriptBytes = appendedResponse.transcriptBytes;
|
|
1270
|
-
recordResponse(diagnosticResponse);
|
|
1271
|
-
broadcast({ kind: "response", response: diagnosticResponse });
|
|
1272
|
-
}
|
|
1273
|
-
else if (appendedResponse.freeze !== undefined) {
|
|
1274
|
-
freeze(appendedResponse.freeze);
|
|
1275
|
-
}
|
|
1362
|
+
// journaled alongside it - and it carries the compiler's OWN
|
|
1363
|
+
// diagnostics, because YMVS310 by itself names no document, no code
|
|
1364
|
+
// and no line (#349).
|
|
1365
|
+
await reportRecompileFailure([
|
|
1366
|
+
serverDiagnostic("YMVS310", "Workspace failed to recompile after a landed changeset"),
|
|
1367
|
+
...recompiled.diagnostics,
|
|
1368
|
+
], event.eventId);
|
|
1276
1369
|
freeze("recompile-failed");
|
|
1277
1370
|
return;
|
|
1278
1371
|
}
|
|
@@ -1424,6 +1517,22 @@ export const startVisualServer = async (options) => {
|
|
|
1424
1517
|
return;
|
|
1425
1518
|
}
|
|
1426
1519
|
sendFrame(socket, { kind: "ready", snapshot: snapshot() });
|
|
1520
|
+
// A recompile that failed before this socket existed - the one at
|
|
1521
|
+
// startup, most often - has had nobody to tell until now (#349).
|
|
1522
|
+
if (standingDiagnostics.length > 0) {
|
|
1523
|
+
sendFrame(socket, {
|
|
1524
|
+
kind: "response",
|
|
1525
|
+
response: {
|
|
1526
|
+
format: "yarramate/visual-response/v1",
|
|
1527
|
+
sessionId,
|
|
1528
|
+
responseId: drawHex(16),
|
|
1529
|
+
eventId: drawHex(16),
|
|
1530
|
+
type: "diagnostic",
|
|
1531
|
+
timestamp: stamp(),
|
|
1532
|
+
payload: { diagnostics: standingDiagnostics },
|
|
1533
|
+
},
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1427
1536
|
});
|
|
1428
1537
|
};
|
|
1429
1538
|
// ------------------------------------------------------------- agent routes
|
package/dist/apply-command.js
CHANGED
|
@@ -719,6 +719,10 @@ export const applyOperations = (input) => {
|
|
|
719
719
|
// single byte is written; any diagnostic rejects the entire batch.
|
|
720
720
|
const compiled = [
|
|
721
721
|
...resolvedWorkspace.profiles,
|
|
722
|
+
// Without patterns this gate compiles a workspace the manifest does not
|
|
723
|
+
// describe, so every commit against a workspace holding pattern instances
|
|
724
|
+
// was refused with YM419 for a pattern that was declared (#268).
|
|
725
|
+
...resolvedWorkspace.patterns,
|
|
722
726
|
...resolvedWorkspace.documents,
|
|
723
727
|
].map((path) => ({
|
|
724
728
|
path,
|
|
@@ -832,6 +836,9 @@ export const planOperations = (store, input) => {
|
|
|
832
836
|
// workspace that declares a profile died on it. No operation can target a
|
|
833
837
|
// profile, so they are read to be COMPILED against, never to be written.
|
|
834
838
|
...input.workspace.profiles,
|
|
839
|
+
// Patterns are compiled against too, and are no more writable than a
|
|
840
|
+
// profile: no operation can target one.
|
|
841
|
+
...input.workspace.patterns,
|
|
835
842
|
...input.workspace.documents,
|
|
836
843
|
...input.workspace.projections,
|
|
837
844
|
...input.workspace.evidence,
|
package/dist/ask-command.js
CHANGED
|
@@ -432,7 +432,11 @@ export function runAskCommand(options, cwd) {
|
|
|
432
432
|
return emit(result, `Workspace ${workspace.id}: check failing\n` +
|
|
433
433
|
`Diagnostics: ${plural(checkPayload.diagnostics.length, 'error')}; run \`yarramate check ${workspacePath}\` for details\n`, 1);
|
|
434
434
|
}
|
|
435
|
-
const compilation = compileWorkspaceWithProfileContext([
|
|
435
|
+
const compilation = compileWorkspaceWithProfileContext([
|
|
436
|
+
...workspace.profiles,
|
|
437
|
+
...workspace.patterns,
|
|
438
|
+
...workspace.documents,
|
|
439
|
+
].map((path) => ({
|
|
436
440
|
path,
|
|
437
441
|
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
438
442
|
})));
|
|
@@ -467,7 +471,7 @@ export function runAskCommand(options, cwd) {
|
|
|
467
471
|
const loadedCatalogue = loadQuestionCatalogue({
|
|
468
472
|
path: shippedCataloguePath,
|
|
469
473
|
source: readFileSync(shippedCataloguePath, 'utf8'),
|
|
470
|
-
});
|
|
474
|
+
}, compilation.profileContext);
|
|
471
475
|
if (!loadedCatalogue.ok)
|
|
472
476
|
return failed(loadedCatalogue.diagnostics);
|
|
473
477
|
const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations));
|
|
@@ -514,7 +518,11 @@ export function runAskCommand(options, cwd) {
|
|
|
514
518
|
return emit(result, `${lines.join('\n')}\n`);
|
|
515
519
|
}
|
|
516
520
|
// Every other mode reads the compiled model directly.
|
|
517
|
-
const compilation = compileWorkspaceWithProfileContext([
|
|
521
|
+
const compilation = compileWorkspaceWithProfileContext([
|
|
522
|
+
...workspace.profiles,
|
|
523
|
+
...workspace.patterns,
|
|
524
|
+
...workspace.documents,
|
|
525
|
+
].map((path) => ({
|
|
518
526
|
path,
|
|
519
527
|
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
520
528
|
})));
|
|
@@ -693,7 +701,7 @@ export function runAskCommand(options, cwd) {
|
|
|
693
701
|
const loadedCatalogue = loadQuestionCatalogue({
|
|
694
702
|
path: cataloguePath ?? resolvedCataloguePath,
|
|
695
703
|
source: readFileSync(resolvedCataloguePath, 'utf8'),
|
|
696
|
-
});
|
|
704
|
+
}, compilation.profileContext);
|
|
697
705
|
if (!loadedCatalogue.ok)
|
|
698
706
|
return failed(loadedCatalogue.diagnostics);
|
|
699
707
|
// The evidence overlay rides along for the one condition that
|
|
@@ -988,7 +996,7 @@ export function runAskCommand(options, cwd) {
|
|
|
988
996
|
const loadedCatalogue = loadQuestionCatalogue({
|
|
989
997
|
path: cataloguePath ?? resolvedCataloguePath,
|
|
990
998
|
source: readFileSync(resolvedCataloguePath, 'utf8'),
|
|
991
|
-
});
|
|
999
|
+
}, compilation.profileContext);
|
|
992
1000
|
if (!loadedCatalogue.ok)
|
|
993
1001
|
return failed(loadedCatalogue.diagnostics);
|
|
994
1002
|
// Loaded ahead of evaluation so the overlay feeds the one condition
|
package/dist/check-command.js
CHANGED
|
@@ -274,7 +274,11 @@ export function runCheckCommand(options, cwd) {
|
|
|
274
274
|
if (ok && result.ok) {
|
|
275
275
|
const successfulCounts = counted;
|
|
276
276
|
const documentCount = result.graph.documents.length;
|
|
277
|
-
const
|
|
277
|
+
const patternCount = resolved.patterns.length;
|
|
278
|
+
// Everything in `coreSources` that is not a document was a profile until
|
|
279
|
+
// patterns joined the source list (#268), and counting them as profiles
|
|
280
|
+
// said "2 profiles" about a workspace with one.
|
|
281
|
+
const profileCount = coreSources.length - documentCount - patternCount;
|
|
278
282
|
const mappingCount = mappingSources.length;
|
|
279
283
|
const projectionCount = resolved.projections.length;
|
|
280
284
|
const evidenceCount = resolved.evidence.length;
|
|
@@ -286,6 +290,11 @@ export function runCheckCommand(options, cwd) {
|
|
|
286
290
|
`${profileCount} ${profileCount === 1 ? 'profile' : 'profiles'}`,
|
|
287
291
|
]
|
|
288
292
|
: []),
|
|
293
|
+
...(patternCount > 0
|
|
294
|
+
? [
|
|
295
|
+
`${patternCount} ${patternCount === 1 ? 'pattern' : 'patterns'}`,
|
|
296
|
+
]
|
|
297
|
+
: []),
|
|
289
298
|
...(mappingCount > 0
|
|
290
299
|
? [
|
|
291
300
|
`${mappingCount} ${mappingCount === 1 ? 'adapter mapping' : 'adapter mappings'}`,
|
package/dist/cli-support.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export declare const resolveCliWorkspaceSources: (paths: readonly string[], cwd:
|
|
|
28
28
|
readonly projections: readonly string[];
|
|
29
29
|
readonly evidence: readonly string[];
|
|
30
30
|
readonly contracts: readonly string[];
|
|
31
|
+
/** Pattern documents, which ride in `paths` and are not documents. */
|
|
32
|
+
readonly patterns: readonly string[];
|
|
31
33
|
} | {
|
|
32
34
|
readonly ok: false;
|
|
33
35
|
readonly diagnostics: readonly Diagnostic[];
|
package/dist/cli-support.js
CHANGED
|
@@ -50,6 +50,7 @@ export const resolveCliWorkspaceSources = (paths, cwd, options = {}) => {
|
|
|
50
50
|
projections: [],
|
|
51
51
|
evidence: [],
|
|
52
52
|
contracts: [],
|
|
53
|
+
patterns: [],
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
const manifestPath = paths[0];
|
|
@@ -60,6 +61,7 @@ export const resolveCliWorkspaceSources = (paths, cwd, options = {}) => {
|
|
|
60
61
|
projections: [],
|
|
61
62
|
evidence: [],
|
|
62
63
|
contracts: [],
|
|
64
|
+
patterns: [],
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
const source = readFileSync(resolve(cwd, manifestPath), 'utf8');
|
|
@@ -70,6 +72,7 @@ export const resolveCliWorkspaceSources = (paths, cwd, options = {}) => {
|
|
|
70
72
|
projections: [],
|
|
71
73
|
evidence: [],
|
|
72
74
|
contracts: [],
|
|
75
|
+
patterns: [],
|
|
73
76
|
};
|
|
74
77
|
}
|
|
75
78
|
const loaded = loadWorkspaceManifest({ path: manifestPath, source }, cwd);
|
|
@@ -78,6 +81,15 @@ export const resolveCliWorkspaceSources = (paths, cwd, options = {}) => {
|
|
|
78
81
|
ok: true,
|
|
79
82
|
paths: [
|
|
80
83
|
...loaded.workspace.profiles,
|
|
84
|
+
// Patterns are compiler input like profiles, and were resolved from
|
|
85
|
+
// the manifest without ever being handed over (#268): a pattern
|
|
86
|
+
// document a workspace declared was silently ignored by every verb,
|
|
87
|
+
// and an instance binding parts then failed YM419 for a pattern that
|
|
88
|
+
// was sitting right there in the manifest. Every test passed because
|
|
89
|
+
// each hands the compiler an explicit source list rather than
|
|
90
|
+
// resolving a workspace - the check that passes was not the check
|
|
91
|
+
// that mattered.
|
|
92
|
+
...loaded.workspace.patterns,
|
|
81
93
|
...loaded.workspace.documents,
|
|
82
94
|
...(options.includeAdapterMappings === true
|
|
83
95
|
? loaded.workspace.adapterMappings
|
|
@@ -86,6 +98,7 @@ export const resolveCliWorkspaceSources = (paths, cwd, options = {}) => {
|
|
|
86
98
|
projections: loaded.workspace.projections,
|
|
87
99
|
evidence: loaded.workspace.evidence,
|
|
88
100
|
contracts: loaded.workspace.contracts,
|
|
101
|
+
patterns: loaded.workspace.patterns,
|
|
89
102
|
}
|
|
90
103
|
: { ok: false, diagnostics: loaded.diagnostics };
|
|
91
104
|
};
|
package/dist/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ const runReconciliation = (options, cwd) => {
|
|
|
37
37
|
}
|
|
38
38
|
const sourcePaths = [
|
|
39
39
|
...loadedWorkspace.workspace.profiles,
|
|
40
|
+
...loadedWorkspace.workspace.patterns,
|
|
40
41
|
...loadedWorkspace.workspace.documents,
|
|
41
42
|
];
|
|
42
43
|
const compilation = compileWorkspace(sourcePaths.map((path) => ({
|
package/dist/design-command.js
CHANGED
|
@@ -191,18 +191,26 @@ export function runDesignCommand(options, cwd) {
|
|
|
191
191
|
const resolvedCataloguePath = cataloguePath === undefined
|
|
192
192
|
? shippedCataloguePath
|
|
193
193
|
: resolve(cwd, cataloguePath);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
// Compiled BEFORE the catalogue loads, so the catalogue can be checked
|
|
195
|
+
// against the vocabulary its kinds are written against (#351). A
|
|
196
|
+
// catalogue naming a kind its own profile does not have loads clean
|
|
197
|
+
// otherwise, and the question it names is dead on arrival.
|
|
198
|
+
const compilation = compileWorkspaceWithProfileContext([
|
|
199
|
+
...workspace.profiles,
|
|
200
|
+
...workspace.patterns,
|
|
201
|
+
...workspace.documents,
|
|
202
|
+
].map((path) => ({
|
|
201
203
|
path,
|
|
202
204
|
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
203
205
|
})));
|
|
204
206
|
if (!compilation.ok)
|
|
205
207
|
return failed(compilation.diagnostics);
|
|
208
|
+
const loadedCatalogue = loadQuestionCatalogue({
|
|
209
|
+
path: cataloguePath ?? resolvedCataloguePath,
|
|
210
|
+
source: readFileSync(resolvedCataloguePath, 'utf8'),
|
|
211
|
+
}, compilation.profileContext);
|
|
212
|
+
if (!loadedCatalogue.ok)
|
|
213
|
+
return failed(loadedCatalogue.diagnostics);
|
|
206
214
|
// The evidence overlay rides along for the one condition that reads
|
|
207
215
|
// it (unchallenged-evidence). A workspace declaring no evidence
|
|
208
216
|
// passes an empty overlay — known to be empty, which keeps that
|
|
@@ -273,8 +281,19 @@ export function runDesignCommand(options, cwd) {
|
|
|
273
281
|
stderr: '',
|
|
274
282
|
};
|
|
275
283
|
}
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
// Read from the REPORT rather than from `progress`, because a wave that
|
|
285
|
+
// has not opened and one with nothing outstanding both count zero (#334).
|
|
286
|
+
// "implementation 0 open" about a gated wave is the same empty-set
|
|
287
|
+
// flattery as the completion sentence below it - which was fixed while
|
|
288
|
+
// this line, directly above it, was not. `progress` keeps its shape: a
|
|
289
|
+
// consumer wanting wave state reads an interrogation report, and a third
|
|
290
|
+
// required field on a published format for a convenience summary is not
|
|
291
|
+
// worth the constructor break.
|
|
292
|
+
const waveSummary = report.waves
|
|
293
|
+
.map((wave) => {
|
|
294
|
+
const open = result.progress.waves.find(({ id }) => id === wave.id)?.open ?? 0;
|
|
295
|
+
return wave.opened ? `${wave.id} ${open} open` : `${wave.id} not yet`;
|
|
296
|
+
})
|
|
278
297
|
.join(' · ');
|
|
279
298
|
const lines = [
|
|
280
299
|
`Design interview — workspace ${workspace.id} · catalogue ${report.catalogue}`,
|
package/dist/export-command.js
CHANGED
|
@@ -149,7 +149,11 @@ export function runExportCommand(options, cwd) {
|
|
|
149
149
|
if (!loadedWorkspace.ok)
|
|
150
150
|
return failed(loadedWorkspace.diagnostics);
|
|
151
151
|
const workspace = loadedWorkspace.workspace;
|
|
152
|
-
const compilation = compileWorkspaceWithProfileContext([
|
|
152
|
+
const compilation = compileWorkspaceWithProfileContext([
|
|
153
|
+
...workspace.profiles,
|
|
154
|
+
...workspace.patterns,
|
|
155
|
+
...workspace.documents,
|
|
156
|
+
].map((path) => ({
|
|
153
157
|
path,
|
|
154
158
|
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
155
159
|
})));
|
|
@@ -203,5 +203,5 @@ export type CatalogueLoadResult = {
|
|
|
203
203
|
readonly ok: false;
|
|
204
204
|
readonly diagnostics: readonly Diagnostic[];
|
|
205
205
|
};
|
|
206
|
-
export declare function loadQuestionCatalogue(catalogueSource: WorkspaceSource): CatalogueLoadResult;
|
|
206
|
+
export declare function loadQuestionCatalogue(catalogueSource: WorkspaceSource, profileContext?: ResolvedProfileContext): CatalogueLoadResult;
|
|
207
207
|
export declare function renderInterrogationReport(report: InterrogationReport): string;
|
|
@@ -527,9 +527,97 @@ export function evaluateCatalogue(catalogue, graph, profileContext, evidence) {
|
|
|
527
527
|
waves,
|
|
528
528
|
};
|
|
529
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* Every qualified kind a catalogue names, from all three fields that carry
|
|
532
|
+
* one.
|
|
533
|
+
*
|
|
534
|
+
* All three die the same way when the kind does not resolve, and two of them
|
|
535
|
+
* are easy to forget. A trigger's kind never matches, so the question never
|
|
536
|
+
* opens. A subject selector's kind selects nothing, so the question is scoped
|
|
537
|
+
* to an empty set. A wave gate's kind never holds, so after #334 the whole
|
|
538
|
+
* wave never opens and carries no questions at all - one typo silently
|
|
539
|
+
* retiring a wave (#351).
|
|
540
|
+
*/
|
|
541
|
+
const kindReferencesOf = (catalogue) => {
|
|
542
|
+
const found = [];
|
|
543
|
+
const fromCondition = (condition, path) => {
|
|
544
|
+
if (typeof condition !== 'object' || condition === null)
|
|
545
|
+
return;
|
|
546
|
+
for (const field of ['kinds', 'counterpartKinds']) {
|
|
547
|
+
const value = condition[field];
|
|
548
|
+
if (!Array.isArray(value))
|
|
549
|
+
continue;
|
|
550
|
+
value.forEach((kind, index) => {
|
|
551
|
+
if (typeof kind === 'string')
|
|
552
|
+
found.push({ kind, path: [...path, field, index] });
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
catalogue.waves.forEach((wave, waveIndex) => {
|
|
557
|
+
;
|
|
558
|
+
(wave.opensWhen ?? []).forEach((condition, conditionIndex) => {
|
|
559
|
+
fromCondition(condition, ['waves', waveIndex, 'opensWhen', conditionIndex]);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
catalogue.questions.forEach((question, questionIndex) => {
|
|
563
|
+
;
|
|
564
|
+
(question.subjects?.kinds ?? []).forEach((kind, index) => {
|
|
565
|
+
found.push({
|
|
566
|
+
kind,
|
|
567
|
+
path: ['questions', questionIndex, 'subjects', 'kinds', index],
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
question.trigger.forEach((condition, conditionIndex) => {
|
|
571
|
+
fromCondition(condition, [
|
|
572
|
+
'questions',
|
|
573
|
+
questionIndex,
|
|
574
|
+
'trigger',
|
|
575
|
+
conditionIndex,
|
|
576
|
+
]);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
return found;
|
|
580
|
+
};
|
|
581
|
+
/**
|
|
582
|
+
* Kinds this catalogue names that the profile they belong to does not have.
|
|
583
|
+
*
|
|
584
|
+
* The check is deliberately narrow, and the narrowness is the design (#351).
|
|
585
|
+
* A kind is reported ONLY when its profile is loaded and the kind is absent
|
|
586
|
+
* from it, which is unambiguously a typo. A kind whose profile is not loaded
|
|
587
|
+
* at all is left alone, because that is a legitimately dormant cross-profile
|
|
588
|
+
* question rather than a mistake: `core-enrichment` names four
|
|
589
|
+
* `yarramate/policy@0.1` constraint kinds, and `yarramate/policy@0.1` loads
|
|
590
|
+
* only when a document selects it or a profile extends it. Reporting those
|
|
591
|
+
* four would put four false positives on the catalogue this repository ships,
|
|
592
|
+
* and a check that cries wolf on its own catalogue gets turned off.
|
|
593
|
+
*
|
|
594
|
+
* Resolution is tested against the kind maps rather than a declared-kinds
|
|
595
|
+
* list, so a kind inherited through `extends` counts. A profile that declares
|
|
596
|
+
* no kinds of its own and inherits every one of them is the case a
|
|
597
|
+
* declared-kinds check would call entirely missing.
|
|
598
|
+
*/
|
|
599
|
+
const unresolvableKinds = (catalogue, profileContext) => {
|
|
600
|
+
const known = new Set([
|
|
601
|
+
...profileContext.conceptKindLineages.keys(),
|
|
602
|
+
...profileContext.relationshipKindLineages.keys(),
|
|
603
|
+
]);
|
|
604
|
+
const loadedProfiles = new Set();
|
|
605
|
+
for (const identity of known) {
|
|
606
|
+
const hash = identity.indexOf('#');
|
|
607
|
+
if (hash > 0)
|
|
608
|
+
loadedProfiles.add(identity.slice(0, hash));
|
|
609
|
+
}
|
|
610
|
+
return kindReferencesOf(catalogue).filter(({ kind }) => {
|
|
611
|
+
if (known.has(kind))
|
|
612
|
+
return false;
|
|
613
|
+
const hash = kind.indexOf('#');
|
|
614
|
+
// Profile absent entirely: dormant, not wrong.
|
|
615
|
+
return hash > 0 && loadedProfiles.has(kind.slice(0, hash));
|
|
616
|
+
});
|
|
617
|
+
};
|
|
530
618
|
// Shared by interrogate and design: schema validation plus the YM911
|
|
531
619
|
// undeclared-wave check, both source-located against the catalogue file.
|
|
532
|
-
export function loadQuestionCatalogue(catalogueSource) {
|
|
620
|
+
export function loadQuestionCatalogue(catalogueSource, profileContext) {
|
|
533
621
|
const loadedCatalogue = loadSourceDocument(catalogueSource, validateCatalogue, 'Question catalogue');
|
|
534
622
|
if (!loadedCatalogue.ok) {
|
|
535
623
|
return { ok: false, diagnostics: loadedCatalogue.diagnostics };
|
|
@@ -549,6 +637,20 @@ export function loadQuestionCatalogue(catalogueSource) {
|
|
|
549
637
|
if (waveDiagnostics.length > 0) {
|
|
550
638
|
return { ok: false, diagnostics: waveDiagnostics };
|
|
551
639
|
}
|
|
640
|
+
// Only when a caller has a compiled workspace to check against. Without one
|
|
641
|
+
// there is no way to tell a typo from a kind whose profile simply is not
|
|
642
|
+
// here, and guessing would be the false positive this check exists to avoid.
|
|
643
|
+
const kindDiagnostics = profileContext === undefined
|
|
644
|
+
? []
|
|
645
|
+
: unresolvableKinds(catalogue, profileContext).map(({ kind, path }) => ({
|
|
646
|
+
severity: 'error',
|
|
647
|
+
code: 'YM914',
|
|
648
|
+
message: `Kind "${kind}" is not declared by profile "${kind.slice(0, kind.indexOf('#'))}", which this workspace loads, so the question can never fire`,
|
|
649
|
+
...locateSourcePath(catalogueSource.path, loadedCatalogue.document.yaml, loadedCatalogue.document.lineCounter, path, `/${path.join('/')}`),
|
|
650
|
+
}));
|
|
651
|
+
if (kindDiagnostics.length > 0) {
|
|
652
|
+
return { ok: false, diagnostics: kindDiagnostics };
|
|
653
|
+
}
|
|
552
654
|
return { ok: true, catalogue };
|
|
553
655
|
}
|
|
554
656
|
// Shared by interrogate and `ask --open`: the wave-by-wave human report.
|