@milehimikey/em 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/event-modeling/SKILL.md +62 -17
- package/.claude/skills/event-modeling/reference/conform.md +33 -6
- package/.claude/skills/event-modeling/reference/em-dsl.md +138 -43
- package/.claude/skills/event-modeling/reference/extract.md +12 -2
- package/.claude/skills/event-modeling/reference/methodology.md +29 -14
- package/.claude/skills/event-modeling/templates/conformance-report.md +9 -7
- package/.claude/skills/event-modeling/templates/slice.md +6 -1
- package/.claude/skills/event-modeling/templates/state.md +8 -0
- package/README.md +1 -0
- package/dist/catalog/build.d.ts +33 -0
- package/dist/catalog/build.js +117 -0
- package/dist/catalog/build.js.map +1 -0
- package/dist/catalog/classify.d.ts +14 -0
- package/dist/catalog/classify.js +44 -0
- package/dist/catalog/classify.js.map +1 -0
- package/dist/catalog/pages.d.ts +45 -0
- package/dist/catalog/pages.js +174 -0
- package/dist/catalog/pages.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +10 -0
- package/dist/catalog/sliceDoc.js +20 -0
- package/dist/catalog/sliceDoc.js.map +1 -0
- package/dist/cli/catalog-inputs.d.ts +16 -0
- package/dist/cli/catalog-inputs.js +18 -0
- package/dist/cli/catalog-inputs.js.map +1 -0
- package/dist/cli/glossary-inputs.d.ts +15 -0
- package/dist/cli/glossary-inputs.js +17 -0
- package/dist/cli/glossary-inputs.js.map +1 -0
- package/dist/cli/render-inputs.d.ts +27 -0
- package/dist/cli/render-inputs.js +41 -0
- package/dist/cli/render-inputs.js.map +1 -0
- package/dist/cli.js +188 -10
- package/dist/cli.js.map +1 -1
- package/dist/emit/diffJson.d.ts +1 -1
- package/dist/emit/diffJson.js +8 -1
- package/dist/emit/diffJson.js.map +1 -1
- package/dist/emit/dot.d.ts +7 -0
- package/dist/emit/dot.js +10 -4
- package/dist/emit/dot.js.map +1 -1
- package/dist/emit/glossaryJson.d.ts +13 -0
- package/dist/emit/glossaryJson.js +36 -0
- package/dist/emit/glossaryJson.js.map +1 -0
- package/dist/emit/json.d.ts +16 -2
- package/dist/emit/json.js +57 -6
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/theme.d.ts +3 -0
- package/dist/emit/theme.js +19 -0
- package/dist/emit/theme.js.map +1 -1
- package/dist/model/diff.d.ts +36 -1
- package/dist/model/diff.js +198 -35
- package/dist/model/diff.js.map +1 -1
- package/dist/model/edges.d.ts +12 -1
- package/dist/model/edges.js +19 -8
- package/dist/model/edges.js.map +1 -1
- package/dist/model/glossary.d.ts +68 -0
- package/dist/model/glossary.js +138 -0
- package/dist/model/glossary.js.map +1 -0
- package/dist/model/model.d.ts +35 -0
- package/dist/model/model.js +41 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/validate.js +98 -1
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +20 -0
- package/dist/parser/parser.js +158 -29
- package/dist/parser/parser.js.map +1 -1
- package/dist/render/drawNotes.d.ts +7 -3
- package/dist/render/drawNotes.js +75 -28
- package/dist/render/drawNotes.js.map +1 -1
- package/dist/render/render.d.ts +20 -1
- package/dist/render/render.js +52 -14
- package/dist/render/render.js.map +1 -1
- package/dist/render/serve.js +155 -0
- package/dist/render/serve.js.map +1 -1
- package/dist/render/sliceDiagram.d.ts +11 -0
- package/dist/render/sliceDiagram.js +198 -0
- package/dist/render/sliceDiagram.js.map +1 -0
- package/dist/render/sliceOverlay.d.ts +21 -0
- package/dist/render/sliceOverlay.js +111 -0
- package/dist/render/sliceOverlay.js.map +1 -0
- package/dist/render/sliceStatus.d.ts +5 -0
- package/dist/render/sliceStatus.js +26 -0
- package/dist/render/sliceStatus.js.map +1 -0
- package/dist/render/statusOverlay.d.ts +16 -0
- package/dist/render/statusOverlay.js +110 -0
- package/dist/render/statusOverlay.js.map +1 -0
- package/dist/render/svgGeometry.d.ts +30 -0
- package/dist/render/svgGeometry.js +33 -14
- package/dist/render/svgGeometry.js.map +1 -1
- package/dist/render/watch.d.ts +1 -1
- package/dist/render/watch.js +5 -2
- package/dist/render/watch.js.map +1 -1
- package/package.json +3 -2
|
@@ -16,11 +16,11 @@ Replace bracketed placeholders; delete guidance comments before finishing.
|
|
|
16
16
|
|
|
17
17
|
## Summary
|
|
18
18
|
|
|
19
|
-
| Surface | Real drift | Model gap | Internal inconsistency | Uncertainty |
|
|
20
|
-
|
|
21
|
-
| Structural | {{n}} | {{n}} | — | {{n}} |
|
|
22
|
-
| Spec | {{n}} | {{n}} | — | {{n}} |
|
|
23
|
-
| Internal | — | — | {{n}} | {{n}} |
|
|
19
|
+
| Surface | Real drift | Model gap | Internal inconsistency | Accepted divergence | Uncertainty |
|
|
20
|
+
|---|---|---|---|---|---|
|
|
21
|
+
| Structural | {{n}} | {{n}} | — | {{n}} | {{n}} |
|
|
22
|
+
| Spec | {{n}} | {{n}} | — | {{n}} | {{n}} |
|
|
23
|
+
| Internal | — | — | {{n}} | {{n}} | {{n}} |
|
|
24
24
|
|
|
25
25
|
{{One or two sentences: overall read — clean, mostly clean with N items, or notably drifted.}}
|
|
26
26
|
|
|
@@ -32,12 +32,14 @@ Replace bracketed placeholders; delete guidance comments before finishing.
|
|
|
32
32
|
### {{n}}. {{Short finding title}}
|
|
33
33
|
|
|
34
34
|
- **Surface:** {{structural | spec | internal}}
|
|
35
|
-
- **Classification:** {{real drift | model gap | internal inconsistency}}
|
|
35
|
+
- **Classification:** {{real drift | model gap | internal inconsistency | accepted divergence}}
|
|
36
36
|
- **Slice:** {{slice name}}
|
|
37
|
-
- **Evidence:** {{file path(s) and line/symbol, or the `em diff --json` entry (type + name)
|
|
37
|
+
- **Evidence:** {{file path(s) and line/symbol, or the `em diff --json` entry (type + name) —
|
|
38
|
+
for accepted divergence, the `divergence "..."` annotation text itself}}
|
|
38
39
|
- **Claim:** {{what the model/doc says vs. what the code/doc shows — the actual disagreement}}
|
|
39
40
|
- **Proposed red note:** `issue "conformance: {{text}}"` on {{element}} (slice "{{slice}}")
|
|
40
41
|
<!-- Omit this line entirely for internal-inconsistency findings with no clear side to flag,
|
|
42
|
+
for any accepted-divergence finding (already ratified — don't propose re-ratifying it),
|
|
41
43
|
or when the finding doesn't warrant a red note (say why instead). -->
|
|
42
44
|
|
|
43
45
|
## Scenario / invariant coverage
|
|
@@ -3,10 +3,15 @@ Rich slice design document. One per slice, stored in <model>/slices/<slice-name>
|
|
|
3
3
|
linked from the .em model with: note "slices/<slice-name>.md" on the slice's defining element.
|
|
4
4
|
Fill every section through Socratic questioning. Leave "Open Questions" rather than guessing.
|
|
5
5
|
Replace the bracketed placeholders; delete guidance comments before finishing.
|
|
6
|
+
|
|
7
|
+
The diagram below is generated, not hand-drawn: `em render <model>.em --slice "{{Slice Name}}"
|
|
8
|
+
-o slices/{{slice-name}}.svg` (kebab-case the slice name to match this doc's own filename).
|
|
6
9
|
-->
|
|
7
10
|
|
|
8
11
|
# Slice: {{Slice Name}}
|
|
9
12
|
|
|
13
|
+

|
|
14
|
+
|
|
10
15
|
- **Pattern:** {{State Change | State View | Automation | Translation}}
|
|
11
16
|
- **Swimlane:** {{Persona/Actor}} → {{Context/Aggregate}}
|
|
12
17
|
- **Status:** {{draft | reviewed | ready-to-implement | implemented}}
|
|
@@ -54,7 +59,7 @@ an event directly.}}
|
|
|
54
59
|
## Read Model / View
|
|
55
60
|
<!-- For State View slices, and any read model this slice produces or feeds. -->
|
|
56
61
|
- **View:** `{{View Name}}` built from events: {{"Event A", "Event B"}}
|
|
57
|
-
- **Consumed by:** {{which UI screen
|
|
62
|
+
- **Consumed by:** {{which UI screen (or API-caller persona), or reaction}}
|
|
58
63
|
<!-- "Consumed by" is required, not optional. A read model nothing displays or watches is
|
|
59
64
|
information projected out of the system and then dropped, and `em validate` warns. Every
|
|
60
65
|
instance of a repeated view needs its own consumer, not just the last one. -->
|
|
@@ -11,6 +11,7 @@ Keep it current at the end of every working session.
|
|
|
11
11
|
- **Current step:** {{1–7, see methodology; or extraction round R1–R7}}
|
|
12
12
|
- **Last updated:** {{YYYY-MM-DD}}
|
|
13
13
|
- **Last conformance:** {{YYYY-MM-DD @ <target-repo revision> — report: conformance/<date>-report.md | never}}
|
|
14
|
+
- **Last stakeholder review:** {{YYYY-MM-DD — attendees: see Participants | never}}
|
|
14
15
|
|
|
15
16
|
## Session inputs
|
|
16
17
|
- **Scope line:** {{one-line description of what's in/out of bounds for this model}}
|
|
@@ -47,6 +48,13 @@ Keep it current at the end of every working session.
|
|
|
47
48
|
attribute each entry to the participant who made the call (see Participants above). -->
|
|
48
49
|
- {{YYYY-MM-DD}}: {{decision}} — {{why}} — by {{participant, if a live workshop}}
|
|
49
50
|
|
|
51
|
+
## Usage log
|
|
52
|
+
<!-- The team's only usage signal today (see docs/usage-data.md) — cheap and coarse on purpose.
|
|
53
|
+
One line per session: phase(s) touched, and validate diagnostic *categories* hit (the rule
|
|
54
|
+
name from docs/validation.md's tables, e.g. "read model nothing consumes" — never the full
|
|
55
|
+
instance message or any domain content). Append, never edit past entries. -->
|
|
56
|
+
- {{YYYY-MM-DD}}: phases: {{discover | extract | model | slice | conform | review | validate | watch, ...}} — validate: {{diagnostic category (docs/usage-data.md#categories), ... | none}}
|
|
57
|
+
|
|
50
58
|
## Open questions / parking lot
|
|
51
59
|
<!-- Unresolved items to bring back to the user. Never guess these. Metadata is optional and
|
|
52
60
|
flat — add only what's known: source (ticket/conversation that spawned it), blocked on
|
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ built this way.
|
|
|
92
92
|
| [docs/timeline.md](docs/timeline.md) | The Two Laws of the Timeline |
|
|
93
93
|
| [docs/ai-workflow.md](docs/ai-workflow.md) | The Claude Code skill: install, phases, artifacts |
|
|
94
94
|
| [docs/dependencies.md](docs/dependencies.md) | What's bundled vs. what needs a system install |
|
|
95
|
+
| [docs/usage-data.md](docs/usage-data.md) | What usage data em captures, and how to roll it up for a retro |
|
|
95
96
|
| [docs/architecture.md](docs/architecture.md) | How rendering works; why Graphviz, not PlantUML |
|
|
96
97
|
| [docs/roadmap.md](docs/roadmap.md) | What's planned |
|
|
97
98
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NormalizedModel } from "../model/model.js";
|
|
2
|
+
import { Grid } from "../layout/grid.js";
|
|
3
|
+
import { Diagnostic } from "../model/validate.js";
|
|
4
|
+
export interface CatalogModelInput {
|
|
5
|
+
/** The .em file as given on the command line — used for the doc-discovery path and display. */
|
|
6
|
+
file: string;
|
|
7
|
+
model: NormalizedModel;
|
|
8
|
+
grid: Grid;
|
|
9
|
+
dot: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CatalogOptions {
|
|
12
|
+
outDir: string;
|
|
13
|
+
format?: "svg" | "png";
|
|
14
|
+
title?: string;
|
|
15
|
+
/** Forwarded to buildSliceDiagram's fallback build so `--keep-empty-lanes`
|
|
16
|
+
* behaves the same way for a built-on-the-fly slice diagram as it does for
|
|
17
|
+
* the main one (which already gets it via the per-file compileFile call
|
|
18
|
+
* that produced `model`/`grid` in the first place). */
|
|
19
|
+
keepEmptyLanes?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface CatalogBuildResult {
|
|
22
|
+
models: number;
|
|
23
|
+
slices: number;
|
|
24
|
+
/** Ref-collision warnings raised while assigning slice/element keys (same diagnostics
|
|
25
|
+
* `em export`/`em diff` surface via computeRefs), tagged by the input file they came from —
|
|
26
|
+
* a duplicate slice/element name still gets a stable `~2` key here, but the caller should
|
|
27
|
+
* print these the same way every other command does rather than build silently. */
|
|
28
|
+
diagnostics: {
|
|
29
|
+
file: string;
|
|
30
|
+
diagnostics: Diagnostic[];
|
|
31
|
+
}[];
|
|
32
|
+
}
|
|
33
|
+
export declare function buildCatalog(inputs: CatalogModelInput[], opts: CatalogOptions): Promise<CatalogBuildResult>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Orchestrates `em catalog`'s static-site build — the only I/O in
|
|
3
|
+
// src/catalog/*. One output directory per input model (so slice keys from
|
|
4
|
+
// different files can never collide, even without a cross-file dedup pass):
|
|
5
|
+
//
|
|
6
|
+
// <outDir>/
|
|
7
|
+
// index.html
|
|
8
|
+
// <model-key>/
|
|
9
|
+
// diagram.svg (or .png)
|
|
10
|
+
// slices/
|
|
11
|
+
// <slice-key>.html
|
|
12
|
+
// <slice-key>.svg (this slice's own diagram — copied in if authored, else built)
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
15
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
16
|
+
import { layoutDot, composeSvg, writeRendered } from "../render/render.js";
|
|
17
|
+
import { buildSliceDiagram } from "../render/sliceDiagram.js";
|
|
18
|
+
import { computeRefs } from "../emit/json.js";
|
|
19
|
+
import { dedupe, kebabSlug } from "../util/slug.js";
|
|
20
|
+
import { classifySlicePattern } from "./classify.js";
|
|
21
|
+
import { parseSliceDoc } from "./sliceDoc.js";
|
|
22
|
+
import { renderIndexPage, renderSlicePage } from "./pages.js";
|
|
23
|
+
export async function buildCatalog(inputs, opts) {
|
|
24
|
+
const format = opts.format ?? "svg";
|
|
25
|
+
const title = opts.title ?? "Event Model Catalog";
|
|
26
|
+
const outDir = opts.outDir;
|
|
27
|
+
await mkdir(outDir, { recursive: true });
|
|
28
|
+
const usedModelKeys = new Set();
|
|
29
|
+
const modelSummaries = [];
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
let sliceCount = 0;
|
|
32
|
+
for (const { file, model, grid, dot } of inputs) {
|
|
33
|
+
const modelKey = dedupe(kebabSlug(basename(file, extname(file))), usedModelKeys, "~");
|
|
34
|
+
const modelDir = join(outDir, modelKey);
|
|
35
|
+
const slicesDir = join(modelDir, "slices");
|
|
36
|
+
await mkdir(slicesDir, { recursive: true });
|
|
37
|
+
const diagramFile = `diagram.${format}`;
|
|
38
|
+
// baseDir = dirname(file), same as `em render`/`em watch` — this is why note hrefs
|
|
39
|
+
// embedded in the diagram still resolve correctly from the catalog's output location.
|
|
40
|
+
const raw = await layoutDot(dot);
|
|
41
|
+
const mainSvg = composeSvg(raw, model, grid, dirname(file), modelDir);
|
|
42
|
+
await writeRendered(mainSvg, join(modelDir, diagramFile), format);
|
|
43
|
+
const refs = computeRefs(model);
|
|
44
|
+
if (refs.diagnostics.length > 0)
|
|
45
|
+
diagnostics.push({ file, diagnostics: refs.diagnostics });
|
|
46
|
+
const sliceSummaries = [];
|
|
47
|
+
for (let i = 0; i < model.slices.length; i++) {
|
|
48
|
+
const slice = model.slices[i];
|
|
49
|
+
const sliceKey = refs.sliceKeys[i];
|
|
50
|
+
const pattern = classifySlicePattern(slice);
|
|
51
|
+
// Look up the doc at the *deduped* key, not a fresh kebabSlug(slice.name): for the
|
|
52
|
+
// (overwhelmingly common) case of a unique slice name they're identical, but if two
|
|
53
|
+
// slices share a name (see the diagnostics above), only the first gets sliceKey ==
|
|
54
|
+
// kebabSlug(name) — the doc a human actually authored on disk. The second slice's key
|
|
55
|
+
// carries a "~2" suffix no doc file will ever match, so it honestly reports "no doc"
|
|
56
|
+
// instead of silently rendering the first slice's doc content on both pages.
|
|
57
|
+
const docRelPath = join("slices", `${sliceKey}.md`);
|
|
58
|
+
const docPath = join(dirname(file), docRelPath);
|
|
59
|
+
const doc = existsSync(docPath) ? parseSliceDoc(readFileSync(docPath, "utf8")) : null;
|
|
60
|
+
// Same sibling-file convention as the doc lookup above, one extension over: a
|
|
61
|
+
// slice diagram authored by `em render --slice` (per the event-modeling skill)
|
|
62
|
+
// is copied in as-is so the catalog stays self-contained; otherwise built
|
|
63
|
+
// fresh via buildSliceDiagram, written only into the catalog's own output
|
|
64
|
+
// tree — `em catalog` never writes back into the source tree.
|
|
65
|
+
const sliceSvgSrcPath = join(dirname(file), "slices", `${sliceKey}.svg`);
|
|
66
|
+
const sliceDiagramFile = `${sliceKey}.svg`;
|
|
67
|
+
if (existsSync(sliceSvgSrcPath)) {
|
|
68
|
+
await copyFile(sliceSvgSrcPath, join(slicesDir, sliceDiagramFile));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// One Graphviz WASM layout pass per slice lacking an authored sibling — unlike
|
|
72
|
+
// the single shared pass the old crop-based approach paid for once per model.
|
|
73
|
+
// Each pass is on a tiny 1-3-column graph, so this is fast in absolute terms
|
|
74
|
+
// and the WASM module itself is loaded once and memoized (render.ts); a real,
|
|
75
|
+
// deliberate trade-off, not an oversight — worth revisiting only if catalog
|
|
76
|
+
// build time on a very-many-slice model actually becomes a complaint.
|
|
77
|
+
const { model: sliceModel, grid: sliceGrid, dot: sliceDot } = buildSliceDiagram(model, i, {
|
|
78
|
+
keepEmptyLanes: opts.keepEmptyLanes,
|
|
79
|
+
});
|
|
80
|
+
const sliceRaw = await layoutDot(sliceDot);
|
|
81
|
+
const sliceSvg = composeSvg(sliceRaw, sliceModel, sliceGrid, dirname(file), slicesDir);
|
|
82
|
+
await writeFile(join(slicesDir, sliceDiagramFile), sliceSvg, "utf8");
|
|
83
|
+
}
|
|
84
|
+
const elementRefs = new Map();
|
|
85
|
+
for (const el of slice.elements) {
|
|
86
|
+
const ref = refs.refById.get(el.id);
|
|
87
|
+
if (ref)
|
|
88
|
+
elementRefs.set(el.id, ref);
|
|
89
|
+
}
|
|
90
|
+
const page = renderSlicePage({
|
|
91
|
+
modelName: model.name,
|
|
92
|
+
diagramFile: `../${diagramFile}`,
|
|
93
|
+
sliceDiagramFile,
|
|
94
|
+
slice,
|
|
95
|
+
sliceKey,
|
|
96
|
+
pattern,
|
|
97
|
+
elementRefs,
|
|
98
|
+
doc,
|
|
99
|
+
docExpectedPath: docRelPath,
|
|
100
|
+
});
|
|
101
|
+
await writeFile(join(slicesDir, `${sliceKey}.html`), page, "utf8");
|
|
102
|
+
sliceSummaries.push({
|
|
103
|
+
key: sliceKey,
|
|
104
|
+
name: slice.name,
|
|
105
|
+
pattern,
|
|
106
|
+
hasDoc: doc !== null,
|
|
107
|
+
status: doc?.status ?? null,
|
|
108
|
+
});
|
|
109
|
+
sliceCount++;
|
|
110
|
+
}
|
|
111
|
+
modelSummaries.push({ key: modelKey, name: model.name, file, diagramFile, slices: sliceSummaries });
|
|
112
|
+
}
|
|
113
|
+
const indexHtml = renderIndexPage(modelSummaries, title, format);
|
|
114
|
+
await writeFile(join(outDir, "index.html"), indexHtml, "utf8");
|
|
115
|
+
return { models: modelSummaries.length, slices: sliceCount, diagnostics };
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/catalog/build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Slice } from "../model/model.js";
|
|
2
|
+
export type SlicePattern = "translation" | "automation" | "state-change" | "state-view" | "unclassified";
|
|
3
|
+
/**
|
|
4
|
+
* Classify a single slice by its element kinds. Known simplification, not a
|
|
5
|
+
* bug: docs/patterns.md describes Automation and Translation as spanning
|
|
6
|
+
* *two* slices — the reaction slice, then a following `command`+`event`
|
|
7
|
+
* slice. That second slice has the exact same kind-signature as a State
|
|
8
|
+
* Change slice, so per-slice classification reads it as State Change. This
|
|
9
|
+
* matches "derived from which element kinds appear in the slice" and keeps
|
|
10
|
+
* the classifier a pure function of one slice at a time; don't "fix" this by
|
|
11
|
+
* trying to look at neighboring slices.
|
|
12
|
+
*/
|
|
13
|
+
export declare function classifySlicePattern(slice: Slice): SlicePattern;
|
|
14
|
+
export declare function slicePatternLabel(pattern: SlicePattern): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Derives a slice's Event Modeling pattern from which element `kind`s it
|
|
3
|
+
// contains — the pattern itself is never stored in the AST/model (see
|
|
4
|
+
// docs/patterns.md), so `em catalog` is the first consumer that needs it
|
|
5
|
+
// computed. Rules follow docs/patterns.md's "DSL elements" table exactly,
|
|
6
|
+
// checked in priority order (a slice can carry a translation and a view
|
|
7
|
+
// together, for instance, so order matters).
|
|
8
|
+
/**
|
|
9
|
+
* Classify a single slice by its element kinds. Known simplification, not a
|
|
10
|
+
* bug: docs/patterns.md describes Automation and Translation as spanning
|
|
11
|
+
* *two* slices — the reaction slice, then a following `command`+`event`
|
|
12
|
+
* slice. That second slice has the exact same kind-signature as a State
|
|
13
|
+
* Change slice, so per-slice classification reads it as State Change. This
|
|
14
|
+
* matches "derived from which element kinds appear in the slice" and keeps
|
|
15
|
+
* the classifier a pure function of one slice at a time; don't "fix" this by
|
|
16
|
+
* trying to look at neighboring slices.
|
|
17
|
+
*/
|
|
18
|
+
export function classifySlicePattern(slice) {
|
|
19
|
+
const kinds = new Set(slice.elements.map((el) => el.kind));
|
|
20
|
+
if (kinds.has("translation"))
|
|
21
|
+
return "translation";
|
|
22
|
+
if (kinds.has("processor") || kinds.has("automation") || kinds.has("saga"))
|
|
23
|
+
return "automation";
|
|
24
|
+
if (kinds.has("command") || kinds.has("event"))
|
|
25
|
+
return "state-change";
|
|
26
|
+
if (kinds.has("view"))
|
|
27
|
+
return "state-view";
|
|
28
|
+
return "unclassified";
|
|
29
|
+
}
|
|
30
|
+
export function slicePatternLabel(pattern) {
|
|
31
|
+
switch (pattern) {
|
|
32
|
+
case "translation":
|
|
33
|
+
return "Translation";
|
|
34
|
+
case "automation":
|
|
35
|
+
return "Automation";
|
|
36
|
+
case "state-change":
|
|
37
|
+
return "State Change";
|
|
38
|
+
case "state-view":
|
|
39
|
+
return "State View";
|
|
40
|
+
case "unclassified":
|
|
41
|
+
return "Unclassified";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=classify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.js","sourceRoot":"","sources":["../../src/catalog/classify.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,yEAAyE;AACzE,sEAAsE;AACtE,yEAAyE;AACzE,0EAA0E;AAC1E,wEAAwE;AACxE,6CAA6C;AAM7C;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAY;IAC/C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC;QAAE,OAAO,aAAa,CAAC;IACnD,IAAI,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,YAAY,CAAC;IAChG,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,cAAc,CAAC;IACtE,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,YAAY,CAAC;IAC3C,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAqB;IACrD,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,aAAa;YAChB,OAAO,aAAa,CAAC;QACvB,KAAK,YAAY;YACf,OAAO,YAAY,CAAC;QACtB,KAAK,cAAc;YACjB,OAAO,cAAc,CAAC;QACxB,KAAK,YAAY;YACf,OAAO,YAAY,CAAC;QACtB,KAAK,cAAc;YACjB,OAAO,cAAc,CAAC;IAC1B,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Slice } from "../model/model.js";
|
|
2
|
+
import { SlicePattern } from "./classify.js";
|
|
3
|
+
import { SliceDoc } from "./sliceDoc.js";
|
|
4
|
+
export declare function escapeHtml(s: string): string;
|
|
5
|
+
/** A slice's row in the index page's per-model table. */
|
|
6
|
+
export interface CatalogSliceSummary {
|
|
7
|
+
key: string;
|
|
8
|
+
name: string;
|
|
9
|
+
pattern: SlicePattern;
|
|
10
|
+
/** Whether a slices/<key>.md doc was found at all — distinct from `status` being null,
|
|
11
|
+
* which can also mean "doc exists but has no recognizable Status line". */
|
|
12
|
+
hasDoc: boolean;
|
|
13
|
+
/** Lowercased Status value from the doc, or null (no doc, or doc has no Status line). */
|
|
14
|
+
status: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface CatalogModelSummary {
|
|
17
|
+
key: string;
|
|
18
|
+
name: string;
|
|
19
|
+
/** Input .em file path, as given on the command line. */
|
|
20
|
+
file: string;
|
|
21
|
+
/** Diagram filename, relative to <outDir>/<model-key>/. */
|
|
22
|
+
diagramFile: string;
|
|
23
|
+
slices: CatalogSliceSummary[];
|
|
24
|
+
}
|
|
25
|
+
export declare function renderIndexPage(models: CatalogModelSummary[], title: string, format: "svg" | "png"): string;
|
|
26
|
+
export interface SlicePageArgs {
|
|
27
|
+
modelName: string;
|
|
28
|
+
/** Full model diagram path relative to this page (which lives in
|
|
29
|
+
* <outDir>/<model-key>/slices/) — shown as a secondary "view full diagram" link
|
|
30
|
+
* (a plain anchor, not an embed, so no MIME type is needed here). */
|
|
31
|
+
diagramFile: string;
|
|
32
|
+
/** This slice's own diagram, relative to this page (same directory) — always
|
|
33
|
+
* svg (buildSliceDiagram writes svg regardless of the main diagram's chosen
|
|
34
|
+
* format). Shown as the page's primary diagram. */
|
|
35
|
+
sliceDiagramFile: string;
|
|
36
|
+
slice: Slice;
|
|
37
|
+
sliceKey: string;
|
|
38
|
+
pattern: SlicePattern;
|
|
39
|
+
/** Internal Element.id -> em export ref, for stable per-row anchors. */
|
|
40
|
+
elementRefs: Map<string, string>;
|
|
41
|
+
doc: SliceDoc | null;
|
|
42
|
+
/** slices/<slug>.md, relative to the source .em file — shown when no doc was found. */
|
|
43
|
+
docExpectedPath: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function renderSlicePage(args: SlicePageArgs): string;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// HTML-string builders for `em catalog`'s static site: an index page and one
|
|
3
|
+
// per-slice detail page. Hand-written template literals, no templating
|
|
4
|
+
// library — same convention as the live-viewer page in
|
|
5
|
+
// src/render/serve.ts's VIEWER_HTML, just as functions instead of one static
|
|
6
|
+
// template, since these pages vary per model/slice.
|
|
7
|
+
import { slicePatternLabel } from "./classify.js";
|
|
8
|
+
export function escapeHtml(s) {
|
|
9
|
+
return s
|
|
10
|
+
.replace(/&/g, "&")
|
|
11
|
+
.replace(/</g, "<")
|
|
12
|
+
.replace(/>/g, ">")
|
|
13
|
+
.replace(/"/g, """)
|
|
14
|
+
.replace(/'/g, "'");
|
|
15
|
+
}
|
|
16
|
+
/** Human-readable status cell: "no doc" (nothing found), "unknown" (doc found, no Status
|
|
17
|
+
* line), or the doc's own Status value verbatim. Shared by the index and slice pages so the
|
|
18
|
+
* two never drift out of sync on how a missing/freeform doc reads. */
|
|
19
|
+
function statusLabel(hasDoc, status) {
|
|
20
|
+
if (!hasDoc)
|
|
21
|
+
return "no doc";
|
|
22
|
+
if (!status)
|
|
23
|
+
return "unknown";
|
|
24
|
+
return status;
|
|
25
|
+
}
|
|
26
|
+
/** MIME type for the diagram `<object>` — must match the actual file `buildCatalog` wrote
|
|
27
|
+
* (`diagram.svg` or `diagram.png`), or the browser won't render it. */
|
|
28
|
+
function diagramMimeType(format) {
|
|
29
|
+
return format === "png" ? "image/png" : "image/svg+xml";
|
|
30
|
+
}
|
|
31
|
+
/** `homeHref` is relative to the page being built (`index.html` from the index page itself,
|
|
32
|
+
* `../../index.html` from a slice page two levels down) so the site works opened straight
|
|
33
|
+
* off disk via `file://`, with no server — an absolute `href="/"` would jump to the
|
|
34
|
+
* filesystem root instead. */
|
|
35
|
+
function layout(title, bodyHtml, homeHref) {
|
|
36
|
+
return `<!doctype html>
|
|
37
|
+
<html lang="en">
|
|
38
|
+
<head>
|
|
39
|
+
<meta charset="utf-8" />
|
|
40
|
+
<title>${escapeHtml(title)}</title>
|
|
41
|
+
<style>
|
|
42
|
+
:root { color-scheme: light dark; }
|
|
43
|
+
body {
|
|
44
|
+
margin: 0; font-family: system-ui, sans-serif; line-height: 1.5;
|
|
45
|
+
background: #fafafa; color: #1f2933;
|
|
46
|
+
}
|
|
47
|
+
header {
|
|
48
|
+
padding: .75rem 1.25rem; background: #1f2933; color: #fff;
|
|
49
|
+
}
|
|
50
|
+
header a { color: #fff; text-decoration: none; }
|
|
51
|
+
main { max-width: 960px; margin: 0 auto; padding: 1.5rem 1.25rem 3rem; }
|
|
52
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
53
|
+
th, td { text-align: left; padding: .4rem .6rem; border-bottom: 1px solid #ddd; font-size: 14px; }
|
|
54
|
+
th { font-weight: 600; background: #f0f2f5; }
|
|
55
|
+
.status { display: inline-block; padding: .1rem .5rem; border-radius: 999px; font-size: 12px; background: #e4e7eb; }
|
|
56
|
+
.pattern { font-size: 12px; color: #52606d; }
|
|
57
|
+
/* Fixed-height frame, not a width-driven one: a slice diagram is often
|
|
58
|
+
narrow and tall relative to the full model's wide/short diagram, so
|
|
59
|
+
sizing by width alone would blow a narrow one's height up hugely.
|
|
60
|
+
object-fit doesn't apply to <object> the way it does <img> (needed here,
|
|
61
|
+
not <img>, so the SVG's clickable note links keep working) — giving it a
|
|
62
|
+
DEFINITE width AND height together stretches it non-uniformly, distorting
|
|
63
|
+
the aspect ratio instead of preserving it. width:auto + height:100% +
|
|
64
|
+
max-width:100% instead sizes it from its own intrinsic aspect ratio
|
|
65
|
+
(capped, never overflowing the frame); the flex centers it horizontally. */
|
|
66
|
+
.diagram-frame {
|
|
67
|
+
height: 420px; border: 1px solid #ddd; margin: 1rem 0; overflow: hidden; background: #fff;
|
|
68
|
+
display: flex; align-items: flex-start; justify-content: center;
|
|
69
|
+
}
|
|
70
|
+
object.diagram { width: auto; height: 100%; max-width: 100%; }
|
|
71
|
+
.full-diagram-link { margin: 0 0 1rem; font-size: 13px; }
|
|
72
|
+
.doc { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #ddd; }
|
|
73
|
+
.doc h1 { font-size: 1.3rem; }
|
|
74
|
+
.doc h2 { font-size: 1.05rem; }
|
|
75
|
+
.no-doc { color: #7b8794; font-style: italic; }
|
|
76
|
+
code { background: #eee; padding: .05rem .3rem; border-radius: 3px; }
|
|
77
|
+
</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<header><a href="${escapeHtml(homeHref)}">Event Model Catalog</a></header>
|
|
81
|
+
<main>
|
|
82
|
+
${bodyHtml}
|
|
83
|
+
</main>
|
|
84
|
+
</body>
|
|
85
|
+
</html>
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
export function renderIndexPage(models, title, format) {
|
|
89
|
+
const sections = models
|
|
90
|
+
.map((m) => {
|
|
91
|
+
const rows = m.slices
|
|
92
|
+
.map((s) => ` <tr>
|
|
93
|
+
<td><a href="${escapeHtml(m.key)}/slices/${escapeHtml(s.key)}.html">${escapeHtml(s.name)}</a></td>
|
|
94
|
+
<td class="pattern">${escapeHtml(slicePatternLabel(s.pattern))}</td>
|
|
95
|
+
<td><span class="status">${escapeHtml(statusLabel(s.hasDoc, s.status))}</span></td>
|
|
96
|
+
</tr>`)
|
|
97
|
+
.join("\n");
|
|
98
|
+
const diagramHref = `${escapeHtml(m.key)}/${escapeHtml(m.diagramFile)}`;
|
|
99
|
+
return ` <section>
|
|
100
|
+
<h2>${escapeHtml(m.name)}</h2>
|
|
101
|
+
<p>${m.slices.length} slice${m.slices.length === 1 ? "" : "s"} · <code>${escapeHtml(m.file)}</code> · <a href="${diagramHref}">open diagram full-size</a></p>
|
|
102
|
+
<div class="diagram-frame"><object class="diagram" type="${diagramMimeType(format)}" data="${diagramHref}"></object></div>
|
|
103
|
+
<table>
|
|
104
|
+
<thead><tr><th>Slice</th><th>Pattern</th><th>Status</th></tr></thead>
|
|
105
|
+
<tbody>
|
|
106
|
+
${rows}
|
|
107
|
+
</tbody>
|
|
108
|
+
</table>
|
|
109
|
+
</section>`;
|
|
110
|
+
})
|
|
111
|
+
.join("\n");
|
|
112
|
+
return layout(title, ` <h1>${escapeHtml(title)}</h1>
|
|
113
|
+
${sections}`, "index.html");
|
|
114
|
+
}
|
|
115
|
+
function formatFields(el) {
|
|
116
|
+
if (!el.fields || el.fields.length === 0)
|
|
117
|
+
return "";
|
|
118
|
+
return el.fields.map((f) => (f.type ? `${f.name}: ${f.type}` : f.name)).join(", ");
|
|
119
|
+
}
|
|
120
|
+
function elementDetail(el) {
|
|
121
|
+
const parts = [];
|
|
122
|
+
if (el.persona)
|
|
123
|
+
parts.push(`@${el.persona}`);
|
|
124
|
+
if (el.context)
|
|
125
|
+
parts.push(`@${el.context}`);
|
|
126
|
+
if (el.from && el.from.length > 0)
|
|
127
|
+
parts.push(`from ${el.from.join(", ")}`);
|
|
128
|
+
if (el.again)
|
|
129
|
+
parts.push("again");
|
|
130
|
+
if (el.public)
|
|
131
|
+
parts.push("public");
|
|
132
|
+
const fields = formatFields(el);
|
|
133
|
+
if (fields)
|
|
134
|
+
parts.push(`{ ${fields} }`);
|
|
135
|
+
return parts.map(escapeHtml).join(" · ");
|
|
136
|
+
}
|
|
137
|
+
function renderElementsTable(slice, elementRefs) {
|
|
138
|
+
const rows = slice.elements
|
|
139
|
+
.map((el) => {
|
|
140
|
+
const ref = elementRefs.get(el.id);
|
|
141
|
+
const anchor = ref ? ` id="${escapeHtml(ref)}"` : "";
|
|
142
|
+
const annotations = [];
|
|
143
|
+
if (el.issue)
|
|
144
|
+
annotations.push(`<div><span class="status">issue</span> ${escapeHtml(el.issue)}</div>`);
|
|
145
|
+
if (el.divergence)
|
|
146
|
+
annotations.push(`<div><span class="status">divergence</span> ${escapeHtml(el.divergence)}</div>`);
|
|
147
|
+
return ` <tr${anchor}>
|
|
148
|
+
<td>${escapeHtml(el.kind)}</td>
|
|
149
|
+
<td>${escapeHtml(el.name)}</td>
|
|
150
|
+
<td>${elementDetail(el)}${annotations.join("")}</td>
|
|
151
|
+
</tr>`;
|
|
152
|
+
})
|
|
153
|
+
.join("\n");
|
|
154
|
+
return ` <table>
|
|
155
|
+
<thead><tr><th>Kind</th><th>Name</th><th>Detail</th></tr></thead>
|
|
156
|
+
<tbody>
|
|
157
|
+
${rows}
|
|
158
|
+
</tbody>
|
|
159
|
+
</table>`;
|
|
160
|
+
}
|
|
161
|
+
export function renderSlicePage(args) {
|
|
162
|
+
const { modelName, diagramFile, sliceDiagramFile, slice, pattern, elementRefs, doc, docExpectedPath } = args;
|
|
163
|
+
const docSection = doc
|
|
164
|
+
? ` <div class="doc">${doc.html}</div>`
|
|
165
|
+
: ` <p class="no-doc">No slice doc found at <code>${escapeHtml(docExpectedPath)}</code>.</p>`;
|
|
166
|
+
return layout(`${slice.name} — ${modelName}`, ` <p><a href="../../index.html">← ${escapeHtml(modelName)}</a></p>
|
|
167
|
+
<h1>${escapeHtml(slice.name)}</h1>
|
|
168
|
+
<p class="pattern">${escapeHtml(slicePatternLabel(pattern))} · <span class="status">${escapeHtml(statusLabel(!!doc, doc?.status ?? null))}</span></p>
|
|
169
|
+
<div class="diagram-frame"><object class="diagram" type="image/svg+xml" data="${escapeHtml(sliceDiagramFile)}"></object></div>
|
|
170
|
+
<p class="full-diagram-link"><a href="${escapeHtml(diagramFile)}">View full model diagram →</a></p>
|
|
171
|
+
${renderElementsTable(slice, elementRefs)}
|
|
172
|
+
${docSection}`, "../../index.html");
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/catalog/pages.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SliceDoc {
|
|
2
|
+
/** The doc's raw markdown, as read from disk. */
|
|
3
|
+
raw: string;
|
|
4
|
+
/** Lowercased value of the doc's `- **Status:** ...` line, or null if no such line is found
|
|
5
|
+
* (a freeform doc that doesn't follow the slice.md template). */
|
|
6
|
+
status: string | null;
|
|
7
|
+
/** The whole doc rendered to HTML. */
|
|
8
|
+
html: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseSliceDoc(markdown: string): SliceDoc;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Parses a slice design doc (slices/<slice-name>.md, see
|
|
3
|
+
// .claude/skills/event-modeling/templates/slice.md) for `em catalog`. `em`
|
|
4
|
+
// has never parsed this markdown structurally before — it only checked the
|
|
5
|
+
// file exists (cli.ts's warnMissingNotes). This is deliberately shallow: pull
|
|
6
|
+
// the one bold-label line the index page needs (`- **Status:** ...`) with a
|
|
7
|
+
// regex rather than a full frontmatter parser, and render the whole doc body
|
|
8
|
+
// as HTML for the per-slice detail page. Pure — no fs access; the caller
|
|
9
|
+
// reads the file and hands us its text.
|
|
10
|
+
import { marked } from "marked";
|
|
11
|
+
const STATUS_LINE = /^-\s*\*\*Status:\*\*\s*(.+?)\s*$/im;
|
|
12
|
+
export function parseSliceDoc(markdown) {
|
|
13
|
+
const match = markdown.match(STATUS_LINE);
|
|
14
|
+
return {
|
|
15
|
+
raw: markdown,
|
|
16
|
+
status: match ? match[1].trim().toLowerCase() : null,
|
|
17
|
+
html: marked.parse(markdown, { async: false }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=sliceDoc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sliceDoc.js","sourceRoot":"","sources":["../../src/catalog/sliceDoc.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,yDAAyD;AACzD,2EAA2E;AAC3E,2EAA2E;AAC3E,8EAA8E;AAC9E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,wCAAwC;AAExC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAYhC,MAAM,WAAW,GAAG,oCAAoC,CAAC;AAEzD,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAC1C,OAAO;QACL,GAAG,EAAE,QAAQ;QACb,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;QACpD,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAW;KACzD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Resolved `em catalog` flag validation, or a user-facing `error`. */
|
|
2
|
+
export type CatalogPlan = {
|
|
3
|
+
error: string;
|
|
4
|
+
} | {
|
|
5
|
+
ok: true;
|
|
6
|
+
format: "svg" | "png";
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Validate the `em catalog` `--format` flag. Pure — no I/O. Only svg/png are
|
|
10
|
+
* accepted: the per-slice page embeds the diagram in an `<object>`/`<img>`,
|
|
11
|
+
* which needs a format a browser can render directly — pdf (and any other
|
|
12
|
+
* rsvg-convert-only format) is out of scope for a static browsable site.
|
|
13
|
+
*/
|
|
14
|
+
export declare function planCatalogArgs(opts: {
|
|
15
|
+
format?: string;
|
|
16
|
+
}): CatalogPlan;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Testable input handling for `em catalog`: --format validation, factored
|
|
3
|
+
// out of cli.ts so it's unit-testable without spawning a subprocess (mirrors
|
|
4
|
+
// src/cli/glossary-inputs.ts's planGlossaryArgs shape).
|
|
5
|
+
/**
|
|
6
|
+
* Validate the `em catalog` `--format` flag. Pure — no I/O. Only svg/png are
|
|
7
|
+
* accepted: the per-slice page embeds the diagram in an `<object>`/`<img>`,
|
|
8
|
+
* which needs a format a browser can render directly — pdf (and any other
|
|
9
|
+
* rsvg-convert-only format) is out of scope for a static browsable site.
|
|
10
|
+
*/
|
|
11
|
+
export function planCatalogArgs(opts) {
|
|
12
|
+
const format = opts.format ?? "svg";
|
|
13
|
+
if (format !== "svg" && format !== "png") {
|
|
14
|
+
return { error: `em catalog: unsupported --format '${format}' (catalog diagrams support svg or png only)` };
|
|
15
|
+
}
|
|
16
|
+
return { ok: true, format };
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=catalog-inputs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-inputs.js","sourceRoot":"","sources":["../../src/cli/catalog-inputs.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,0EAA0E;AAC1E,6EAA6E;AAC7E,wDAAwD;AAKxD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,IAAyB;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;IACpC,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QACzC,OAAO,EAAE,KAAK,EAAE,qCAAqC,MAAM,8CAA8C,EAAE,CAAC;IAC9G,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC9B,CAAC"}
|