eventmodeler 0.6.11 → 0.6.13
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/index.js +1831 -31
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -92,11 +92,11 @@ var require_re = __commonJS((exports, module) => {
|
|
|
92
92
|
}
|
|
93
93
|
return value;
|
|
94
94
|
};
|
|
95
|
-
var createToken = (
|
|
95
|
+
var createToken = (name2, value, isGlobal) => {
|
|
96
96
|
const safe = makeSafeRegex(value);
|
|
97
97
|
const index = R++;
|
|
98
|
-
debug(
|
|
99
|
-
t[
|
|
98
|
+
debug(name2, index, value);
|
|
99
|
+
t[name2] = index;
|
|
100
100
|
src[index] = value;
|
|
101
101
|
safeSrc[index] = safe;
|
|
102
102
|
re[index] = new RegExp(value, isGlobal ? "g" : undefined);
|
|
@@ -1815,8 +1815,8 @@ var require_semver2 = __commonJS((exports, module) => {
|
|
|
1815
1815
|
});
|
|
1816
1816
|
|
|
1817
1817
|
// src/index.ts
|
|
1818
|
-
import * as
|
|
1819
|
-
import * as
|
|
1818
|
+
import * as fs6 from "node:fs";
|
|
1819
|
+
import * as path6 from "node:path";
|
|
1820
1820
|
import { fileURLToPath } from "node:url";
|
|
1821
1821
|
import { Command } from "commander";
|
|
1822
1822
|
|
|
@@ -2281,7 +2281,8 @@ function loadProjectConfig(startDir) {
|
|
|
2281
2281
|
const content = fs2.readFileSync(configPath, "utf-8");
|
|
2282
2282
|
const config = JSON.parse(content);
|
|
2283
2283
|
if (config.type === "cloud" && config.modelId && config.modelName) {
|
|
2284
|
-
|
|
2284
|
+
const mode = config.mode === "sheets" || config.mode === "freeform" ? config.mode : undefined;
|
|
2285
|
+
return { type: "cloud", modelId: config.modelId, modelName: config.modelName, mode };
|
|
2285
2286
|
}
|
|
2286
2287
|
return null;
|
|
2287
2288
|
} catch {
|
|
@@ -2291,10 +2292,19 @@ function loadProjectConfig(startDir) {
|
|
|
2291
2292
|
function saveProjectConfig(config, targetDir = process.cwd()) {
|
|
2292
2293
|
const configPath = path2.join(targetDir, PROJECT_CONFIG_FILENAME);
|
|
2293
2294
|
const content = { type: "cloud", modelId: config.modelId, modelName: config.modelName };
|
|
2295
|
+
if (config.mode)
|
|
2296
|
+
content.mode = config.mode;
|
|
2294
2297
|
fs2.writeFileSync(configPath, JSON.stringify(content, null, 2) + `
|
|
2295
2298
|
`);
|
|
2296
2299
|
return configPath;
|
|
2297
2300
|
}
|
|
2301
|
+
function removeProjectConfig(startDir) {
|
|
2302
|
+
const configPath = findProjectConfigPath(startDir);
|
|
2303
|
+
if (!configPath)
|
|
2304
|
+
return null;
|
|
2305
|
+
fs2.rmSync(configPath);
|
|
2306
|
+
return configPath;
|
|
2307
|
+
}
|
|
2298
2308
|
function getProjectRoot(startDir = process.cwd()) {
|
|
2299
2309
|
const configPath = findProjectConfigPath(startDir);
|
|
2300
2310
|
if (configPath) {
|
|
@@ -2480,7 +2490,7 @@ async function init() {
|
|
|
2480
2490
|
} else {
|
|
2481
2491
|
console.log("Your models:");
|
|
2482
2492
|
models.forEach((m, i) => {
|
|
2483
|
-
console.log(` ${i + 1}. ${m.name} (${m.role}, ${m.visibility})`);
|
|
2493
|
+
console.log(` ${i + 1}. ${m.name} (${m.role}, ${m.visibility}, ${m.mode ?? "freeform"})`);
|
|
2484
2494
|
});
|
|
2485
2495
|
console.log(` n. Create a new model`);
|
|
2486
2496
|
const answer = (await ask(`
|
|
@@ -2494,16 +2504,17 @@ Pick a model [1]: `)).trim() || "1";
|
|
|
2494
2504
|
process.exit(2);
|
|
2495
2505
|
}
|
|
2496
2506
|
const m = models[idx - 1];
|
|
2497
|
-
picked = { id: m.id, name: m.name };
|
|
2507
|
+
picked = { id: m.id, name: m.name, mode: m.mode ?? "freeform" };
|
|
2498
2508
|
}
|
|
2499
2509
|
}
|
|
2500
2510
|
const configPath = saveProjectConfig({
|
|
2501
2511
|
type: "cloud",
|
|
2502
2512
|
modelId: picked.id,
|
|
2503
|
-
modelName: picked.name
|
|
2513
|
+
modelName: picked.name,
|
|
2514
|
+
mode: picked.mode ?? "freeform"
|
|
2504
2515
|
});
|
|
2505
2516
|
console.log(`
|
|
2506
|
-
Linked to model "${picked.name}" (${picked.id}).`);
|
|
2517
|
+
Linked to model "${picked.name}" (${picked.id}, ${picked.mode ?? "freeform"}).`);
|
|
2507
2518
|
console.log(`Wrote ${configPath}.`);
|
|
2508
2519
|
} finally {
|
|
2509
2520
|
rl.close();
|
|
@@ -2525,13 +2536,41 @@ async function createModel(ask) {
|
|
|
2525
2536
|
method: "POST",
|
|
2526
2537
|
json: { name, visibility: visAnswer }
|
|
2527
2538
|
});
|
|
2528
|
-
return { id: created.id, name: created.name };
|
|
2539
|
+
return { id: created.id, name: created.name, mode: "freeform" };
|
|
2529
2540
|
} catch (err) {
|
|
2530
2541
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2531
2542
|
console.error(`Failed to create model: ${msg}`);
|
|
2532
2543
|
process.exit(1);
|
|
2533
2544
|
}
|
|
2534
2545
|
}
|
|
2546
|
+
|
|
2547
|
+
// src/commands/status.ts
|
|
2548
|
+
function registerStatusCommands(program, conn) {
|
|
2549
|
+
program.command("status").description("Show the current connection: which model, which mode, which command surface").option("--model <id>", "Report the state for this model instead of .eventmodeler.json").action(() => {
|
|
2550
|
+
if (conn.state === "disconnected") {
|
|
2551
|
+
console.log("Not connected. Run `eventmodeler init` to link a model, or pass --model <id>.");
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
const name = conn.modelName ? `"${conn.modelName}" ` : "";
|
|
2555
|
+
const id = conn.modelId ?? "(unknown id)";
|
|
2556
|
+
if (conn.state === "unknown") {
|
|
2557
|
+
console.log(`Connected to ${name}${id} — mode could not be resolved; all commands are available.`);
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
const surface = conn.state === "sheets" ? "sheets surface (`sheet …`)" : "freeform surface (create/update/remove/show/…)";
|
|
2561
|
+
console.log(`Connected to ${name}${id}`);
|
|
2562
|
+
console.log(` mode: ${conn.state} — ${surface}`);
|
|
2563
|
+
if (conn.overridden)
|
|
2564
|
+
console.log(" (via --model override)");
|
|
2565
|
+
});
|
|
2566
|
+
program.command("disconnect").description("Unlink the current directory from its model (removes .eventmodeler.json)").action(() => {
|
|
2567
|
+
const removed = removeProjectConfig();
|
|
2568
|
+
if (removed)
|
|
2569
|
+
console.log(`Disconnected — removed ${removed}.`);
|
|
2570
|
+
else
|
|
2571
|
+
console.log("Not connected (no .eventmodeler.json found).");
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2535
2574
|
// ../packages/canvas-model/src/build.ts
|
|
2536
2575
|
import * as Y2 from "yjs";
|
|
2537
2576
|
function buildYArray(items) {
|
|
@@ -2577,6 +2616,10 @@ function getEntry(doc, scope, id) {
|
|
|
2577
2616
|
return entry instanceof Y4.AbstractType ? entry.toJSON() : entry;
|
|
2578
2617
|
}
|
|
2579
2618
|
// ../packages/canvas-model/src/dimensions.ts
|
|
2619
|
+
var GRID_SIZE = 20;
|
|
2620
|
+
function snap(value) {
|
|
2621
|
+
return Math.round(value / GRID_SIZE) * GRID_SIZE;
|
|
2622
|
+
}
|
|
2580
2623
|
var ELEMENT_DIMENSIONS = {
|
|
2581
2624
|
commandSticky: { width: 160, height: 100 },
|
|
2582
2625
|
eventSticky: { width: 160, height: 100 },
|
|
@@ -2605,6 +2648,35 @@ function canonicalSizeForScope(scope) {
|
|
|
2605
2648
|
}
|
|
2606
2649
|
// ../packages/canvas-model/src/actions.ts
|
|
2607
2650
|
import * as Y5 from "yjs";
|
|
2651
|
+
|
|
2652
|
+
// ../packages/canvas-model/src/cascade.ts
|
|
2653
|
+
var FLOWS_SCOPE = "flows";
|
|
2654
|
+
var SCENARIOS_SCOPE = "scenarios";
|
|
2655
|
+
var SCENARIO_SECTIONS = ["givenEntries", "whenEntries", "thenEntries"];
|
|
2656
|
+
function cascadeStickyRemoval(doc, removedIds) {
|
|
2657
|
+
if (removedIds.size === 0)
|
|
2658
|
+
return;
|
|
2659
|
+
const flows = getScopeMap(doc, FLOWS_SCOPE);
|
|
2660
|
+
for (const [fid, f] of flows.entries()) {
|
|
2661
|
+
const src = f.get("sourceId");
|
|
2662
|
+
const tgt = f.get("targetId");
|
|
2663
|
+
if (removedIds.has(src) || removedIds.has(tgt))
|
|
2664
|
+
flows.delete(fid);
|
|
2665
|
+
}
|
|
2666
|
+
const scenarios = getScopeMap(doc, SCENARIOS_SCOPE);
|
|
2667
|
+
for (const scenario of scenarios.values()) {
|
|
2668
|
+
for (const key of SCENARIO_SECTIONS) {
|
|
2669
|
+
const arr = scenario.get(key);
|
|
2670
|
+
if (!arr)
|
|
2671
|
+
continue;
|
|
2672
|
+
for (let i = arr.length - 1;i >= 0; i--) {
|
|
2673
|
+
const stickyId = arr.get(i).get("stickyId");
|
|
2674
|
+
if (stickyId && removedIds.has(stickyId))
|
|
2675
|
+
arr.delete(i, 1);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2608
2680
|
// ../packages/canvas-model/src/containment.ts
|
|
2609
2681
|
function centerOf(bbox) {
|
|
2610
2682
|
return { x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2 };
|
|
@@ -2630,7 +2702,7 @@ function innermostContainer(containers, point) {
|
|
|
2630
2702
|
return best;
|
|
2631
2703
|
}
|
|
2632
2704
|
// ../packages/canvas-model/src/completeness.ts
|
|
2633
|
-
var
|
|
2705
|
+
var FLOWS_SCOPE2 = "flows";
|
|
2634
2706
|
var SLICES_SCOPE = "slices";
|
|
2635
2707
|
var AGGREGATES_SCOPE = "aggregates";
|
|
2636
2708
|
var FIELD_BEARING_SCOPES = [
|
|
@@ -2678,7 +2750,7 @@ function targetIdSet(target) {
|
|
|
2678
2750
|
}
|
|
2679
2751
|
function inboundFlows(doc, target) {
|
|
2680
2752
|
const ids = targetIdSet(target);
|
|
2681
|
-
return getEntries(doc,
|
|
2753
|
+
return getEntries(doc, FLOWS_SCOPE2).filter((f) => ids.has(f.targetId));
|
|
2682
2754
|
}
|
|
2683
2755
|
function untracedFields(doc, target) {
|
|
2684
2756
|
const fields = target.fields ?? [];
|
|
@@ -2756,13 +2828,19 @@ var baseContainer = (idKey) => NamedBox.extend({
|
|
|
2756
2828
|
});
|
|
2757
2829
|
var SliceStatusSchema = z.enum(["draft", "created", "planned", "in-progress", "blocked", "done"]);
|
|
2758
2830
|
var SliceSchema = baseContainer("sliceId").extend({
|
|
2759
|
-
status: SliceStatusSchema.optional()
|
|
2831
|
+
status: SliceStatusSchema.optional(),
|
|
2832
|
+
note: z.string().optional()
|
|
2760
2833
|
});
|
|
2761
2834
|
var AggregateSchema = baseContainer("aggregateId");
|
|
2762
2835
|
var ActorSchema = baseContainer("actorId");
|
|
2763
2836
|
var ChapterSchema = baseContainer("chapterId");
|
|
2764
2837
|
var ContextSchema = baseContainer("contextId");
|
|
2765
|
-
var
|
|
2838
|
+
var LaneKindSchema = z.enum(["actor", "interaction", "swimlane", "specification"]);
|
|
2839
|
+
var SwimLaneSchema = baseContainer("swimLaneId").extend({
|
|
2840
|
+
laneKind: LaneKindSchema.optional(),
|
|
2841
|
+
note: z.string().optional(),
|
|
2842
|
+
color: z.string().optional()
|
|
2843
|
+
});
|
|
2766
2844
|
var NoteSchema = Box.extend({
|
|
2767
2845
|
noteId: z.string().uuid().optional(),
|
|
2768
2846
|
modelId: z.string().uuid().optional(),
|
|
@@ -2828,7 +2906,8 @@ var FlowTypeSchema = z.enum([
|
|
|
2828
2906
|
"ScreenToCommand",
|
|
2829
2907
|
"ProcessorToCommand",
|
|
2830
2908
|
"CommandToExternalEvent",
|
|
2831
|
-
"ExternalEventToProcessor"
|
|
2909
|
+
"ExternalEventToProcessor",
|
|
2910
|
+
"ExternalEventToScreen"
|
|
2832
2911
|
]);
|
|
2833
2912
|
var FlowHandleSchema = z.enum([
|
|
2834
2913
|
"top-source",
|
|
@@ -2855,6 +2934,32 @@ var FlowSchema = z.object({
|
|
|
2855
2934
|
targetHandle: FlowHandleSchema.optional(),
|
|
2856
2935
|
mappings: z.array(FlowMappingSchema).optional()
|
|
2857
2936
|
});
|
|
2937
|
+
var SheetFlowSchema = z.object({
|
|
2938
|
+
sheetFlowId: z.string().uuid().optional(),
|
|
2939
|
+
modelId: z.string().uuid().optional(),
|
|
2940
|
+
sourceChapterId: z.string().uuid(),
|
|
2941
|
+
sourceHandle: z.string().min(1),
|
|
2942
|
+
targetChapterId: z.string().uuid(),
|
|
2943
|
+
targetHandle: z.string().min(1)
|
|
2944
|
+
});
|
|
2945
|
+
var CellRefSchema = z.object({
|
|
2946
|
+
stickyId: z.string().uuid(),
|
|
2947
|
+
scope: z.enum([
|
|
2948
|
+
"commands",
|
|
2949
|
+
"events",
|
|
2950
|
+
"readModels",
|
|
2951
|
+
"screens",
|
|
2952
|
+
"processors",
|
|
2953
|
+
"externalEvents",
|
|
2954
|
+
"scenarios"
|
|
2955
|
+
])
|
|
2956
|
+
});
|
|
2957
|
+
var SheetStructureSchema = z.object({
|
|
2958
|
+
columns: z.array(z.string().uuid()),
|
|
2959
|
+
rows: z.array(z.string().uuid()),
|
|
2960
|
+
cells: z.record(z.string(), z.array(CellRefSchema)),
|
|
2961
|
+
cellNotes: z.record(z.string(), z.string()).optional()
|
|
2962
|
+
});
|
|
2858
2963
|
var SCOPE_SCHEMAS = {
|
|
2859
2964
|
commands: CommandSchema,
|
|
2860
2965
|
events: EventSchema,
|
|
@@ -2869,7 +2974,8 @@ var SCOPE_SCHEMAS = {
|
|
|
2869
2974
|
contexts: ContextSchema,
|
|
2870
2975
|
swimLanes: SwimLaneSchema,
|
|
2871
2976
|
notes: NoteSchema,
|
|
2872
|
-
flows: FlowSchema
|
|
2977
|
+
flows: FlowSchema,
|
|
2978
|
+
sheetFlows: SheetFlowSchema
|
|
2873
2979
|
};
|
|
2874
2980
|
function isValidatedScope(scope) {
|
|
2875
2981
|
return scope in SCOPE_SCHEMAS;
|
|
@@ -2893,6 +2999,519 @@ function pickSchema(scope, data) {
|
|
|
2893
2999
|
}
|
|
2894
3000
|
return SCOPE_SCHEMAS[scope];
|
|
2895
3001
|
}
|
|
3002
|
+
// ../packages/canvas-model/src/grid.ts
|
|
3003
|
+
import * as Y6 from "yjs";
|
|
3004
|
+
var GRID_SCOPE = "grid";
|
|
3005
|
+
function getGridMap(doc) {
|
|
3006
|
+
return doc.getMap(GRID_SCOPE);
|
|
3007
|
+
}
|
|
3008
|
+
function cellKey(sliceId, swimLaneId) {
|
|
3009
|
+
return `${sliceId}:${swimLaneId}`;
|
|
3010
|
+
}
|
|
3011
|
+
function resolveSheet(doc, chapterId) {
|
|
3012
|
+
const grid = getGridMap(doc);
|
|
3013
|
+
let sheet = grid.get(chapterId);
|
|
3014
|
+
if (!sheet) {
|
|
3015
|
+
sheet = new Y6.Map;
|
|
3016
|
+
sheet.set("columns", new Y6.Array);
|
|
3017
|
+
sheet.set("rows", new Y6.Array);
|
|
3018
|
+
sheet.set("cells", new Y6.Map);
|
|
3019
|
+
grid.set(chapterId, sheet);
|
|
3020
|
+
}
|
|
3021
|
+
return {
|
|
3022
|
+
sheet,
|
|
3023
|
+
columns: sheet.get("columns"),
|
|
3024
|
+
rows: sheet.get("rows"),
|
|
3025
|
+
cells: sheet.get("cells")
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
function ensureSheet(doc, chapterId) {
|
|
3029
|
+
doc.transact(() => {
|
|
3030
|
+
resolveSheet(doc, chapterId);
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
function readSheet(doc, chapterId) {
|
|
3034
|
+
const sheet = getGridMap(doc).get(chapterId);
|
|
3035
|
+
if (!sheet)
|
|
3036
|
+
return;
|
|
3037
|
+
return sheet.toJSON();
|
|
3038
|
+
}
|
|
3039
|
+
function clampIndex(at, length) {
|
|
3040
|
+
if (at === undefined || at > length)
|
|
3041
|
+
return length;
|
|
3042
|
+
return at < 0 ? 0 : at;
|
|
3043
|
+
}
|
|
3044
|
+
function insertUnique(arr, value, at) {
|
|
3045
|
+
if (arr.toArray().includes(value))
|
|
3046
|
+
return;
|
|
3047
|
+
arr.insert(clampIndex(at, arr.length), [value]);
|
|
3048
|
+
}
|
|
3049
|
+
function addColumn(doc, chapterId, sliceId, at) {
|
|
3050
|
+
doc.transact(() => {
|
|
3051
|
+
const { columns } = resolveSheet(doc, chapterId);
|
|
3052
|
+
insertUnique(columns, sliceId, at);
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
function moveColumn(doc, chapterId, sliceId, toIndex) {
|
|
3056
|
+
doc.transact(() => {
|
|
3057
|
+
const { columns } = resolveSheet(doc, chapterId);
|
|
3058
|
+
const from = columns.toArray().indexOf(sliceId);
|
|
3059
|
+
if (from === -1)
|
|
3060
|
+
return;
|
|
3061
|
+
columns.delete(from, 1);
|
|
3062
|
+
columns.insert(clampIndex(toIndex, columns.length), [sliceId]);
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
function removeColumn(doc, chapterId, sliceId) {
|
|
3066
|
+
doc.transact(() => {
|
|
3067
|
+
const sheet = getGridMap(doc).get(chapterId);
|
|
3068
|
+
if (!sheet)
|
|
3069
|
+
return;
|
|
3070
|
+
const columns = sheet.get("columns");
|
|
3071
|
+
const cells = sheet.get("cells");
|
|
3072
|
+
const cellNotes = sheet.get("cellNotes");
|
|
3073
|
+
const idx = columns.toArray().indexOf(sliceId);
|
|
3074
|
+
if (idx !== -1)
|
|
3075
|
+
columns.delete(idx, 1);
|
|
3076
|
+
for (const key of [...cells.keys()]) {
|
|
3077
|
+
if (key.startsWith(`${sliceId}:`))
|
|
3078
|
+
cells.delete(key);
|
|
3079
|
+
}
|
|
3080
|
+
for (const key of [...cellNotes?.keys() ?? []]) {
|
|
3081
|
+
if (key.startsWith(`${sliceId}:`))
|
|
3082
|
+
cellNotes.delete(key);
|
|
3083
|
+
}
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
function addRow(doc, chapterId, swimLaneId, at) {
|
|
3087
|
+
doc.transact(() => {
|
|
3088
|
+
const { rows } = resolveSheet(doc, chapterId);
|
|
3089
|
+
insertUnique(rows, swimLaneId, at);
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
function moveRow(doc, chapterId, swimLaneId, toIndex) {
|
|
3093
|
+
doc.transact(() => {
|
|
3094
|
+
const { rows } = resolveSheet(doc, chapterId);
|
|
3095
|
+
const from = rows.toArray().indexOf(swimLaneId);
|
|
3096
|
+
if (from === -1)
|
|
3097
|
+
return;
|
|
3098
|
+
rows.delete(from, 1);
|
|
3099
|
+
rows.insert(clampIndex(toIndex, rows.length), [swimLaneId]);
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
function removeRow(doc, chapterId, swimLaneId) {
|
|
3103
|
+
doc.transact(() => {
|
|
3104
|
+
const sheet = getGridMap(doc).get(chapterId);
|
|
3105
|
+
if (!sheet)
|
|
3106
|
+
return;
|
|
3107
|
+
const rows = sheet.get("rows");
|
|
3108
|
+
const cells = sheet.get("cells");
|
|
3109
|
+
const cellNotes = sheet.get("cellNotes");
|
|
3110
|
+
const idx = rows.toArray().indexOf(swimLaneId);
|
|
3111
|
+
if (idx !== -1)
|
|
3112
|
+
rows.delete(idx, 1);
|
|
3113
|
+
for (const key of [...cells.keys()]) {
|
|
3114
|
+
if (key.endsWith(`:${swimLaneId}`))
|
|
3115
|
+
cells.delete(key);
|
|
3116
|
+
}
|
|
3117
|
+
for (const key of [...cellNotes?.keys() ?? []]) {
|
|
3118
|
+
if (key.endsWith(`:${swimLaneId}`))
|
|
3119
|
+
cellNotes.delete(key);
|
|
3120
|
+
}
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
function getCellArray(cells, key, create) {
|
|
3124
|
+
let arr = cells.get(key);
|
|
3125
|
+
if (!arr && create) {
|
|
3126
|
+
arr = new Y6.Array;
|
|
3127
|
+
cells.set(key, arr);
|
|
3128
|
+
}
|
|
3129
|
+
return arr;
|
|
3130
|
+
}
|
|
3131
|
+
function findStickyCell(cells, stickyId) {
|
|
3132
|
+
for (const key of cells.keys()) {
|
|
3133
|
+
const arr = cells.get(key);
|
|
3134
|
+
const index = arr.toArray().findIndex((m) => m.get("stickyId") === stickyId);
|
|
3135
|
+
if (index !== -1)
|
|
3136
|
+
return { key, index };
|
|
3137
|
+
}
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
function placeInCell(doc, chapterId, sliceId, swimLaneId, ref, at) {
|
|
3141
|
+
doc.transact(() => {
|
|
3142
|
+
const { cells } = resolveSheet(doc, chapterId);
|
|
3143
|
+
const existing = findStickyCell(cells, ref.stickyId);
|
|
3144
|
+
if (existing)
|
|
3145
|
+
cells.get(existing.key).delete(existing.index, 1);
|
|
3146
|
+
const arr = getCellArray(cells, cellKey(sliceId, swimLaneId), true);
|
|
3147
|
+
arr.insert(clampIndex(at, arr.length), [
|
|
3148
|
+
(() => {
|
|
3149
|
+
const m = new Y6.Map;
|
|
3150
|
+
m.set("stickyId", ref.stickyId);
|
|
3151
|
+
m.set("scope", ref.scope);
|
|
3152
|
+
return m;
|
|
3153
|
+
})()
|
|
3154
|
+
]);
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
function removeSticky(doc, chapterId, stickyId) {
|
|
3158
|
+
doc.transact(() => {
|
|
3159
|
+
const sheet = getGridMap(doc).get(chapterId);
|
|
3160
|
+
if (!sheet)
|
|
3161
|
+
return;
|
|
3162
|
+
const cells = sheet.get("cells");
|
|
3163
|
+
const loc = findStickyCell(cells, stickyId);
|
|
3164
|
+
if (!loc)
|
|
3165
|
+
return;
|
|
3166
|
+
cells.get(loc.key).delete(loc.index, 1);
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
function removeSheet(doc, chapterId) {
|
|
3170
|
+
doc.transact(() => {
|
|
3171
|
+
getGridMap(doc).delete(chapterId);
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
function getCellNotesMap(doc, chapterId, create) {
|
|
3175
|
+
const sheet = getGridMap(doc).get(chapterId);
|
|
3176
|
+
if (!sheet)
|
|
3177
|
+
return;
|
|
3178
|
+
let notes = sheet.get("cellNotes");
|
|
3179
|
+
if (!notes && create) {
|
|
3180
|
+
notes = new Y6.Map;
|
|
3181
|
+
sheet.set("cellNotes", notes);
|
|
3182
|
+
}
|
|
3183
|
+
return notes;
|
|
3184
|
+
}
|
|
3185
|
+
function readCellNotes(doc, chapterId) {
|
|
3186
|
+
const notes = getGridMap(doc).get(chapterId)?.get("cellNotes");
|
|
3187
|
+
return notes ? notes.toJSON() : {};
|
|
3188
|
+
}
|
|
3189
|
+
function setCellNote(doc, chapterId, sliceId, swimLaneId, text) {
|
|
3190
|
+
doc.transact(() => {
|
|
3191
|
+
const key = cellKey(sliceId, swimLaneId);
|
|
3192
|
+
const trimmed = text.trim();
|
|
3193
|
+
if (!trimmed) {
|
|
3194
|
+
getCellNotesMap(doc, chapterId, false)?.delete(key);
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
getCellNotesMap(doc, chapterId, true).set(key, trimmed);
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
// ../packages/canvas-model/src/sheet-delete.ts
|
|
3201
|
+
var CHAPTERS_SCOPE = "chapters";
|
|
3202
|
+
var SLICES_SCOPE2 = "slices";
|
|
3203
|
+
var SWIM_LANES_SCOPE = "swimLanes";
|
|
3204
|
+
var SCENARIOS_SCOPE2 = "scenarios";
|
|
3205
|
+
var SHEET_FLOWS_SCOPE = "sheetFlows";
|
|
3206
|
+
function deleteSheet(doc, chapterId) {
|
|
3207
|
+
doc.transact(() => {
|
|
3208
|
+
const sheet = getGridMap(doc).get(chapterId);
|
|
3209
|
+
const removedStickies = new Set;
|
|
3210
|
+
const sliceIds = new Set;
|
|
3211
|
+
const laneIds = new Set;
|
|
3212
|
+
if (sheet) {
|
|
3213
|
+
for (const id of sheet.get("columns").toArray())
|
|
3214
|
+
sliceIds.add(id);
|
|
3215
|
+
for (const id of sheet.get("rows").toArray())
|
|
3216
|
+
laneIds.add(id);
|
|
3217
|
+
const cells = sheet.get("cells");
|
|
3218
|
+
for (const key of cells.keys()) {
|
|
3219
|
+
for (const ref of cells.get(key).toArray()) {
|
|
3220
|
+
const stickyId = ref.get("stickyId");
|
|
3221
|
+
getScopeMap(doc, ref.get("scope")).delete(stickyId);
|
|
3222
|
+
removedStickies.add(stickyId);
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
for (const id of sliceIds)
|
|
3227
|
+
getScopeMap(doc, SLICES_SCOPE2).delete(id);
|
|
3228
|
+
for (const id of laneIds)
|
|
3229
|
+
getScopeMap(doc, SWIM_LANES_SCOPE).delete(id);
|
|
3230
|
+
const scenarios = getScopeMap(doc, SCENARIOS_SCOPE2);
|
|
3231
|
+
for (const [id, sc] of scenarios.entries()) {
|
|
3232
|
+
if (sliceIds.has(sc.get("sliceId"))) {
|
|
3233
|
+
scenarios.delete(id);
|
|
3234
|
+
removedStickies.add(id);
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
cascadeStickyRemoval(doc, removedStickies);
|
|
3238
|
+
const sheetFlows = getScopeMap(doc, SHEET_FLOWS_SCOPE);
|
|
3239
|
+
for (const [id, f] of sheetFlows.entries()) {
|
|
3240
|
+
if (f.get("sourceChapterId") === chapterId || f.get("targetChapterId") === chapterId) {
|
|
3241
|
+
sheetFlows.delete(id);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
removeSheet(doc, chapterId);
|
|
3245
|
+
getScopeMap(doc, CHAPTERS_SCOPE).delete(chapterId);
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
// ../packages/canvas-model/src/sheet-layout.ts
|
|
3249
|
+
function applyLayoutOverride(cells, ov) {
|
|
3250
|
+
const out = {};
|
|
3251
|
+
for (const [k, arr] of Object.entries(cells))
|
|
3252
|
+
out[k] = arr.filter((r) => r.stickyId !== ov.stickyId);
|
|
3253
|
+
if (ov.toSliceId && ov.toSwimLaneId && ov.scope) {
|
|
3254
|
+
const key = cellKey(ov.toSliceId, ov.toSwimLaneId);
|
|
3255
|
+
const arr = out[key] ? [...out[key]] : [];
|
|
3256
|
+
const at = ov.index === undefined ? arr.length : Math.max(0, Math.min(arr.length, ov.index));
|
|
3257
|
+
arr.splice(at, 0, { stickyId: ov.stickyId, scope: ov.scope });
|
|
3258
|
+
out[key] = arr;
|
|
3259
|
+
}
|
|
3260
|
+
return out;
|
|
3261
|
+
}
|
|
3262
|
+
var SHEET_LAYOUT = {
|
|
3263
|
+
headerTop: 80,
|
|
3264
|
+
headerLeft: 200,
|
|
3265
|
+
cellPadX: 60,
|
|
3266
|
+
cellPadY: 60,
|
|
3267
|
+
stickyGap: 20,
|
|
3268
|
+
minColWidth: 340,
|
|
3269
|
+
minRowHeight: 240
|
|
3270
|
+
};
|
|
3271
|
+
var SCENARIO_CARD = { width: 300, height: 80 };
|
|
3272
|
+
function stickySize(scope) {
|
|
3273
|
+
if (scope === "scenarios")
|
|
3274
|
+
return SCENARIO_CARD;
|
|
3275
|
+
return FIXED_SIZE_SCOPE_DIMENSIONS[scope] ?? { width: 160, height: 100 };
|
|
3276
|
+
}
|
|
3277
|
+
function isVerticalCell(refs) {
|
|
3278
|
+
return refs.length > 0 && refs[0].scope === "scenarios";
|
|
3279
|
+
}
|
|
3280
|
+
function cellContentSize(refs) {
|
|
3281
|
+
if (refs.length === 0)
|
|
3282
|
+
return { width: 0, height: 0 };
|
|
3283
|
+
const vertical = isVerticalCell(refs);
|
|
3284
|
+
let width = 0;
|
|
3285
|
+
let height = 0;
|
|
3286
|
+
refs.forEach((ref, i) => {
|
|
3287
|
+
const s = stickySize(ref.scope);
|
|
3288
|
+
if (vertical) {
|
|
3289
|
+
height += s.height + (i > 0 ? SHEET_LAYOUT.stickyGap : 0);
|
|
3290
|
+
width = Math.max(width, s.width);
|
|
3291
|
+
} else {
|
|
3292
|
+
width += s.width + (i > 0 ? SHEET_LAYOUT.stickyGap : 0);
|
|
3293
|
+
height = Math.max(height, s.height);
|
|
3294
|
+
}
|
|
3295
|
+
});
|
|
3296
|
+
return { width, height };
|
|
3297
|
+
}
|
|
3298
|
+
function pruneOrphanCellRefs(doc, chapterId) {
|
|
3299
|
+
const sheet = getGridMap(doc).get(chapterId);
|
|
3300
|
+
if (!sheet)
|
|
3301
|
+
return 0;
|
|
3302
|
+
const cells = sheet.get("cells");
|
|
3303
|
+
let pruned = 0;
|
|
3304
|
+
for (const key of [...cells.keys()]) {
|
|
3305
|
+
const arr = cells.get(key);
|
|
3306
|
+
for (let i = arr.length - 1;i >= 0; i -= 1) {
|
|
3307
|
+
const ref = arr.get(i);
|
|
3308
|
+
const scope = ref.get("scope");
|
|
3309
|
+
const stickyId = ref.get("stickyId");
|
|
3310
|
+
if (!getScopeMap(doc, scope).has(stickyId)) {
|
|
3311
|
+
arr.delete(i, 1);
|
|
3312
|
+
pruned += 1;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
return pruned;
|
|
3317
|
+
}
|
|
3318
|
+
function computeSheetLayout(doc, chapterId, override) {
|
|
3319
|
+
const structure = readSheet(doc, chapterId);
|
|
3320
|
+
if (!structure)
|
|
3321
|
+
return;
|
|
3322
|
+
const chapter = getEntry(doc, "chapters", chapterId);
|
|
3323
|
+
const originX = snap(chapter?.x ?? 0);
|
|
3324
|
+
const originY = snap(chapter?.y ?? 0);
|
|
3325
|
+
const { columns, rows } = structure;
|
|
3326
|
+
const cells = override ? applyLayoutOverride(structure.cells, override) : structure.cells;
|
|
3327
|
+
const colWidths = columns.map((sliceId) => {
|
|
3328
|
+
let w = 0;
|
|
3329
|
+
for (const swimLaneId of rows) {
|
|
3330
|
+
const refs = cells[cellKey(sliceId, swimLaneId)] ?? [];
|
|
3331
|
+
const c = cellContentSize(refs);
|
|
3332
|
+
if (c.width > 0)
|
|
3333
|
+
w = Math.max(w, c.width + SHEET_LAYOUT.cellPadX * 2);
|
|
3334
|
+
}
|
|
3335
|
+
return snap(Math.max(w, SHEET_LAYOUT.minColWidth));
|
|
3336
|
+
});
|
|
3337
|
+
const rowHeights = rows.map((swimLaneId) => {
|
|
3338
|
+
let h = 0;
|
|
3339
|
+
for (const sliceId of columns) {
|
|
3340
|
+
const refs = cells[cellKey(sliceId, swimLaneId)] ?? [];
|
|
3341
|
+
const c = cellContentSize(refs);
|
|
3342
|
+
if (c.height > 0)
|
|
3343
|
+
h = Math.max(h, c.height + SHEET_LAYOUT.cellPadY * 2);
|
|
3344
|
+
}
|
|
3345
|
+
return snap(Math.max(h, SHEET_LAYOUT.minRowHeight));
|
|
3346
|
+
});
|
|
3347
|
+
const colX = [];
|
|
3348
|
+
let xCursor = originX + SHEET_LAYOUT.headerLeft;
|
|
3349
|
+
colWidths.forEach((w, i) => {
|
|
3350
|
+
colX[i] = xCursor;
|
|
3351
|
+
xCursor += w;
|
|
3352
|
+
});
|
|
3353
|
+
const rowY = [];
|
|
3354
|
+
let yCursor = originY + SHEET_LAYOUT.headerTop;
|
|
3355
|
+
rowHeights.forEach((h, i) => {
|
|
3356
|
+
rowY[i] = yCursor;
|
|
3357
|
+
yCursor += h;
|
|
3358
|
+
});
|
|
3359
|
+
const gridWidth = SHEET_LAYOUT.headerLeft + colWidths.reduce((a, b) => a + b, 0);
|
|
3360
|
+
const gridHeight = SHEET_LAYOUT.headerTop + rowHeights.reduce((a, b) => a + b, 0);
|
|
3361
|
+
const columnLayouts = columns.map((sliceId, i) => ({
|
|
3362
|
+
sliceId,
|
|
3363
|
+
index: i,
|
|
3364
|
+
x: colX[i],
|
|
3365
|
+
y: originY,
|
|
3366
|
+
width: colWidths[i],
|
|
3367
|
+
height: gridHeight
|
|
3368
|
+
}));
|
|
3369
|
+
const rowLayouts = rows.map((swimLaneId, i) => ({
|
|
3370
|
+
swimLaneId,
|
|
3371
|
+
index: i,
|
|
3372
|
+
x: originX,
|
|
3373
|
+
y: rowY[i],
|
|
3374
|
+
width: gridWidth,
|
|
3375
|
+
height: rowHeights[i]
|
|
3376
|
+
}));
|
|
3377
|
+
const stickies = [];
|
|
3378
|
+
columns.forEach((sliceId, ci) => {
|
|
3379
|
+
rows.forEach((swimLaneId, ri) => {
|
|
3380
|
+
const refs = cells[cellKey(sliceId, swimLaneId)] ?? [];
|
|
3381
|
+
if (refs.length === 0)
|
|
3382
|
+
return;
|
|
3383
|
+
const content = cellContentSize(refs);
|
|
3384
|
+
const cellLeft = colX[ci];
|
|
3385
|
+
const cellTop = rowY[ri];
|
|
3386
|
+
const midX = cellLeft + colWidths[ci] / 2;
|
|
3387
|
+
const midY = cellTop + rowHeights[ri] / 2;
|
|
3388
|
+
if (isVerticalCell(refs)) {
|
|
3389
|
+
let cursorY = cellTop + SHEET_LAYOUT.cellPadY;
|
|
3390
|
+
for (const ref of refs) {
|
|
3391
|
+
const size = stickySize(ref.scope);
|
|
3392
|
+
stickies.push({
|
|
3393
|
+
stickyId: ref.stickyId,
|
|
3394
|
+
scope: ref.scope,
|
|
3395
|
+
sliceId,
|
|
3396
|
+
swimLaneId,
|
|
3397
|
+
x: Math.round(midX - size.width / 2),
|
|
3398
|
+
y: Math.round(cursorY),
|
|
3399
|
+
width: size.width,
|
|
3400
|
+
height: size.height
|
|
3401
|
+
});
|
|
3402
|
+
cursorY += size.height + SHEET_LAYOUT.stickyGap;
|
|
3403
|
+
}
|
|
3404
|
+
} else {
|
|
3405
|
+
let cursorX = midX - content.width / 2;
|
|
3406
|
+
for (const ref of refs) {
|
|
3407
|
+
const size = stickySize(ref.scope);
|
|
3408
|
+
stickies.push({
|
|
3409
|
+
stickyId: ref.stickyId,
|
|
3410
|
+
scope: ref.scope,
|
|
3411
|
+
sliceId,
|
|
3412
|
+
swimLaneId,
|
|
3413
|
+
x: Math.round(cursorX),
|
|
3414
|
+
y: Math.round(midY - size.height / 2),
|
|
3415
|
+
width: size.width,
|
|
3416
|
+
height: size.height
|
|
3417
|
+
});
|
|
3418
|
+
cursorX += size.width + SHEET_LAYOUT.stickyGap;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
});
|
|
3422
|
+
});
|
|
3423
|
+
return {
|
|
3424
|
+
chapterId,
|
|
3425
|
+
chapter: { x: originX, y: originY, width: gridWidth, height: gridHeight },
|
|
3426
|
+
columns: columnLayouts,
|
|
3427
|
+
rows: rowLayouts,
|
|
3428
|
+
stickies
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
// ../packages/canvas-model/src/reflow.ts
|
|
3432
|
+
var REFLOW_ORIGIN = Symbol("sheet-reflow");
|
|
3433
|
+
var CHAPTERS_SCOPE2 = "chapters";
|
|
3434
|
+
var SLICES_SCOPE3 = "slices";
|
|
3435
|
+
var SWIM_LANES_SCOPE2 = "swimLanes";
|
|
3436
|
+
var STICKY_SCOPES = [
|
|
3437
|
+
"commands",
|
|
3438
|
+
"events",
|
|
3439
|
+
"readModels",
|
|
3440
|
+
"screens",
|
|
3441
|
+
"processors",
|
|
3442
|
+
"externalEvents",
|
|
3443
|
+
"scenarios"
|
|
3444
|
+
];
|
|
3445
|
+
function setIfChanged(m, key, val) {
|
|
3446
|
+
if (m.get(key) !== val)
|
|
3447
|
+
m.set(key, val);
|
|
3448
|
+
}
|
|
3449
|
+
function applyBox(m, box, size = true) {
|
|
3450
|
+
if (!m)
|
|
3451
|
+
return;
|
|
3452
|
+
setIfChanged(m, "x", box.x);
|
|
3453
|
+
setIfChanged(m, "y", box.y);
|
|
3454
|
+
if (size) {
|
|
3455
|
+
setIfChanged(m, "width", box.width);
|
|
3456
|
+
setIfChanged(m, "height", box.height);
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
function reflowAllSheets(doc, origin = REFLOW_ORIGIN) {
|
|
3460
|
+
const chapters = getScopeMap(doc, CHAPTERS_SCOPE2);
|
|
3461
|
+
if (chapters.size === 0)
|
|
3462
|
+
return;
|
|
3463
|
+
doc.transact(() => {
|
|
3464
|
+
for (const chapterId of chapters.keys())
|
|
3465
|
+
ensureSheet(doc, chapterId);
|
|
3466
|
+
const slices = getScopeMap(doc, SLICES_SCOPE3);
|
|
3467
|
+
const lanes = getScopeMap(doc, SWIM_LANES_SCOPE2);
|
|
3468
|
+
const stickyMaps = STICKY_SCOPES.map((s) => getScopeMap(doc, s));
|
|
3469
|
+
for (const chapterId of getGridMap(doc).keys()) {
|
|
3470
|
+
pruneOrphanCellRefs(doc, chapterId);
|
|
3471
|
+
const layout = computeSheetLayout(doc, chapterId);
|
|
3472
|
+
if (!layout)
|
|
3473
|
+
continue;
|
|
3474
|
+
const ch = chapters.get(chapterId);
|
|
3475
|
+
if (ch) {
|
|
3476
|
+
setIfChanged(ch, "width", layout.chapter.width);
|
|
3477
|
+
setIfChanged(ch, "height", layout.chapter.height);
|
|
3478
|
+
}
|
|
3479
|
+
for (const col of layout.columns)
|
|
3480
|
+
applyBox(slices.get(col.sliceId), col);
|
|
3481
|
+
for (const row of layout.rows)
|
|
3482
|
+
applyBox(lanes.get(row.swimLaneId), row);
|
|
3483
|
+
const scenarioOrder = new Map;
|
|
3484
|
+
for (const s of layout.stickies) {
|
|
3485
|
+
const map = stickyMaps[STICKY_SCOPES.indexOf(s.scope)];
|
|
3486
|
+
const entry = map?.get(s.stickyId);
|
|
3487
|
+
applyBox(entry, s, false);
|
|
3488
|
+
if (s.scope === "scenarios" && entry) {
|
|
3489
|
+
if (entry.get("sliceId") !== s.sliceId)
|
|
3490
|
+
entry.set("sliceId", s.sliceId);
|
|
3491
|
+
const idx = scenarioOrder.get(s.sliceId) ?? 0;
|
|
3492
|
+
setIfChanged(entry, "order", idx);
|
|
3493
|
+
scenarioOrder.set(s.sliceId, idx + 1);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
}, origin);
|
|
3498
|
+
}
|
|
3499
|
+
// ../packages/canvas-model/src/lanes.ts
|
|
3500
|
+
var LANE_KINDS = ["actor", "interaction", "swimlane", "specification"];
|
|
3501
|
+
var LANE_ELEMENT_SCOPES = {
|
|
3502
|
+
actor: ["screens", "processors"],
|
|
3503
|
+
interaction: ["commands", "readModels"],
|
|
3504
|
+
swimlane: ["events", "externalEvents"],
|
|
3505
|
+
specification: ["scenarios"]
|
|
3506
|
+
};
|
|
3507
|
+
function isLaneKind(v) {
|
|
3508
|
+
return typeof v === "string" && LANE_KINDS.includes(v);
|
|
3509
|
+
}
|
|
3510
|
+
function laneAllows(kind, scope) {
|
|
3511
|
+
if (!isLaneKind(kind))
|
|
3512
|
+
return false;
|
|
3513
|
+
return LANE_ELEMENT_SCOPES[kind].includes(scope);
|
|
3514
|
+
}
|
|
2896
3515
|
// src/lib/resolve-model.ts
|
|
2897
3516
|
function resolveModelId(flagValue) {
|
|
2898
3517
|
if (flagValue)
|
|
@@ -4239,6 +4858,1158 @@ function registerSnapshotCommands(program) {
|
|
|
4239
4858
|
});
|
|
4240
4859
|
}
|
|
4241
4860
|
|
|
4861
|
+
// src/commands/sheet.ts
|
|
4862
|
+
import * as fs5 from "node:fs";
|
|
4863
|
+
import * as path5 from "node:path";
|
|
4864
|
+
|
|
4865
|
+
// src/lib/sheet-render.ts
|
|
4866
|
+
var SCOPE_TAG = {
|
|
4867
|
+
commands: "cmd",
|
|
4868
|
+
events: "evt",
|
|
4869
|
+
readModels: "rm",
|
|
4870
|
+
screens: "scr",
|
|
4871
|
+
processors: "prc",
|
|
4872
|
+
externalEvents: "ext",
|
|
4873
|
+
scenarios: "scn"
|
|
4874
|
+
};
|
|
4875
|
+
var LEGEND = "tags: cmd command · evt event · rm read-model · scr screen · prc processor · ext external-event · scn scenario ( * = linked copy · † = has note )";
|
|
4876
|
+
var ORIGINAL_ID_KEY2 = {
|
|
4877
|
+
events: "originalEventStickyId",
|
|
4878
|
+
readModels: "originalReadModelStickyId",
|
|
4879
|
+
screens: "originalScreenId",
|
|
4880
|
+
externalEvents: "originalExternalEventId"
|
|
4881
|
+
};
|
|
4882
|
+
var EMPTY = "—";
|
|
4883
|
+
var DEFAULTS = { maxColWidth: 40, widthBudget: 120 };
|
|
4884
|
+
function colLabel(i) {
|
|
4885
|
+
let n = i;
|
|
4886
|
+
let s = "";
|
|
4887
|
+
do {
|
|
4888
|
+
s = String.fromCharCode(65 + n % 26) + s;
|
|
4889
|
+
n = Math.floor(n / 26) - 1;
|
|
4890
|
+
} while (n >= 0);
|
|
4891
|
+
return s;
|
|
4892
|
+
}
|
|
4893
|
+
function wrap(text, width) {
|
|
4894
|
+
if (text.length <= width)
|
|
4895
|
+
return [text];
|
|
4896
|
+
const lines = [];
|
|
4897
|
+
let cur = "";
|
|
4898
|
+
for (const word of text.split(" ")) {
|
|
4899
|
+
if (word.length > width) {
|
|
4900
|
+
if (cur) {
|
|
4901
|
+
lines.push(cur);
|
|
4902
|
+
cur = "";
|
|
4903
|
+
}
|
|
4904
|
+
let rest = word;
|
|
4905
|
+
while (rest.length > width) {
|
|
4906
|
+
lines.push(rest.slice(0, width));
|
|
4907
|
+
rest = rest.slice(width);
|
|
4908
|
+
}
|
|
4909
|
+
cur = rest;
|
|
4910
|
+
} else if (!cur) {
|
|
4911
|
+
cur = word;
|
|
4912
|
+
} else if (cur.length + 1 + word.length <= width) {
|
|
4913
|
+
cur += " " + word;
|
|
4914
|
+
} else {
|
|
4915
|
+
lines.push(cur);
|
|
4916
|
+
cur = word;
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
if (cur)
|
|
4920
|
+
lines.push(cur);
|
|
4921
|
+
return lines;
|
|
4922
|
+
}
|
|
4923
|
+
function entityName(doc, scope2, id) {
|
|
4924
|
+
const e = getEntry(doc, scope2, id);
|
|
4925
|
+
return e?.name?.trim() || "(unnamed)";
|
|
4926
|
+
}
|
|
4927
|
+
function elementText(doc, ref) {
|
|
4928
|
+
const tag = SCOPE_TAG[ref.scope] ?? ref.scope;
|
|
4929
|
+
const e = getEntry(doc, ref.scope, ref.stickyId);
|
|
4930
|
+
const originalKey = ORIGINAL_ID_KEY2[ref.scope];
|
|
4931
|
+
if (e?.isLinkedCopy && originalKey && typeof e[originalKey] === "string") {
|
|
4932
|
+
return `${tag} ${entityName(doc, ref.scope, e[originalKey])} *`;
|
|
4933
|
+
}
|
|
4934
|
+
return `${tag} ${entityName(doc, ref.scope, ref.stickyId)}`;
|
|
4935
|
+
}
|
|
4936
|
+
function pad(s, width) {
|
|
4937
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
4938
|
+
}
|
|
4939
|
+
var NOTE = "†";
|
|
4940
|
+
function resolve4(doc, chapterId) {
|
|
4941
|
+
const structure = readSheet(doc, chapterId);
|
|
4942
|
+
if (!structure)
|
|
4943
|
+
return;
|
|
4944
|
+
const { columns, rows, cells } = structure;
|
|
4945
|
+
const notes = readCellNotes(doc, chapterId);
|
|
4946
|
+
const cols = columns.map((sliceId, i) => ({
|
|
4947
|
+
label: colLabel(i),
|
|
4948
|
+
name: entityName(doc, "slices", sliceId),
|
|
4949
|
+
note: Boolean(getEntry(doc, "slices", sliceId)?.note)
|
|
4950
|
+
}));
|
|
4951
|
+
const lanes2 = rows.map((swimLaneId, i) => {
|
|
4952
|
+
const e = getEntry(doc, "swimLanes", swimLaneId);
|
|
4953
|
+
return { label: String(i + 1), name: e?.name?.trim() || "(unnamed)", kind: e?.laneKind ?? "?", note: Boolean(e?.note) };
|
|
4954
|
+
});
|
|
4955
|
+
const elems = columns.map((sliceId) => rows.map((swimLaneId) => (cells[cellKey(sliceId, swimLaneId)] ?? []).map((r) => elementText(doc, r))));
|
|
4956
|
+
const cellNote = columns.map((sliceId) => rows.map((swimLaneId) => Boolean(notes[cellKey(sliceId, swimLaneId)])));
|
|
4957
|
+
return {
|
|
4958
|
+
title: entityName(doc, "chapters", chapterId),
|
|
4959
|
+
columns: cols,
|
|
4960
|
+
rows: lanes2,
|
|
4961
|
+
elems,
|
|
4962
|
+
cellNote,
|
|
4963
|
+
empty: columns.length === 0 && rows.length === 0
|
|
4964
|
+
};
|
|
4965
|
+
}
|
|
4966
|
+
function rowLabel(r) {
|
|
4967
|
+
const base = r.name.toLowerCase() === r.kind.toLowerCase() ? `${r.label} ${r.name}` : `${r.label} ${r.name} (${r.kind})`;
|
|
4968
|
+
return r.note ? `${base} ${NOTE}` : base;
|
|
4969
|
+
}
|
|
4970
|
+
function header(r) {
|
|
4971
|
+
return `Sheet "${r.title}" (${r.columns.length} slices × ${r.rows.length} lanes)`;
|
|
4972
|
+
}
|
|
4973
|
+
function cellLines(elems, width, hasNote) {
|
|
4974
|
+
const lines = elems.length === 0 ? [EMPTY] : (() => {
|
|
4975
|
+
const joined = elems.join(" · ");
|
|
4976
|
+
return joined.length <= width ? [joined] : elems.flatMap((e) => wrap(e, width));
|
|
4977
|
+
})();
|
|
4978
|
+
if (hasNote)
|
|
4979
|
+
lines[lines.length - 1] = `${lines[lines.length - 1]} ${NOTE}`;
|
|
4980
|
+
return lines;
|
|
4981
|
+
}
|
|
4982
|
+
function colHead(c) {
|
|
4983
|
+
return c.note ? `${c.label} ${c.name} ${NOTE}` : `${c.label} ${c.name}`;
|
|
4984
|
+
}
|
|
4985
|
+
function renderTable(r, opts) {
|
|
4986
|
+
const gutter = Math.max(...r.rows.map(rowLabel).map((s) => s.length), 0);
|
|
4987
|
+
const colWidths = r.columns.map((c, ci) => {
|
|
4988
|
+
const widest = Math.max(colHead(c).length, ...r.elems[ci].map((cell, ri) => Math.max(0, ...cell.map((e) => e.length)) + (r.cellNote[ci][ri] ? 2 : 0)), EMPTY.length);
|
|
4989
|
+
return Math.min(widest, opts.maxColWidth + 2);
|
|
4990
|
+
});
|
|
4991
|
+
const totalWidth = gutter + colWidths.reduce((a, w) => a + w + 2, 0);
|
|
4992
|
+
const lines = [header(r), LEGEND, ""];
|
|
4993
|
+
const headCells = r.columns.map((c, ci) => wrap(colHead(c), colWidths[ci]));
|
|
4994
|
+
const headHeight = Math.max(...headCells.map((h) => h.length));
|
|
4995
|
+
for (let i = 0;i < headHeight; i += 1) {
|
|
4996
|
+
lines.push(pad("", gutter) + " " + r.columns.map((_, ci) => pad(headCells[ci][i] ?? "", colWidths[ci])).join(" "));
|
|
4997
|
+
}
|
|
4998
|
+
lines.push("─".repeat(totalWidth));
|
|
4999
|
+
r.rows.forEach((row, ri) => {
|
|
5000
|
+
const cellsLines = r.columns.map((_, ci) => cellLines(r.elems[ci][ri], colWidths[ci], r.cellNote[ci][ri]));
|
|
5001
|
+
const rowHeight = Math.max(...cellsLines.map((cl) => cl.length));
|
|
5002
|
+
for (let i = 0;i < rowHeight; i += 1) {
|
|
5003
|
+
const label = i === 0 ? rowLabel(row) : "";
|
|
5004
|
+
const body = r.columns.map((_, ci) => pad(cellsLines[ci][i] ?? "", colWidths[ci])).join(" ");
|
|
5005
|
+
lines.push(pad(label, gutter) + " " + body);
|
|
5006
|
+
}
|
|
5007
|
+
if (ri < r.rows.length - 1)
|
|
5008
|
+
lines.push("┈".repeat(totalWidth));
|
|
5009
|
+
});
|
|
5010
|
+
return { text: lines.join(`
|
|
5011
|
+
`), width: totalWidth };
|
|
5012
|
+
}
|
|
5013
|
+
function renderListing(r) {
|
|
5014
|
+
const lines = [header(r), LEGEND, ""];
|
|
5015
|
+
r.columns.forEach((c, ci) => {
|
|
5016
|
+
lines.push(`${c.label} "${c.name}"${c.note ? ` ${NOTE}` : ""}`);
|
|
5017
|
+
let any = false;
|
|
5018
|
+
r.rows.forEach((row, ri) => {
|
|
5019
|
+
const elems = r.elems[ci][ri];
|
|
5020
|
+
const noted = r.cellNote[ci][ri];
|
|
5021
|
+
if (elems.length === 0 && !noted)
|
|
5022
|
+
return;
|
|
5023
|
+
any = true;
|
|
5024
|
+
const addr = `${c.label}${row.label}`;
|
|
5025
|
+
const mark = noted ? ` ${NOTE}` : "";
|
|
5026
|
+
if (elems.length <= 1) {
|
|
5027
|
+
lines.push(` ${addr} ${elems[0] ?? EMPTY} [${row.kind}]${mark}`);
|
|
5028
|
+
} else {
|
|
5029
|
+
lines.push(` ${addr} [${row.kind}]${mark}`);
|
|
5030
|
+
elems.forEach((e, i) => lines.push(` ${addr}.${i + 1} ${e}`));
|
|
5031
|
+
}
|
|
5032
|
+
});
|
|
5033
|
+
if (!any)
|
|
5034
|
+
lines.push(" (empty)");
|
|
5035
|
+
lines.push("");
|
|
5036
|
+
});
|
|
5037
|
+
return lines.join(`
|
|
5038
|
+
`).trimEnd();
|
|
5039
|
+
}
|
|
5040
|
+
function renderSheetOverview(doc, chapterId, options = {}) {
|
|
5041
|
+
const opts = { ...DEFAULTS, ...options };
|
|
5042
|
+
const r = resolve4(doc, chapterId);
|
|
5043
|
+
if (!r)
|
|
5044
|
+
return;
|
|
5045
|
+
if (r.empty)
|
|
5046
|
+
return `${header(r)}
|
|
5047
|
+
|
|
5048
|
+
(empty sheet)`;
|
|
5049
|
+
const table = renderTable(r, opts);
|
|
5050
|
+
return table.width <= opts.widthBudget ? table.text : renderListing(r);
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5053
|
+
// src/lib/sheet-address.ts
|
|
5054
|
+
class SheetAddressError extends Error {
|
|
5055
|
+
constructor(message) {
|
|
5056
|
+
super(message);
|
|
5057
|
+
this.name = "SheetAddressError";
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
function name(doc, scope2, id) {
|
|
5061
|
+
return getEntry(doc, scope2, id)?.name?.trim() || "(unnamed)";
|
|
5062
|
+
}
|
|
5063
|
+
function colIndexFromLabel(label) {
|
|
5064
|
+
if (!/^[A-Za-z]+$/.test(label))
|
|
5065
|
+
return -1;
|
|
5066
|
+
let n = 0;
|
|
5067
|
+
for (const ch of label.toUpperCase())
|
|
5068
|
+
n = n * 26 + (ch.charCodeAt(0) - 64);
|
|
5069
|
+
return n - 1;
|
|
5070
|
+
}
|
|
5071
|
+
function resolveSheet2(doc, ref) {
|
|
5072
|
+
const grid2 = getGridMap(doc);
|
|
5073
|
+
const ids = [...grid2.keys()];
|
|
5074
|
+
if (ids.length === 0)
|
|
5075
|
+
throw new SheetAddressError("This model has no sheets.");
|
|
5076
|
+
const done = (chapterId) => ({ chapterId, structure: readSheet(doc, chapterId) });
|
|
5077
|
+
if (ref && grid2.has(ref))
|
|
5078
|
+
return done(ref);
|
|
5079
|
+
if (!ref) {
|
|
5080
|
+
if (ids.length === 1)
|
|
5081
|
+
return done(ids[0]);
|
|
5082
|
+
const list = ids.map((id) => `"${name(doc, "chapters", id)}"`).join(", ");
|
|
5083
|
+
throw new SheetAddressError(`Multiple sheets — name one. Available: ${list}`);
|
|
5084
|
+
}
|
|
5085
|
+
const lower = ref.toLowerCase();
|
|
5086
|
+
const matches = ids.filter((id) => name(doc, "chapters", id).toLowerCase() === lower);
|
|
5087
|
+
if (matches.length === 0) {
|
|
5088
|
+
const list = ids.map((id) => `"${name(doc, "chapters", id)}"`).join(", ") || "(none)";
|
|
5089
|
+
throw new SheetAddressError(`No sheet named "${ref}". Available: ${list}`);
|
|
5090
|
+
}
|
|
5091
|
+
if (matches.length > 1)
|
|
5092
|
+
throw new SheetAddressError(`Ambiguous sheet name "${ref}" — pass the chapterId instead.`);
|
|
5093
|
+
return done(matches[0]);
|
|
5094
|
+
}
|
|
5095
|
+
function resolveColumn(doc, s, ref) {
|
|
5096
|
+
const { columns } = s.structure;
|
|
5097
|
+
const make = (index) => ({ sliceId: columns[index], index, label: labelOf(index) });
|
|
5098
|
+
if (columns.includes(ref))
|
|
5099
|
+
return make(columns.indexOf(ref));
|
|
5100
|
+
const byLabel = colIndexFromLabel(ref);
|
|
5101
|
+
if (byLabel >= 0 && byLabel < columns.length)
|
|
5102
|
+
return make(byLabel);
|
|
5103
|
+
const lower = ref.toLowerCase();
|
|
5104
|
+
const hits = columns.filter((id) => name(doc, "slices", id).toLowerCase() === lower);
|
|
5105
|
+
if (hits.length === 1)
|
|
5106
|
+
return make(columns.indexOf(hits[0]));
|
|
5107
|
+
if (hits.length > 1)
|
|
5108
|
+
throw new SheetAddressError(`Ambiguous column name "${ref}" — use its letter instead.`);
|
|
5109
|
+
throw new SheetAddressError(byLabel >= 0 ? `No column "${ref}" — not a slice name, and column ${ref.toUpperCase()} is out of range (sheet has ${columns.length} columns).` : `No column named "${ref}".`);
|
|
5110
|
+
}
|
|
5111
|
+
function resolveRow(doc, s, ref) {
|
|
5112
|
+
const { rows } = s.structure;
|
|
5113
|
+
const make = (index) => ({ swimLaneId: rows[index], index, label: String(index + 1) });
|
|
5114
|
+
if (rows.includes(ref))
|
|
5115
|
+
return make(rows.indexOf(ref));
|
|
5116
|
+
const numeric = /^\d+$/.test(ref);
|
|
5117
|
+
if (numeric) {
|
|
5118
|
+
const index = parseInt(ref, 10) - 1;
|
|
5119
|
+
if (index >= 0 && index < rows.length)
|
|
5120
|
+
return make(index);
|
|
5121
|
+
}
|
|
5122
|
+
const lower = ref.toLowerCase();
|
|
5123
|
+
const hits = rows.filter((id) => name(doc, "swimLanes", id).toLowerCase() === lower);
|
|
5124
|
+
if (hits.length === 1)
|
|
5125
|
+
return make(rows.indexOf(hits[0]));
|
|
5126
|
+
if (hits.length > 1)
|
|
5127
|
+
throw new SheetAddressError(`Ambiguous row name "${ref}" — use its number instead.`);
|
|
5128
|
+
throw new SheetAddressError(numeric ? `No row "${ref}" — not a lane name, and row ${ref} is out of range (sheet has ${rows.length} rows).` : `No row named "${ref}".`);
|
|
5129
|
+
}
|
|
5130
|
+
function labelOf(index) {
|
|
5131
|
+
let n = index;
|
|
5132
|
+
let str = "";
|
|
5133
|
+
do {
|
|
5134
|
+
str = String.fromCharCode(65 + n % 26) + str;
|
|
5135
|
+
n = Math.floor(n / 26) - 1;
|
|
5136
|
+
} while (n >= 0);
|
|
5137
|
+
return str;
|
|
5138
|
+
}
|
|
5139
|
+
function parseCellAddress(addr) {
|
|
5140
|
+
const m = /^([A-Za-z]+)(\d+)(?:\.(\d+))?$/.exec(addr.trim());
|
|
5141
|
+
if (!m)
|
|
5142
|
+
throw new SheetAddressError(`"${addr}" is not a cell address (expected e.g. B2 or B2.1).`);
|
|
5143
|
+
return { colLabel: m[1], rowNum: parseInt(m[2], 10), ordinal: m[3] ? parseInt(m[3], 10) : undefined };
|
|
5144
|
+
}
|
|
5145
|
+
function resolveCell(doc, s, addr) {
|
|
5146
|
+
const { colLabel: colLabel2, rowNum } = parseCellAddress(addr);
|
|
5147
|
+
const col = resolveColumn(doc, s, colLabel2);
|
|
5148
|
+
const row = resolveRow(doc, s, String(rowNum));
|
|
5149
|
+
const refs = (s.structure.cells[cellKey(col.sliceId, row.swimLaneId)] ?? []).map((r) => ({
|
|
5150
|
+
stickyId: r.stickyId,
|
|
5151
|
+
scope: r.scope
|
|
5152
|
+
}));
|
|
5153
|
+
return {
|
|
5154
|
+
sliceId: col.sliceId,
|
|
5155
|
+
swimLaneId: row.swimLaneId,
|
|
5156
|
+
colIndex: col.index,
|
|
5157
|
+
rowIndex: row.index,
|
|
5158
|
+
address: `${col.label}${row.label}`,
|
|
5159
|
+
refs
|
|
5160
|
+
};
|
|
5161
|
+
}
|
|
5162
|
+
function resolveElement(doc, s, addr) {
|
|
5163
|
+
const { ordinal } = parseCellAddress(addr);
|
|
5164
|
+
const cell = resolveCell(doc, s, addr);
|
|
5165
|
+
if (cell.refs.length === 0)
|
|
5166
|
+
throw new SheetAddressError(`Cell ${cell.address} is empty.`);
|
|
5167
|
+
let index;
|
|
5168
|
+
if (ordinal === undefined) {
|
|
5169
|
+
if (cell.refs.length > 1) {
|
|
5170
|
+
throw new SheetAddressError(`Cell ${cell.address} holds ${cell.refs.length} elements — address one as ${cell.address}.1 … ${cell.address}.${cell.refs.length}.`);
|
|
5171
|
+
}
|
|
5172
|
+
index = 0;
|
|
5173
|
+
} else {
|
|
5174
|
+
index = ordinal - 1;
|
|
5175
|
+
if (index < 0 || index >= cell.refs.length) {
|
|
5176
|
+
throw new SheetAddressError(`Cell ${cell.address} has no element #${ordinal} (it holds ${cell.refs.length}).`);
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
const ref = cell.refs[index];
|
|
5180
|
+
return {
|
|
5181
|
+
stickyId: ref.stickyId,
|
|
5182
|
+
scope: ref.scope,
|
|
5183
|
+
sliceId: cell.sliceId,
|
|
5184
|
+
swimLaneId: cell.swimLaneId,
|
|
5185
|
+
ordinal: index + 1,
|
|
5186
|
+
address: `${cell.address}.${index + 1}`
|
|
5187
|
+
};
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
// src/commands/sheet.ts
|
|
5191
|
+
var TYPE_LABEL = {
|
|
5192
|
+
commands: "command",
|
|
5193
|
+
events: "event",
|
|
5194
|
+
readModels: "readmodel",
|
|
5195
|
+
screens: "screen",
|
|
5196
|
+
processors: "processor",
|
|
5197
|
+
externalEvents: "external-event",
|
|
5198
|
+
scenarios: "scenario"
|
|
5199
|
+
};
|
|
5200
|
+
var ELEMENT_TYPES = ["command", "event", "readmodel", "screen", "processor", "external-event", "scenario"];
|
|
5201
|
+
var FLOW_TYPE = {
|
|
5202
|
+
commands: { events: "CommandToEvent", externalEvents: "CommandToExternalEvent" },
|
|
5203
|
+
events: { readModels: "EventToReadModel" },
|
|
5204
|
+
readModels: { screens: "ReadModelToScreen", processors: "ReadModelToProcessor" },
|
|
5205
|
+
screens: { commands: "ScreenToCommand" },
|
|
5206
|
+
processors: { commands: "ProcessorToCommand" },
|
|
5207
|
+
externalEvents: { processors: "ExternalEventToProcessor", screens: "ExternalEventToScreen" }
|
|
5208
|
+
};
|
|
5209
|
+
var SRC_HANDLE = {
|
|
5210
|
+
commands: "bottom-source",
|
|
5211
|
+
events: "top-source",
|
|
5212
|
+
readModels: "top-source",
|
|
5213
|
+
screens: "bottom-source",
|
|
5214
|
+
processors: "bottom-source",
|
|
5215
|
+
externalEvents: "top-source"
|
|
5216
|
+
};
|
|
5217
|
+
var TGT_HANDLE = {
|
|
5218
|
+
commands: "top-target",
|
|
5219
|
+
events: "top-target",
|
|
5220
|
+
readModels: "bottom-target",
|
|
5221
|
+
screens: "bottom-target",
|
|
5222
|
+
processors: "bottom-target",
|
|
5223
|
+
externalEvents: "top-target"
|
|
5224
|
+
};
|
|
5225
|
+
var LINKED_COPY = {
|
|
5226
|
+
events: { idKey: "eventStickyId", originalIdKey: "originalEventStickyId" },
|
|
5227
|
+
readModels: { idKey: "readModelStickyId", originalIdKey: "originalReadModelStickyId" },
|
|
5228
|
+
screens: { idKey: "screenId", originalIdKey: "originalScreenId" },
|
|
5229
|
+
externalEvents: { idKey: "externalEventId", originalIdKey: "originalExternalEventId" }
|
|
5230
|
+
};
|
|
5231
|
+
function nameOf(doc, scope2, id) {
|
|
5232
|
+
return getEntry(doc, scope2, id)?.name?.trim() || "(unnamed)";
|
|
5233
|
+
}
|
|
5234
|
+
function colLabel2(i) {
|
|
5235
|
+
let n = i;
|
|
5236
|
+
let s = "";
|
|
5237
|
+
do {
|
|
5238
|
+
s = String.fromCharCode(65 + n % 26) + s;
|
|
5239
|
+
n = Math.floor(n / 26) - 1;
|
|
5240
|
+
} while (n >= 0);
|
|
5241
|
+
return s;
|
|
5242
|
+
}
|
|
5243
|
+
async function runWrite(opts, fn) {
|
|
5244
|
+
const modelId = resolveModelId(opts.model);
|
|
5245
|
+
try {
|
|
5246
|
+
const out = await withDoc(modelId, (doc) => {
|
|
5247
|
+
const s = resolveSheet2(doc, opts.sheet);
|
|
5248
|
+
const result = fn(doc, s, modelId);
|
|
5249
|
+
reflowAllSheets(doc);
|
|
5250
|
+
return result;
|
|
5251
|
+
});
|
|
5252
|
+
console.log(out);
|
|
5253
|
+
} catch (err) {
|
|
5254
|
+
if (err instanceof SheetAddressError) {
|
|
5255
|
+
console.error(err.message);
|
|
5256
|
+
process.exit(2);
|
|
5257
|
+
}
|
|
5258
|
+
throw err;
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
function atIndex(at) {
|
|
5262
|
+
if (at === undefined)
|
|
5263
|
+
return;
|
|
5264
|
+
const n = parseInt(at, 10);
|
|
5265
|
+
if (!Number.isFinite(n) || n < 1)
|
|
5266
|
+
return;
|
|
5267
|
+
return n - 1;
|
|
5268
|
+
}
|
|
5269
|
+
function parsePayload(nameOrJson) {
|
|
5270
|
+
const trimmed = nameOrJson.trim();
|
|
5271
|
+
if (trimmed.startsWith("{")) {
|
|
5272
|
+
let parsed;
|
|
5273
|
+
try {
|
|
5274
|
+
parsed = JSON.parse(trimmed);
|
|
5275
|
+
} catch (err) {
|
|
5276
|
+
throw new SheetAddressError(`Invalid JSON payload: ${err.message}`);
|
|
5277
|
+
}
|
|
5278
|
+
if (!parsed || typeof parsed !== "object")
|
|
5279
|
+
throw new SheetAddressError("Payload must be a JSON object or a bare name.");
|
|
5280
|
+
return parsed;
|
|
5281
|
+
}
|
|
5282
|
+
return { name: trimmed };
|
|
5283
|
+
}
|
|
5284
|
+
function rejectLane(doc, swimLaneId, rowIndex, kind, type) {
|
|
5285
|
+
const allowed = (LANE_ELEMENT_SCOPES[kind] ?? []).map((sc) => TYPE_LABEL[sc] ?? sc).join(", ");
|
|
5286
|
+
throw new SheetAddressError(`Lane "${nameOf(doc, "swimLanes", swimLaneId)}" (row ${rowIndex + 1}, kind=${kind}) accepts ${allowed || "(nothing)"}, not ${type}.`);
|
|
5287
|
+
}
|
|
5288
|
+
function insertColumn(doc, s, modelId, name2, at) {
|
|
5289
|
+
const id = crypto.randomUUID();
|
|
5290
|
+
const d = ELEMENT_DIMENSIONS.slice;
|
|
5291
|
+
const v = validateEntry("slices", { name: name2, x: 0, y: 0, width: d.width, height: d.height });
|
|
5292
|
+
if (!v.ok)
|
|
5293
|
+
throw new SheetAddressError(`Invalid slice: ${v.issues.join("; ")}`);
|
|
5294
|
+
doc.transact(() => {
|
|
5295
|
+
getScopeMap(doc, "slices").set(id, deepToY({ ...v.data, sliceId: id, modelId }));
|
|
5296
|
+
addColumn(doc, s.chapterId, id, at);
|
|
5297
|
+
});
|
|
5298
|
+
const idx = readSheet(doc, s.chapterId).columns.indexOf(id);
|
|
5299
|
+
return `${colLabel2(idx)} "${name2}" ${id}`;
|
|
5300
|
+
}
|
|
5301
|
+
function insertLane(doc, s, modelId, kind, name2, at) {
|
|
5302
|
+
const id = crypto.randomUUID();
|
|
5303
|
+
const d = ELEMENT_DIMENSIONS.swimLane;
|
|
5304
|
+
const v = validateEntry("swimLanes", { name: name2, laneKind: kind, x: 0, y: 0, width: d.width, height: d.height });
|
|
5305
|
+
if (!v.ok)
|
|
5306
|
+
throw new SheetAddressError(`Invalid lane: ${v.issues.join("; ")}`);
|
|
5307
|
+
doc.transact(() => {
|
|
5308
|
+
getScopeMap(doc, "swimLanes").set(id, deepToY({ ...v.data, swimLaneId: id, modelId }));
|
|
5309
|
+
addRow(doc, s.chapterId, id, at);
|
|
5310
|
+
});
|
|
5311
|
+
const idx = readSheet(doc, s.chapterId).rows.indexOf(id);
|
|
5312
|
+
return `${idx + 1} "${name2}" (${kind}) ${id}`;
|
|
5313
|
+
}
|
|
5314
|
+
function insertElement(doc, s, modelId, type, cellArg, nameOrJson) {
|
|
5315
|
+
const cell = resolveCell(doc, s, cellArg);
|
|
5316
|
+
const kind = getEntry(doc, "swimLanes", cell.swimLaneId)?.laneKind ?? "?";
|
|
5317
|
+
const payload = parsePayload(nameOrJson);
|
|
5318
|
+
const id = crypto.randomUUID();
|
|
5319
|
+
if (type === "scenario") {
|
|
5320
|
+
if (!laneAllows(kind, "scenarios"))
|
|
5321
|
+
rejectLane(doc, cell.swimLaneId, cell.rowIndex, kind, "scenario");
|
|
5322
|
+
if (!payload.name)
|
|
5323
|
+
throw new SheetAddressError("A scenario needs a name.");
|
|
5324
|
+
const entry2 = {
|
|
5325
|
+
scenarioId: id,
|
|
5326
|
+
modelId,
|
|
5327
|
+
sliceId: cell.sliceId,
|
|
5328
|
+
name: payload.name,
|
|
5329
|
+
order: 0,
|
|
5330
|
+
givenEntries: [],
|
|
5331
|
+
whenEntries: [],
|
|
5332
|
+
thenEntries: [],
|
|
5333
|
+
x: 0,
|
|
5334
|
+
y: 0,
|
|
5335
|
+
width: SCENARIO_CARD.width,
|
|
5336
|
+
height: SCENARIO_CARD.height
|
|
5337
|
+
};
|
|
5338
|
+
doc.transact(() => {
|
|
5339
|
+
getScopeMap(doc, "scenarios").set(id, deepToY(entry2));
|
|
5340
|
+
placeInCell(doc, s.chapterId, cell.sliceId, cell.swimLaneId, { stickyId: id, scope: "scenarios" });
|
|
5341
|
+
});
|
|
5342
|
+
return addressOf(doc, s, cell.sliceId, cell.swimLaneId, id, cell.address);
|
|
5343
|
+
}
|
|
5344
|
+
const meta = resolveType(type);
|
|
5345
|
+
if (!laneAllows(kind, meta.scope))
|
|
5346
|
+
rejectLane(doc, cell.swimLaneId, cell.rowIndex, kind, type);
|
|
5347
|
+
const entry = { ...payload, x: 0, y: 0 };
|
|
5348
|
+
enforceCanonicalSize(meta, entry);
|
|
5349
|
+
const v = validateEntry(meta.scope, entry);
|
|
5350
|
+
if (!v.ok)
|
|
5351
|
+
throw new SheetAddressError(`Invalid ${type}: ${v.issues.join("; ")}`);
|
|
5352
|
+
const final = { ...v.data, [meta.idKey]: id, modelId };
|
|
5353
|
+
doc.transact(() => {
|
|
5354
|
+
getScopeMap(doc, meta.scope).set(id, deepToY(final));
|
|
5355
|
+
placeInCell(doc, s.chapterId, cell.sliceId, cell.swimLaneId, { stickyId: id, scope: meta.scope });
|
|
5356
|
+
});
|
|
5357
|
+
return addressOf(doc, s, cell.sliceId, cell.swimLaneId, id, cell.address);
|
|
5358
|
+
}
|
|
5359
|
+
function addressOf(doc, s, sliceId, swimLaneId, id, cellAddr) {
|
|
5360
|
+
const refs = readSheet(doc, s.chapterId).cells[cellKey(sliceId, swimLaneId)] ?? [];
|
|
5361
|
+
const ord = refs.findIndex((r) => r.stickyId === id) + 1;
|
|
5362
|
+
return `${cellAddr}.${ord} ${id}`;
|
|
5363
|
+
}
|
|
5364
|
+
function moveElement(doc, s, elAddr, toCell) {
|
|
5365
|
+
const el = resolveElement(doc, s, elAddr);
|
|
5366
|
+
const dest = resolveCell(doc, s, toCell);
|
|
5367
|
+
const kind = getEntry(doc, "swimLanes", dest.swimLaneId)?.laneKind ?? "?";
|
|
5368
|
+
if (!laneAllows(kind, el.scope))
|
|
5369
|
+
rejectLane(doc, dest.swimLaneId, dest.rowIndex, kind, TYPE_LABEL[el.scope] ?? el.scope);
|
|
5370
|
+
doc.transact(() => {
|
|
5371
|
+
placeInCell(doc, s.chapterId, dest.sliceId, dest.swimLaneId, { stickyId: el.stickyId, scope: el.scope });
|
|
5372
|
+
});
|
|
5373
|
+
return addressOf(doc, s, dest.sliceId, dest.swimLaneId, el.stickyId, dest.address);
|
|
5374
|
+
}
|
|
5375
|
+
function reorderAxis(doc, s, ref, to) {
|
|
5376
|
+
const toIndex = to - 1;
|
|
5377
|
+
if (/^\d+$/.test(ref)) {
|
|
5378
|
+
const row = resolveRow(doc, s, ref);
|
|
5379
|
+
doc.transact(() => moveRow(doc, s.chapterId, row.swimLaneId, toIndex));
|
|
5380
|
+
const idx = readSheet(doc, s.chapterId).rows.indexOf(row.swimLaneId);
|
|
5381
|
+
return `row → ${idx + 1}`;
|
|
5382
|
+
}
|
|
5383
|
+
if (colIndexFromLabel(ref) >= 0 || s.structure.columns.length > 0) {
|
|
5384
|
+
const col = resolveColumn(doc, s, ref);
|
|
5385
|
+
doc.transact(() => moveColumn(doc, s.chapterId, col.sliceId, toIndex));
|
|
5386
|
+
const idx = readSheet(doc, s.chapterId).columns.indexOf(col.sliceId);
|
|
5387
|
+
return `column → ${colLabel2(idx)}`;
|
|
5388
|
+
}
|
|
5389
|
+
throw new SheetAddressError(`reorder takes a column letter (A) or row number (1), got "${ref}".`);
|
|
5390
|
+
}
|
|
5391
|
+
function removeTarget(doc, s, target) {
|
|
5392
|
+
if (/^[A-Za-z]+\d+(\.\d+)?$/.test(target)) {
|
|
5393
|
+
const el = resolveElement(doc, s, target);
|
|
5394
|
+
doc.transact(() => {
|
|
5395
|
+
removeSticky(doc, s.chapterId, el.stickyId);
|
|
5396
|
+
getScopeMap(doc, el.scope).delete(el.stickyId);
|
|
5397
|
+
cascadeStickyRemoval(doc, new Set([el.stickyId]));
|
|
5398
|
+
});
|
|
5399
|
+
return `removed ${el.address} (${TYPE_LABEL[el.scope] ?? el.scope})`;
|
|
5400
|
+
}
|
|
5401
|
+
if (/^\d+$/.test(target)) {
|
|
5402
|
+
const row = resolveRow(doc, s, target);
|
|
5403
|
+
const laneName = nameOf(doc, "swimLanes", row.swimLaneId);
|
|
5404
|
+
const refs2 = s.structure.columns.flatMap((sliceId) => s.structure.cells[cellKey(sliceId, row.swimLaneId)] ?? []);
|
|
5405
|
+
doc.transact(() => {
|
|
5406
|
+
removeRow(doc, s.chapterId, row.swimLaneId);
|
|
5407
|
+
getScopeMap(doc, "swimLanes").delete(row.swimLaneId);
|
|
5408
|
+
for (const r of refs2)
|
|
5409
|
+
getScopeMap(doc, r.scope).delete(r.stickyId);
|
|
5410
|
+
cascadeStickyRemoval(doc, new Set(refs2.map((r) => r.stickyId)));
|
|
5411
|
+
});
|
|
5412
|
+
return `removed row ${row.label} "${laneName}" and its ${refs2.length} element(s)`;
|
|
5413
|
+
}
|
|
5414
|
+
const col = resolveColumn(doc, s, target);
|
|
5415
|
+
const sliceName = nameOf(doc, "slices", col.sliceId);
|
|
5416
|
+
const refs = s.structure.rows.flatMap((swimLaneId) => s.structure.cells[cellKey(col.sliceId, swimLaneId)] ?? []);
|
|
5417
|
+
doc.transact(() => {
|
|
5418
|
+
removeColumn(doc, s.chapterId, col.sliceId);
|
|
5419
|
+
getScopeMap(doc, "slices").delete(col.sliceId);
|
|
5420
|
+
for (const r of refs)
|
|
5421
|
+
getScopeMap(doc, r.scope).delete(r.stickyId);
|
|
5422
|
+
cascadeStickyRemoval(doc, new Set(refs.map((r) => r.stickyId)));
|
|
5423
|
+
});
|
|
5424
|
+
return `removed column ${col.label} "${sliceName}" and its ${refs.length} element(s)`;
|
|
5425
|
+
}
|
|
5426
|
+
function updateElement(doc, s, modelId, address, json) {
|
|
5427
|
+
const el = resolveElement(doc, s, address);
|
|
5428
|
+
let parsed;
|
|
5429
|
+
try {
|
|
5430
|
+
const p = JSON.parse(json);
|
|
5431
|
+
if (!p || typeof p !== "object")
|
|
5432
|
+
throw new Error("payload must be a JSON object");
|
|
5433
|
+
parsed = p;
|
|
5434
|
+
} catch (err) {
|
|
5435
|
+
throw new SheetAddressError(`Invalid JSON: ${err.message}`);
|
|
5436
|
+
}
|
|
5437
|
+
const map = getScopeMap(doc, el.scope);
|
|
5438
|
+
const existing = getEntry(doc, el.scope, el.stickyId) ?? {};
|
|
5439
|
+
if (!isValidatedScope(el.scope)) {
|
|
5440
|
+
const merged = { ...existing, ...parsed, scenarioId: el.stickyId, modelId, sliceId: existing.sliceId };
|
|
5441
|
+
doc.transact(() => {
|
|
5442
|
+
map.delete(el.stickyId);
|
|
5443
|
+
map.set(el.stickyId, deepToY(merged));
|
|
5444
|
+
});
|
|
5445
|
+
return `updated ${el.address}`;
|
|
5446
|
+
}
|
|
5447
|
+
const meta = resolveScope(el.scope);
|
|
5448
|
+
const entry = { x: 0, y: 0, ...parsed };
|
|
5449
|
+
enforceCanonicalSize(meta, entry);
|
|
5450
|
+
const v = validateEntry(el.scope, entry);
|
|
5451
|
+
if (!v.ok)
|
|
5452
|
+
throw new SheetAddressError(`Invalid ${TYPE_LABEL[el.scope] ?? el.scope}: ${v.issues.join("; ")}`);
|
|
5453
|
+
const final = { ...v.data, [meta.idKey]: el.stickyId, modelId };
|
|
5454
|
+
doc.transact(() => {
|
|
5455
|
+
map.delete(el.stickyId);
|
|
5456
|
+
map.set(el.stickyId, deepToY(final));
|
|
5457
|
+
});
|
|
5458
|
+
return `updated ${el.address}`;
|
|
5459
|
+
}
|
|
5460
|
+
function flowBetween(doc, s, modelId, fromAddr, toAddr, typeOverride) {
|
|
5461
|
+
const from = resolveElement(doc, s, fromAddr);
|
|
5462
|
+
const to = resolveElement(doc, s, toAddr);
|
|
5463
|
+
const inferred = FLOW_TYPE[from.scope]?.[to.scope];
|
|
5464
|
+
if (!inferred) {
|
|
5465
|
+
throw new SheetAddressError(`No valid flow from ${TYPE_LABEL[from.scope] ?? from.scope} to ${TYPE_LABEL[to.scope] ?? to.scope}.`);
|
|
5466
|
+
}
|
|
5467
|
+
if (typeOverride && typeOverride !== inferred) {
|
|
5468
|
+
throw new SheetAddressError(`Flow ${from.address}→${to.address} is ${inferred}, not ${typeOverride}.`);
|
|
5469
|
+
}
|
|
5470
|
+
const id = crypto.randomUUID();
|
|
5471
|
+
const entry = {
|
|
5472
|
+
flowId: id,
|
|
5473
|
+
modelId,
|
|
5474
|
+
flowType: inferred,
|
|
5475
|
+
sourceId: from.stickyId,
|
|
5476
|
+
targetId: to.stickyId,
|
|
5477
|
+
sourceHandle: SRC_HANDLE[from.scope],
|
|
5478
|
+
targetHandle: TGT_HANDLE[to.scope]
|
|
5479
|
+
};
|
|
5480
|
+
const v = validateEntry("flows", entry);
|
|
5481
|
+
if (!v.ok)
|
|
5482
|
+
throw new SheetAddressError(`Invalid flow: ${v.issues.join("; ")}`);
|
|
5483
|
+
doc.transact(() => {
|
|
5484
|
+
getScopeMap(doc, "flows").set(id, deepToY({ ...v.data, flowId: id }));
|
|
5485
|
+
});
|
|
5486
|
+
return `${from.address} → ${to.address} (${inferred}) ${id}`;
|
|
5487
|
+
}
|
|
5488
|
+
function sliceFlowHandle(side, dir, sliceId) {
|
|
5489
|
+
return `sf:slice:${side}:${dir}:${sliceId}`;
|
|
5490
|
+
}
|
|
5491
|
+
function sheetEdgeHandle(side, dir) {
|
|
5492
|
+
return `sf:sheet:${side}:${dir}`;
|
|
5493
|
+
}
|
|
5494
|
+
function resolveAnchor(doc, sheet, dir, col, side) {
|
|
5495
|
+
const flag = dir === "source" ? "from" : "to";
|
|
5496
|
+
const sheetName = nameOf(doc, "chapters", sheet.chapterId);
|
|
5497
|
+
if (col !== undefined) {
|
|
5498
|
+
const c = resolveColumn(doc, sheet, col);
|
|
5499
|
+
const s2 = side ?? (dir === "source" ? "bottom" : "top");
|
|
5500
|
+
if (s2 !== "top" && s2 !== "bottom") {
|
|
5501
|
+
throw new SheetAddressError(`--${flag}-side must be top|bottom for a slice anchor, got "${s2}".`);
|
|
5502
|
+
}
|
|
5503
|
+
return { handle: sliceFlowHandle(s2, dir, c.sliceId), label: `"${sheetName}" ${c.label} (${s2})` };
|
|
5504
|
+
}
|
|
5505
|
+
const s = side ?? (dir === "source" ? "right" : "left");
|
|
5506
|
+
if (s !== "left" && s !== "right") {
|
|
5507
|
+
throw new SheetAddressError(`--${flag}-side must be left|right for a sheet-edge anchor, got "${s}".`);
|
|
5508
|
+
}
|
|
5509
|
+
return { handle: sheetEdgeHandle(s, dir), label: `"${sheetName}" (${s} edge)` };
|
|
5510
|
+
}
|
|
5511
|
+
function connectSheets(doc, modelId, fromRef, toRef, opts) {
|
|
5512
|
+
const src = resolveSheet2(doc, fromRef);
|
|
5513
|
+
const dst = resolveSheet2(doc, toRef);
|
|
5514
|
+
if (src.chapterId === dst.chapterId) {
|
|
5515
|
+
throw new SheetAddressError("Connect joins two different sheets — source and target are the same sheet.");
|
|
5516
|
+
}
|
|
5517
|
+
const from = resolveAnchor(doc, src, "source", opts.from, opts.fromSide);
|
|
5518
|
+
const to = resolveAnchor(doc, dst, "target", opts.to, opts.toSide);
|
|
5519
|
+
const id = crypto.randomUUID();
|
|
5520
|
+
const entry = {
|
|
5521
|
+
sheetFlowId: id,
|
|
5522
|
+
sourceChapterId: src.chapterId,
|
|
5523
|
+
sourceHandle: from.handle,
|
|
5524
|
+
targetChapterId: dst.chapterId,
|
|
5525
|
+
targetHandle: to.handle
|
|
5526
|
+
};
|
|
5527
|
+
const v = validateEntry("sheetFlows", entry);
|
|
5528
|
+
if (!v.ok)
|
|
5529
|
+
throw new SheetAddressError(`Invalid sheet flow: ${v.issues.join("; ")}`);
|
|
5530
|
+
doc.transact(() => {
|
|
5531
|
+
getScopeMap(doc, "sheetFlows").set(id, deepToY({ ...v.data, sheetFlowId: id, modelId }));
|
|
5532
|
+
});
|
|
5533
|
+
return `${from.label} → ${to.label} ${id}`;
|
|
5534
|
+
}
|
|
5535
|
+
function canonicalId(doc, scope2, id) {
|
|
5536
|
+
const e = getEntry(doc, scope2, id);
|
|
5537
|
+
const key = LINKED_COPY[scope2]?.originalIdKey;
|
|
5538
|
+
if (e?.isLinkedCopy && key && typeof e[key] === "string")
|
|
5539
|
+
return e[key];
|
|
5540
|
+
return id;
|
|
5541
|
+
}
|
|
5542
|
+
function copyElement(doc, s, modelId, srcAddr, toCell) {
|
|
5543
|
+
const src = resolveElement(doc, s, srcAddr);
|
|
5544
|
+
const meta = LINKED_COPY[src.scope];
|
|
5545
|
+
if (!meta) {
|
|
5546
|
+
throw new SheetAddressError(`${TYPE_LABEL[src.scope] ?? src.scope} elements can't have linked copies (only events, read-models, screens, external-events).`);
|
|
5547
|
+
}
|
|
5548
|
+
const dest = resolveCell(doc, s, toCell);
|
|
5549
|
+
const kind = getEntry(doc, "swimLanes", dest.swimLaneId)?.laneKind ?? "?";
|
|
5550
|
+
if (!laneAllows(kind, src.scope))
|
|
5551
|
+
rejectLane(doc, dest.swimLaneId, dest.rowIndex, kind, TYPE_LABEL[src.scope] ?? src.scope);
|
|
5552
|
+
const originalId = canonicalId(doc, src.scope, src.stickyId);
|
|
5553
|
+
const id = crypto.randomUUID();
|
|
5554
|
+
const dim = FIXED_SIZE_DIM(src.scope);
|
|
5555
|
+
doc.transact(() => {
|
|
5556
|
+
getScopeMap(doc, src.scope).set(id, deepToY({ [meta.idKey]: id, modelId, name: "", x: 0, y: 0, width: dim.width, height: dim.height, isLinkedCopy: true, [meta.originalIdKey]: originalId }));
|
|
5557
|
+
placeInCell(doc, s.chapterId, dest.sliceId, dest.swimLaneId, { stickyId: id, scope: src.scope });
|
|
5558
|
+
});
|
|
5559
|
+
return addressOf(doc, s, dest.sliceId, dest.swimLaneId, id, dest.address);
|
|
5560
|
+
}
|
|
5561
|
+
function FIXED_SIZE_DIM(scope2) {
|
|
5562
|
+
const meta = resolveScope(scope2);
|
|
5563
|
+
const entry = {};
|
|
5564
|
+
if (meta)
|
|
5565
|
+
enforceCanonicalSize(meta, entry);
|
|
5566
|
+
return { width: entry.width ?? 160, height: entry.height ?? 100 };
|
|
5567
|
+
}
|
|
5568
|
+
function patchEntityNote(doc, scope2, id, text) {
|
|
5569
|
+
doc.transact(() => {
|
|
5570
|
+
const e = getScopeMap(doc, scope2).get(id);
|
|
5571
|
+
if (!e)
|
|
5572
|
+
return;
|
|
5573
|
+
if (text)
|
|
5574
|
+
e.set("note", text);
|
|
5575
|
+
else
|
|
5576
|
+
e.delete("note");
|
|
5577
|
+
});
|
|
5578
|
+
}
|
|
5579
|
+
function noteTarget(doc, s, target, text) {
|
|
5580
|
+
const t = text.trim();
|
|
5581
|
+
const verb = t ? "noted" : "cleared note on";
|
|
5582
|
+
if (/^[A-Za-z]+\d+\.\d+$/.test(target)) {
|
|
5583
|
+
throw new SheetAddressError(`Notes attach to a cell (B2), column (A), or lane (2) — not an element (${target}).`);
|
|
5584
|
+
}
|
|
5585
|
+
if (/^[A-Za-z]+\d+$/.test(target)) {
|
|
5586
|
+
const cell = resolveCell(doc, s, target);
|
|
5587
|
+
setCellNote(doc, s.chapterId, cell.sliceId, cell.swimLaneId, t);
|
|
5588
|
+
return `${verb} cell ${cell.address}`;
|
|
5589
|
+
}
|
|
5590
|
+
if (/^\d+$/.test(target)) {
|
|
5591
|
+
const row = resolveRow(doc, s, target);
|
|
5592
|
+
patchEntityNote(doc, "swimLanes", row.swimLaneId, t);
|
|
5593
|
+
return `${verb} lane ${row.label} "${nameOf(doc, "swimLanes", row.swimLaneId)}"`;
|
|
5594
|
+
}
|
|
5595
|
+
const col = resolveColumn(doc, s, target);
|
|
5596
|
+
patchEntityNote(doc, "slices", col.sliceId, t);
|
|
5597
|
+
return `${verb} column ${col.label} "${nameOf(doc, "slices", col.sliceId)}"`;
|
|
5598
|
+
}
|
|
5599
|
+
function buildFromSpec(doc, modelId, nameArg, spec) {
|
|
5600
|
+
if (!spec || typeof spec !== "object")
|
|
5601
|
+
throw new SheetAddressError("Spec must be a JSON object.");
|
|
5602
|
+
const s = spec;
|
|
5603
|
+
const sheetName = String(nameArg ?? s.name ?? s.sheet ?? "Sheet");
|
|
5604
|
+
const cols = (Array.isArray(s.columns) ? s.columns : []).map((c) => typeof c === "string" ? { name: c } : c);
|
|
5605
|
+
const rows = (Array.isArray(s.rows) ? s.rows : []).map((r) => {
|
|
5606
|
+
const o = r;
|
|
5607
|
+
return { name: o.name, kind: o.kind ?? o.laneKind ?? "", note: o.note };
|
|
5608
|
+
});
|
|
5609
|
+
if (cols.length === 0)
|
|
5610
|
+
throw new SheetAddressError('Spec needs a non-empty "columns" array.');
|
|
5611
|
+
if (rows.length === 0)
|
|
5612
|
+
throw new SheetAddressError('Spec needs a non-empty "rows" array.');
|
|
5613
|
+
for (const r of rows) {
|
|
5614
|
+
if (!isLaneKind(r.kind))
|
|
5615
|
+
throw new SheetAddressError(`Row "${r.name}" has invalid kind "${r.kind}".`);
|
|
5616
|
+
}
|
|
5617
|
+
const cells = s.cells && typeof s.cells === "object" ? s.cells : {};
|
|
5618
|
+
const ops = [];
|
|
5619
|
+
const count = {};
|
|
5620
|
+
const scopeAt = {};
|
|
5621
|
+
const cellOf = (addr) => {
|
|
5622
|
+
const { colLabel: cl, rowNum } = parseCellAddress(addr);
|
|
5623
|
+
const ci = colIndexFromLabel(cl);
|
|
5624
|
+
const ri = rowNum - 1;
|
|
5625
|
+
if (ci < 0 || ci >= cols.length)
|
|
5626
|
+
throw new SheetAddressError(`Cell ${addr}: column out of range.`);
|
|
5627
|
+
if (ri < 0 || ri >= rows.length)
|
|
5628
|
+
throw new SheetAddressError(`Cell ${addr}: row out of range.`);
|
|
5629
|
+
return { ci, ri, kind: rows[ri].kind, cl, rowNum };
|
|
5630
|
+
};
|
|
5631
|
+
for (const [addr, elems] of Object.entries(cells)) {
|
|
5632
|
+
const { ci, ri, kind, cl, rowNum } = cellOf(addr);
|
|
5633
|
+
for (const el of elems) {
|
|
5634
|
+
const address = `${cl}${rowNum}.${count[`${cl}${rowNum}`] = (count[`${cl}${rowNum}`] ?? 0) + 1}`;
|
|
5635
|
+
if (el.type === "scenario") {
|
|
5636
|
+
if (!laneAllows(kind, "scenarios"))
|
|
5637
|
+
throw new SheetAddressError(`Cell ${addr}: ${kind} lane can't hold a scenario.`);
|
|
5638
|
+
if (!el.name)
|
|
5639
|
+
throw new SheetAddressError(`Cell ${addr}: scenario needs a name.`);
|
|
5640
|
+
ops.push({ kind: "scenario", ci, ri, name: String(el.name), address });
|
|
5641
|
+
scopeAt[address] = "scenarios";
|
|
5642
|
+
continue;
|
|
5643
|
+
}
|
|
5644
|
+
const meta = resolveType(el.type);
|
|
5645
|
+
if (!meta)
|
|
5646
|
+
throw new SheetAddressError(`Cell ${addr}: unknown element type "${el.type}".`);
|
|
5647
|
+
if (!laneAllows(kind, meta.scope))
|
|
5648
|
+
throw new SheetAddressError(`Cell ${addr}: ${kind} lane can't hold a ${el.type}.`);
|
|
5649
|
+
const { type: _t, ...rest } = el;
|
|
5650
|
+
const entry = { x: 0, y: 0, ...rest };
|
|
5651
|
+
enforceCanonicalSize(meta, entry);
|
|
5652
|
+
const v = validateEntry(meta.scope, entry);
|
|
5653
|
+
if (!v.ok)
|
|
5654
|
+
throw new SheetAddressError(`Cell ${addr} (${el.type}): ${v.issues.join("; ")}`);
|
|
5655
|
+
ops.push({ kind: "sticky", ci, ri, scope: meta.scope, idKey: meta.idKey, data: v.data, address });
|
|
5656
|
+
scopeAt[address] = meta.scope;
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
const specCopies = Array.isArray(s.copies) ? s.copies : [];
|
|
5660
|
+
for (const c of specCopies) {
|
|
5661
|
+
const { ci, ri, kind, cl, rowNum } = cellOf(c.at);
|
|
5662
|
+
const meta = resolveType(c.type);
|
|
5663
|
+
if (!meta || !(meta.scope in LINKED_COPY))
|
|
5664
|
+
throw new SheetAddressError(`Copy at ${c.at}: "${c.type}" can't be a linked copy.`);
|
|
5665
|
+
if (!laneAllows(kind, meta.scope))
|
|
5666
|
+
throw new SheetAddressError(`Copy at ${c.at}: ${kind} lane can't hold a ${c.type}.`);
|
|
5667
|
+
if (scopeAt[c.copyOf] !== meta.scope)
|
|
5668
|
+
throw new SheetAddressError(`Copy at ${c.at}: copyOf ${c.copyOf} is not a ${c.type}.`);
|
|
5669
|
+
const address = `${cl}${rowNum}.${count[`${cl}${rowNum}`] = (count[`${cl}${rowNum}`] ?? 0) + 1}`;
|
|
5670
|
+
ops.push({ kind: "copy", ci, ri, scope: meta.scope, copyOf: c.copyOf, address });
|
|
5671
|
+
scopeAt[address] = meta.scope;
|
|
5672
|
+
}
|
|
5673
|
+
const specFlows = Array.isArray(s.flows) ? s.flows : [];
|
|
5674
|
+
for (const f of specFlows) {
|
|
5675
|
+
const ss = scopeAt[f.from];
|
|
5676
|
+
const ts = scopeAt[f.to];
|
|
5677
|
+
if (!ss)
|
|
5678
|
+
throw new SheetAddressError(`Flow: unknown source ${f.from}.`);
|
|
5679
|
+
if (!ts)
|
|
5680
|
+
throw new SheetAddressError(`Flow: unknown target ${f.to}.`);
|
|
5681
|
+
const inferred = FLOW_TYPE[ss]?.[ts];
|
|
5682
|
+
if (!inferred)
|
|
5683
|
+
throw new SheetAddressError(`Flow ${f.from}→${f.to}: no valid flow from ${TYPE_LABEL[ss] ?? ss} to ${TYPE_LABEL[ts] ?? ts}.`);
|
|
5684
|
+
if (f.type && f.type !== inferred)
|
|
5685
|
+
throw new SheetAddressError(`Flow ${f.from}→${f.to} is ${inferred}, not ${f.type}.`);
|
|
5686
|
+
}
|
|
5687
|
+
const chapterId = crypto.randomUUID();
|
|
5688
|
+
const idByAddress = {};
|
|
5689
|
+
doc.transact(() => {
|
|
5690
|
+
const cd = ELEMENT_DIMENSIONS.chapter;
|
|
5691
|
+
getScopeMap(doc, "chapters").set(chapterId, deepToY({ chapterId, modelId, name: sheetName, x: 0, y: 0, width: cd.width, height: cd.height }));
|
|
5692
|
+
ensureSheet(doc, chapterId);
|
|
5693
|
+
const sd = ELEMENT_DIMENSIONS.slice;
|
|
5694
|
+
const sliceIds = cols.map((c) => {
|
|
5695
|
+
const id = crypto.randomUUID();
|
|
5696
|
+
const entry = { sliceId: id, modelId, name: c.name, x: 0, y: 0, width: sd.width, height: sd.height };
|
|
5697
|
+
if (c.status)
|
|
5698
|
+
entry.status = c.status;
|
|
5699
|
+
if (c.note)
|
|
5700
|
+
entry.note = c.note;
|
|
5701
|
+
getScopeMap(doc, "slices").set(id, deepToY(entry));
|
|
5702
|
+
addColumn(doc, chapterId, id);
|
|
5703
|
+
return id;
|
|
5704
|
+
});
|
|
5705
|
+
const ld = ELEMENT_DIMENSIONS.swimLane;
|
|
5706
|
+
const laneIds = rows.map((r) => {
|
|
5707
|
+
const id = crypto.randomUUID();
|
|
5708
|
+
const entry = { swimLaneId: id, modelId, name: r.name, laneKind: r.kind, x: 0, y: 0, width: ld.width, height: ld.height };
|
|
5709
|
+
if (r.note)
|
|
5710
|
+
entry.note = r.note;
|
|
5711
|
+
getScopeMap(doc, "swimLanes").set(id, deepToY(entry));
|
|
5712
|
+
addRow(doc, chapterId, id);
|
|
5713
|
+
return id;
|
|
5714
|
+
});
|
|
5715
|
+
for (const op of ops) {
|
|
5716
|
+
if (op.kind === "copy")
|
|
5717
|
+
continue;
|
|
5718
|
+
const id = crypto.randomUUID();
|
|
5719
|
+
if (op.kind === "scenario") {
|
|
5720
|
+
getScopeMap(doc, "scenarios").set(id, deepToY({
|
|
5721
|
+
scenarioId: id,
|
|
5722
|
+
modelId,
|
|
5723
|
+
sliceId: sliceIds[op.ci],
|
|
5724
|
+
name: op.name,
|
|
5725
|
+
order: 0,
|
|
5726
|
+
givenEntries: [],
|
|
5727
|
+
whenEntries: [],
|
|
5728
|
+
thenEntries: [],
|
|
5729
|
+
x: 0,
|
|
5730
|
+
y: 0,
|
|
5731
|
+
width: SCENARIO_CARD.width,
|
|
5732
|
+
height: SCENARIO_CARD.height
|
|
5733
|
+
}));
|
|
5734
|
+
placeInCell(doc, chapterId, sliceIds[op.ci], laneIds[op.ri], { stickyId: id, scope: "scenarios" });
|
|
5735
|
+
} else {
|
|
5736
|
+
getScopeMap(doc, op.scope).set(id, deepToY({ ...op.data, [op.idKey]: id, modelId }));
|
|
5737
|
+
placeInCell(doc, chapterId, sliceIds[op.ci], laneIds[op.ri], { stickyId: id, scope: op.scope });
|
|
5738
|
+
}
|
|
5739
|
+
idByAddress[op.address] = id;
|
|
5740
|
+
}
|
|
5741
|
+
for (const op of ops) {
|
|
5742
|
+
if (op.kind !== "copy")
|
|
5743
|
+
continue;
|
|
5744
|
+
const meta = LINKED_COPY[op.scope];
|
|
5745
|
+
const id = crypto.randomUUID();
|
|
5746
|
+
const dim = FIXED_SIZE_DIM(op.scope);
|
|
5747
|
+
getScopeMap(doc, op.scope).set(id, deepToY({ [meta.idKey]: id, modelId, name: "", x: 0, y: 0, width: dim.width, height: dim.height, isLinkedCopy: true, [meta.originalIdKey]: idByAddress[op.copyOf] }));
|
|
5748
|
+
placeInCell(doc, chapterId, sliceIds[op.ci], laneIds[op.ri], { stickyId: id, scope: op.scope });
|
|
5749
|
+
idByAddress[op.address] = id;
|
|
5750
|
+
}
|
|
5751
|
+
for (const f of specFlows) {
|
|
5752
|
+
const fid = crypto.randomUUID();
|
|
5753
|
+
const ss = scopeAt[f.from];
|
|
5754
|
+
const ts = scopeAt[f.to];
|
|
5755
|
+
getScopeMap(doc, "flows").set(fid, deepToY({ flowId: fid, modelId, flowType: FLOW_TYPE[ss][ts], sourceId: idByAddress[f.from], targetId: idByAddress[f.to], sourceHandle: SRC_HANDLE[ss], targetHandle: TGT_HANDLE[ts] }));
|
|
5756
|
+
}
|
|
5757
|
+
const specCellNotes = s.cellNotes && typeof s.cellNotes === "object" ? s.cellNotes : {};
|
|
5758
|
+
for (const [addr, text] of Object.entries(specCellNotes)) {
|
|
5759
|
+
const { colLabel: cl, rowNum } = parseCellAddress(addr);
|
|
5760
|
+
const ci = colIndexFromLabel(cl);
|
|
5761
|
+
const ri = rowNum - 1;
|
|
5762
|
+
if (ci >= 0 && ci < cols.length && ri >= 0 && ri < rows.length && text) {
|
|
5763
|
+
setCellNote(doc, chapterId, sliceIds[ci], laneIds[ri], text);
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
});
|
|
5767
|
+
reflowAllSheets(doc);
|
|
5768
|
+
const nElems = ops.length;
|
|
5769
|
+
return `built sheet "${sheetName}" — ${cols.length} slices, ${rows.length} lanes, ${nElems} elements, ${specFlows.length} flows ${chapterId}`;
|
|
5770
|
+
}
|
|
5771
|
+
function buildSheetJson(doc, s) {
|
|
5772
|
+
const { columns, rows, cells } = s.structure;
|
|
5773
|
+
const columnViews = columns.map((sliceId, i) => {
|
|
5774
|
+
const e = getEntry(doc, "slices", sliceId);
|
|
5775
|
+
return { label: colLabel2(i), name: e?.name ?? "(unnamed)", sliceId, status: e?.status ?? null, note: e?.note ?? null };
|
|
5776
|
+
});
|
|
5777
|
+
const rowViews = rows.map((swimLaneId, i) => {
|
|
5778
|
+
const e = getEntry(doc, "swimLanes", swimLaneId);
|
|
5779
|
+
return { label: String(i + 1), name: e?.name ?? "(unnamed)", laneKind: e?.laneKind ?? null, swimLaneId, note: e?.note ?? null };
|
|
5780
|
+
});
|
|
5781
|
+
const cellNotes = readCellNotes(doc, s.chapterId);
|
|
5782
|
+
const cellNotesByAddr = {};
|
|
5783
|
+
for (const [k, text] of Object.entries(cellNotes)) {
|
|
5784
|
+
const [sliceId, swimLaneId] = k.split(":");
|
|
5785
|
+
const ci = columns.indexOf(sliceId);
|
|
5786
|
+
const ri = rows.indexOf(swimLaneId);
|
|
5787
|
+
if (ci >= 0 && ri >= 0)
|
|
5788
|
+
cellNotesByAddr[`${colLabel2(ci)}${ri + 1}`] = text;
|
|
5789
|
+
}
|
|
5790
|
+
const cellViews = {};
|
|
5791
|
+
columns.forEach((sliceId, ci) => {
|
|
5792
|
+
rows.forEach((swimLaneId, ri) => {
|
|
5793
|
+
const refs = cells[cellKey(sliceId, swimLaneId)] ?? [];
|
|
5794
|
+
if (refs.length === 0)
|
|
5795
|
+
return;
|
|
5796
|
+
cellViews[`${colLabel2(ci)}${ri + 1}`] = refs.map((r, k) => ({
|
|
5797
|
+
address: `${colLabel2(ci)}${ri + 1}.${k + 1}`,
|
|
5798
|
+
type: TYPE_LABEL[r.scope] ?? r.scope,
|
|
5799
|
+
stickyId: r.stickyId,
|
|
5800
|
+
name: nameOf(doc, r.scope, r.stickyId)
|
|
5801
|
+
}));
|
|
5802
|
+
});
|
|
5803
|
+
});
|
|
5804
|
+
return { sheet: nameOf(doc, "chapters", s.chapterId), chapterId: s.chapterId, columns: columnViews, rows: rowViews, cells: cellViews, cellNotes: cellNotesByAddr };
|
|
5805
|
+
}
|
|
5806
|
+
function buildCellView(doc, s, cellArg) {
|
|
5807
|
+
const { ordinal } = parseCellAddress(cellArg);
|
|
5808
|
+
const cell = resolveCell(doc, s, cellArg);
|
|
5809
|
+
const slice = getEntry(doc, "slices", cell.sliceId);
|
|
5810
|
+
const lane = getEntry(doc, "swimLanes", cell.swimLaneId);
|
|
5811
|
+
let refs = cell.refs.map((r, i) => ({ ...r, ord: i + 1 }));
|
|
5812
|
+
if (ordinal !== undefined) {
|
|
5813
|
+
const one = refs[ordinal - 1];
|
|
5814
|
+
if (!one)
|
|
5815
|
+
throw new SheetAddressError(`Cell ${cell.address} has no element #${ordinal} (it holds ${refs.length}).`);
|
|
5816
|
+
refs = [one];
|
|
5817
|
+
}
|
|
5818
|
+
const flows = getEntries(doc, "flows");
|
|
5819
|
+
return {
|
|
5820
|
+
cell: cell.address,
|
|
5821
|
+
note: readCellNotes(doc, s.chapterId)[cellKey(cell.sliceId, cell.swimLaneId)] ?? null,
|
|
5822
|
+
slice: { label: colLabel2(cell.colIndex), name: slice?.name ?? "(unnamed)", sliceId: cell.sliceId, note: slice?.note ?? null },
|
|
5823
|
+
lane: { label: String(cell.rowIndex + 1), name: lane?.name ?? "(unnamed)", laneKind: lane?.laneKind ?? null, swimLaneId: cell.swimLaneId, note: lane?.note ?? null },
|
|
5824
|
+
elements: refs.map((r) => ({
|
|
5825
|
+
address: `${cell.address}.${r.ord}`,
|
|
5826
|
+
type: TYPE_LABEL[r.scope] ?? r.scope,
|
|
5827
|
+
...getEntry(doc, r.scope, r.stickyId) ?? {},
|
|
5828
|
+
outboundFlows: flows.filter((f) => f.sourceId === r.stickyId).map((f) => ({ flowType: f.flowType, to: endpointName(doc, f.targetId) })),
|
|
5829
|
+
inboundFlows: flows.filter((f) => f.targetId === r.stickyId).map((f) => ({ flowType: f.flowType, from: endpointName(doc, f.sourceId) }))
|
|
5830
|
+
}))
|
|
5831
|
+
};
|
|
5832
|
+
}
|
|
5833
|
+
function endpointName(doc, id) {
|
|
5834
|
+
if (!id)
|
|
5835
|
+
return "(none)";
|
|
5836
|
+
for (const scope2 of ["commands", "events", "readModels", "screens", "processors", "externalEvents"]) {
|
|
5837
|
+
const e = getEntry(doc, scope2, id);
|
|
5838
|
+
if (e) {
|
|
5839
|
+
const key = ORIGINAL_ID_KEY_LOCAL[scope2];
|
|
5840
|
+
const nm = e.isLinkedCopy && key && typeof e[key] === "string" ? nameOf(doc, scope2, e[key]) : nameOf(doc, scope2, id);
|
|
5841
|
+
return `${TYPE_LABEL[scope2] ?? scope2} ${nm}`;
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
return id;
|
|
5845
|
+
}
|
|
5846
|
+
var ORIGINAL_ID_KEY_LOCAL = {
|
|
5847
|
+
events: "originalEventStickyId",
|
|
5848
|
+
readModels: "originalReadModelStickyId",
|
|
5849
|
+
screens: "originalScreenId",
|
|
5850
|
+
externalEvents: "originalExternalEventId"
|
|
5851
|
+
};
|
|
5852
|
+
function registerSheetCommands(program) {
|
|
5853
|
+
const sheet = program.command("sheet").description("Work with sheets-mode models (grid of slices × lanes)");
|
|
5854
|
+
sheet.command("show [sheet] [cell]").description("Render a sheet as a 2D grid; pass a cell (e.g. B2 or B2.1) to drill into it").option("--json", "Emit the lossless machine-readable structure instead of the 2D table").option("--model <id>", "Target model id (overrides .eventmodeler.json)").action(async (sheetRef, cellArg, opts) => {
|
|
5855
|
+
const modelId = resolveModelId(opts.model);
|
|
5856
|
+
if (cellArg === undefined && sheetRef !== undefined && /^[A-Za-z]+\d+(\.\d+)?$/.test(sheetRef)) {
|
|
5857
|
+
cellArg = sheetRef;
|
|
5858
|
+
sheetRef = undefined;
|
|
5859
|
+
}
|
|
5860
|
+
try {
|
|
5861
|
+
const output = await withDoc(modelId, (doc) => {
|
|
5862
|
+
const s = resolveSheet2(doc, sheetRef);
|
|
5863
|
+
if (cellArg)
|
|
5864
|
+
return JSON.stringify(buildCellView(doc, s, cellArg), null, 2);
|
|
5865
|
+
if (opts.json)
|
|
5866
|
+
return JSON.stringify(buildSheetJson(doc, s), null, 2);
|
|
5867
|
+
return renderSheetOverview(doc, s.chapterId) ?? "(empty sheet)";
|
|
5868
|
+
});
|
|
5869
|
+
console.log(output);
|
|
5870
|
+
} catch (err) {
|
|
5871
|
+
if (err instanceof SheetAddressError) {
|
|
5872
|
+
console.error(err.message);
|
|
5873
|
+
process.exit(2);
|
|
5874
|
+
}
|
|
5875
|
+
throw err;
|
|
5876
|
+
}
|
|
5877
|
+
});
|
|
5878
|
+
sheet.command("new [name]").description("Create a new sheet — empty, or scaffolded whole from --from <file>").option("--from <file>", "Build the whole sheet from a JSON spec (mirrors `show --json`)").option("--model <id>", "Target model id").action(async (name2, opts) => {
|
|
5879
|
+
const modelId = resolveModelId(opts.model);
|
|
5880
|
+
try {
|
|
5881
|
+
if (opts.from) {
|
|
5882
|
+
const filePath = path5.resolve(process.cwd(), opts.from);
|
|
5883
|
+
if (!fs5.existsSync(filePath))
|
|
5884
|
+
throw new SheetAddressError(`Spec file not found: ${opts.from}`);
|
|
5885
|
+
let spec;
|
|
5886
|
+
try {
|
|
5887
|
+
spec = JSON.parse(fs5.readFileSync(filePath, "utf8"));
|
|
5888
|
+
} catch (err) {
|
|
5889
|
+
throw new SheetAddressError(`Invalid JSON in ${opts.from}: ${err.message}`);
|
|
5890
|
+
}
|
|
5891
|
+
const out2 = await withDoc(modelId, (doc) => buildFromSpec(doc, modelId, name2, spec));
|
|
5892
|
+
console.log(out2);
|
|
5893
|
+
return;
|
|
5894
|
+
}
|
|
5895
|
+
if (!name2)
|
|
5896
|
+
throw new SheetAddressError("Pass a <name> (or --from <file>).");
|
|
5897
|
+
const id = crypto.randomUUID();
|
|
5898
|
+
const d = ELEMENT_DIMENSIONS.chapter;
|
|
5899
|
+
const v = validateEntry("chapters", { name: name2, x: 0, y: 0, width: d.width, height: d.height });
|
|
5900
|
+
if (!v.ok)
|
|
5901
|
+
throw new SheetAddressError(`Invalid sheet: ${v.issues.join("; ")}`);
|
|
5902
|
+
const out = await withDoc(modelId, (doc) => {
|
|
5903
|
+
doc.transact(() => {
|
|
5904
|
+
getScopeMap(doc, "chapters").set(id, deepToY({ ...v.data, chapterId: id, modelId }));
|
|
5905
|
+
ensureSheet(doc, id);
|
|
5906
|
+
});
|
|
5907
|
+
reflowAllSheets(doc);
|
|
5908
|
+
return `sheet "${name2}" ${id}`;
|
|
5909
|
+
});
|
|
5910
|
+
console.log(out);
|
|
5911
|
+
} catch (err) {
|
|
5912
|
+
if (err instanceof SheetAddressError) {
|
|
5913
|
+
console.error(err.message);
|
|
5914
|
+
process.exit(2);
|
|
5915
|
+
}
|
|
5916
|
+
throw err;
|
|
5917
|
+
}
|
|
5918
|
+
});
|
|
5919
|
+
sheet.command("delete [sheet]").description("Delete a whole sheet and everything in it — slices, lanes, elements, scenarios, flows. Destructive; pass --yes to confirm.").option("--yes", "Confirm the deletion (required — this cannot be undone)").option("--model <id>", "Target model id").action(async (sheetRef, opts) => {
|
|
5920
|
+
const modelId = resolveModelId(opts.model);
|
|
5921
|
+
try {
|
|
5922
|
+
const out = await withDoc(modelId, (doc) => {
|
|
5923
|
+
const s = resolveSheet2(doc, sheetRef);
|
|
5924
|
+
const name2 = getEntry(doc, "chapters", s.chapterId)?.name ?? s.chapterId;
|
|
5925
|
+
const elements = Object.values(s.structure.cells).reduce((n, refs) => n + refs.length, 0);
|
|
5926
|
+
const summary = `sheet "${name2}" — ${s.structure.columns.length} slice(s), ${s.structure.rows.length} lane(s), ${elements} element(s)`;
|
|
5927
|
+
if (!opts.yes) {
|
|
5928
|
+
return `Would delete ${summary}.
|
|
5929
|
+
This cannot be undone. Re-run with --yes to confirm.`;
|
|
5930
|
+
}
|
|
5931
|
+
deleteSheet(doc, s.chapterId);
|
|
5932
|
+
reflowAllSheets(doc);
|
|
5933
|
+
return `Deleted ${summary}.`;
|
|
5934
|
+
});
|
|
5935
|
+
console.log(out);
|
|
5936
|
+
} catch (err) {
|
|
5937
|
+
if (err instanceof SheetAddressError) {
|
|
5938
|
+
console.error(err.message);
|
|
5939
|
+
process.exit(2);
|
|
5940
|
+
}
|
|
5941
|
+
throw err;
|
|
5942
|
+
}
|
|
5943
|
+
});
|
|
5944
|
+
const insert = sheet.command("insert").description("Insert a slice (column), a lane (row), or an element into a cell");
|
|
5945
|
+
insert.command("slice <name>").description("Insert a new slice (column)").option("--at <n>", "Position (1-based); appends if omitted").option("--sheet <ref>", "Which sheet (name or chapterId) when the model has several").option("--model <id>", "Target model id").action((name2, opts) => runWrite(opts, (doc, s, modelId) => insertColumn(doc, s, modelId, name2, atIndex(opts.at))));
|
|
5946
|
+
for (const kind of LANE_KINDS) {
|
|
5947
|
+
insert.command(`${kind} <name>`).description(`Insert a ${kind} lane (row)`).option("--at <n>", "Position (1-based); appends if omitted").option("--sheet <ref>", "Which sheet (name or chapterId) when the model has several").option("--model <id>", "Target model id").action((name2, opts) => runWrite(opts, (doc, s, modelId) => insertLane(doc, s, modelId, kind, name2, atIndex(opts.at))));
|
|
5948
|
+
}
|
|
5949
|
+
for (const type of ELEMENT_TYPES) {
|
|
5950
|
+
insert.command(`${type} <cell> <element>`).description(`Insert a ${type} into a cell (e.g. B2). <element> is a name or a JSON object.`).option("--sheet <ref>", "Which sheet (name or chapterId) when the model has several").option("--model <id>", "Target model id").action((cell, element, opts) => runWrite(opts, (doc, s, modelId) => insertElement(doc, s, modelId, type, cell, element)));
|
|
5951
|
+
}
|
|
5952
|
+
sheet.command("move <element>").description("Move an element to another cell, e.g. `move B2.1 --to C2` (keeps its id + flows)").requiredOption("--to <cell>", "Destination cell (e.g. C2)").option("--sheet <ref>", "Which sheet when the model has several").option("--model <id>", "Target model id").action((element, opts) => runWrite(opts, (doc, s) => moveElement(doc, s, element, opts.to)));
|
|
5953
|
+
sheet.command("reorder <ref>").description("Reorder a slice (column letter) or lane (row number), e.g. `reorder A --to 3`").requiredOption("--to <n>", "New 1-based position").option("--sheet <ref>", "Which sheet when the model has several").option("--model <id>", "Target model id").action((ref, opts) => runWrite(opts, (doc, s) => reorderAxis(doc, s, ref, parseInt(opts.to, 10))));
|
|
5954
|
+
sheet.command("rm <target>").description("Remove an element (B2.1), a column (A), or a lane (2) — cascades to flows/scenarios").option("--sheet <ref>", "Which sheet when the model has several").option("--model <id>", "Target model id").action((target, opts) => runWrite(opts, (doc, s) => removeTarget(doc, s, target)));
|
|
5955
|
+
sheet.command("update <address> <json>").description(`Replace an element's shape in place (e.g. \`update A2.1 '{"name":"PlaceOrderV2","fields":[…]}'\`)`).option("--sheet <ref>", "Which sheet when the model has several").option("--model <id>", "Target model id").action((address, json, opts) => runWrite(opts, (doc, s, modelId) => updateElement(doc, s, modelId, address, json)));
|
|
5956
|
+
sheet.command("flow <from> <to>").description("Connect two elements, e.g. `flow A2.1 A3.1` (flow type inferred from the element kinds)").option("--type <flowType>", "Force a flow type (otherwise inferred)").option("--sheet <ref>", "Which sheet when the model has several").option("--model <id>", "Target model id").action((from, to, opts) => runWrite(opts, (doc, s, modelId) => flowBetween(doc, s, modelId, from, to, opts.type)));
|
|
5957
|
+
sheet.command("connect <fromSheet> <toSheet>").description("Draw a branching flow between two sheets, e.g. `connect Checkout Fulfillment` (sheet edges), or anchor on slices with `--from D --to A`").option("--from <col>", "Anchor the source on a slice (column letter/name); omit for the source sheet's right edge").option("--to <col>", "Anchor the target on a slice (column letter/name); omit for the target sheet's left edge").option("--from-side <side>", "Source anchor side — top|bottom for a slice (default bottom), left|right for a sheet edge (default right)").option("--to-side <side>", "Target anchor side — top|bottom for a slice (default top), left|right for a sheet edge (default left)").option("--model <id>", "Target model id").action(async (fromSheet, toSheet, opts) => {
|
|
5958
|
+
const modelId = resolveModelId(opts.model);
|
|
5959
|
+
try {
|
|
5960
|
+
const out = await withDoc(modelId, (doc) => connectSheets(doc, modelId, fromSheet, toSheet, opts));
|
|
5961
|
+
console.log(out);
|
|
5962
|
+
} catch (err) {
|
|
5963
|
+
if (err instanceof SheetAddressError) {
|
|
5964
|
+
console.error(err.message);
|
|
5965
|
+
process.exit(2);
|
|
5966
|
+
}
|
|
5967
|
+
throw err;
|
|
5968
|
+
}
|
|
5969
|
+
});
|
|
5970
|
+
sheet.command("copy <element>").description("Place a linked copy of an element into another cell, e.g. `copy A3.1 --to D3` (events/read-models/screens/external-events)").requiredOption("--to <cell>", "Destination cell").option("--sheet <ref>", "Which sheet when the model has several").option("--model <id>", "Target model id").action((element, opts) => runWrite(opts, (doc, s, modelId) => copyElement(doc, s, modelId, element, opts.to)));
|
|
5971
|
+
sheet.command("note <target> [text]").description('Annotate a cell (B2), column (A), or lane (2). Omit text (or pass "") to clear.').option("--sheet <ref>", "Which sheet when the model has several").option("--model <id>", "Target model id").action((target, text, opts) => runWrite(opts, (doc, s) => noteTarget(doc, s, target, text ?? "")));
|
|
5972
|
+
}
|
|
5973
|
+
|
|
5974
|
+
// src/lib/connection-state.ts
|
|
5975
|
+
function argvModel(argv) {
|
|
5976
|
+
for (let i = 0;i < argv.length; i += 1) {
|
|
5977
|
+
const a = argv[i];
|
|
5978
|
+
if (a === "--model")
|
|
5979
|
+
return argv[i + 1];
|
|
5980
|
+
if (a.startsWith("--model="))
|
|
5981
|
+
return a.slice("--model=".length);
|
|
5982
|
+
}
|
|
5983
|
+
return;
|
|
5984
|
+
}
|
|
5985
|
+
async function fetchMode(modelId) {
|
|
5986
|
+
try {
|
|
5987
|
+
const res = await api(`/api/models/${modelId}/visibility`);
|
|
5988
|
+
return res.mode === "sheets" || res.mode === "freeform" ? res.mode : undefined;
|
|
5989
|
+
} catch {
|
|
5990
|
+
return;
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
async function resolveConnectionState(argv) {
|
|
5994
|
+
const cfg = loadProjectConfig();
|
|
5995
|
+
const override = argvModel(argv);
|
|
5996
|
+
const modelId = override ?? cfg?.modelId;
|
|
5997
|
+
if (!modelId)
|
|
5998
|
+
return { state: "disconnected" };
|
|
5999
|
+
const usingConfig = !override || override === cfg?.modelId;
|
|
6000
|
+
if (usingConfig && cfg?.mode) {
|
|
6001
|
+
return { state: cfg.mode, modelId, modelName: cfg.modelName, mode: cfg.mode };
|
|
6002
|
+
}
|
|
6003
|
+
const mode = await fetchMode(modelId);
|
|
6004
|
+
return {
|
|
6005
|
+
state: mode ?? "unknown",
|
|
6006
|
+
modelId,
|
|
6007
|
+
modelName: usingConfig ? cfg?.modelName : undefined,
|
|
6008
|
+
mode,
|
|
6009
|
+
overridden: Boolean(override && override !== cfg?.modelId)
|
|
6010
|
+
};
|
|
6011
|
+
}
|
|
6012
|
+
|
|
4242
6013
|
// src/lib/update-check.ts
|
|
4243
6014
|
var import_semver = __toESM(require_semver2(), 1);
|
|
4244
6015
|
import { spawn } from "node:child_process";
|
|
@@ -4299,28 +6070,28 @@ async function fetchLatestVersion() {
|
|
|
4299
6070
|
}
|
|
4300
6071
|
}
|
|
4301
6072
|
function promptYesNo(question) {
|
|
4302
|
-
return new Promise((
|
|
6073
|
+
return new Promise((resolve6) => {
|
|
4303
6074
|
const rl = readline2.createInterface({
|
|
4304
6075
|
input: process.stdin,
|
|
4305
6076
|
output: process.stdout
|
|
4306
6077
|
});
|
|
4307
6078
|
const timeout = setTimeout(() => {
|
|
4308
6079
|
rl.close();
|
|
4309
|
-
|
|
6080
|
+
resolve6(false);
|
|
4310
6081
|
}, PROMPT_TIMEOUT_MS);
|
|
4311
6082
|
rl.question(question, (answer) => {
|
|
4312
6083
|
clearTimeout(timeout);
|
|
4313
6084
|
rl.close();
|
|
4314
|
-
|
|
6085
|
+
resolve6(["y", "yes"].includes(answer.trim().toLowerCase()));
|
|
4315
6086
|
});
|
|
4316
6087
|
});
|
|
4317
6088
|
}
|
|
4318
6089
|
function runUpdate() {
|
|
4319
6090
|
const [command, args] = updateCommand(detectPackageManager());
|
|
4320
|
-
return new Promise((
|
|
6091
|
+
return new Promise((resolve6) => {
|
|
4321
6092
|
const child = spawn(command, args, { stdio: "inherit" });
|
|
4322
|
-
child.on("close", (code) =>
|
|
4323
|
-
child.on("error", () =>
|
|
6093
|
+
child.on("close", (code) => resolve6(code ?? 1));
|
|
6094
|
+
child.on("error", () => resolve6(1));
|
|
4324
6095
|
});
|
|
4325
6096
|
}
|
|
4326
6097
|
async function maybeUpdateCli(packageName, currentVersion, argv) {
|
|
@@ -4361,21 +6132,50 @@ async function maybeUpdateCli(packageName, currentVersion, argv) {
|
|
|
4361
6132
|
|
|
4362
6133
|
// src/index.ts
|
|
4363
6134
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
4364
|
-
var __dirname2 =
|
|
4365
|
-
var packageJson = JSON.parse(
|
|
6135
|
+
var __dirname2 = path6.dirname(__filename2);
|
|
6136
|
+
var packageJson = JSON.parse(fs6.readFileSync(path6.join(__dirname2, "..", "package.json"), "utf-8"));
|
|
4366
6137
|
var program = new Command().name("eventmodeler").version(packageJson.version).description("CLI tool for interacting with Event Model files").option("--id <uuid>", "Skip name resolution, use UUID directly").option("--no-update-check", "Skip checking npm for a newer CLI version");
|
|
4367
6138
|
setProgram(program);
|
|
6139
|
+
var FREEFORM_COMMANDS = new Set(["create", "update", "remove", "show"]);
|
|
6140
|
+
var conn = await resolveConnectionState(process.argv);
|
|
4368
6141
|
registerAuthCommands(program);
|
|
4369
6142
|
registerInitCommands(program);
|
|
6143
|
+
registerStatusCommands(program, conn);
|
|
4370
6144
|
registerListCommands(program);
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
registerSnapshotCommands(program);
|
|
6145
|
+
var connected = conn.state !== "disconnected";
|
|
6146
|
+
var freeform = conn.state === "freeform" || conn.state === "unknown";
|
|
6147
|
+
var sheets = conn.state === "sheets" || conn.state === "unknown";
|
|
6148
|
+
if (connected) {
|
|
6149
|
+
registerSummaryCommands(program);
|
|
6150
|
+
registerSnapshotCommands(program);
|
|
6151
|
+
}
|
|
6152
|
+
if (freeform) {
|
|
6153
|
+
registerShowCommands(program);
|
|
6154
|
+
registerCreateCommands(program);
|
|
6155
|
+
registerUpdateCommands(program);
|
|
6156
|
+
registerRemoveCommands(program);
|
|
6157
|
+
}
|
|
6158
|
+
if (sheets) {
|
|
6159
|
+
registerSheetCommands(program);
|
|
6160
|
+
}
|
|
6161
|
+
program.on("command:*", (operands) => {
|
|
6162
|
+
redirectUnknown(operands[0] ?? "", conn);
|
|
6163
|
+
});
|
|
4377
6164
|
await maybeUpdateCli(packageJson.name, packageJson.version, process.argv);
|
|
4378
6165
|
program.parseAsync(process.argv).catch((err) => {
|
|
4379
6166
|
console.error("Error:", err.message);
|
|
4380
6167
|
process.exit(1);
|
|
4381
6168
|
});
|
|
6169
|
+
function redirectUnknown(cmd, c) {
|
|
6170
|
+
if (c.state === "disconnected") {
|
|
6171
|
+
console.error(`Not connected — run \`eventmodeler init\` to link a model, or pass --model <id>. (command: ${cmd || "(none)"})`);
|
|
6172
|
+
} else if (c.state === "sheets" && FREEFORM_COMMANDS.has(cmd)) {
|
|
6173
|
+
console.error(`\`${cmd}\` isn't available on a sheets model — use the \`sheet\` family: \`sheet insert\`, \`sheet show\`, \`sheet move\`, \`sheet update\`, \`sheet rm\`.`);
|
|
6174
|
+
} else if (c.state === "freeform" && cmd === "sheet") {
|
|
6175
|
+
const who = c.modelName ? `"${c.modelName}"` : c.modelId ?? "this model";
|
|
6176
|
+
console.error(`\`sheet\` commands are for sheets-mode models; ${who} is freeform. Use create/update/remove/show.`);
|
|
6177
|
+
} else {
|
|
6178
|
+
console.error(`Unknown command: ${cmd}`);
|
|
6179
|
+
}
|
|
6180
|
+
process.exit(2);
|
|
6181
|
+
}
|