@milehimikey/em-portal 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/dist/build.d.ts +21 -0
  4. package/dist/build.js +132 -0
  5. package/dist/build.js.map +1 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +43 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/crossModel.d.ts +14 -0
  10. package/dist/crossModel.js +49 -0
  11. package/dist/crossModel.js.map +1 -0
  12. package/dist/em/docBody.d.ts +5 -0
  13. package/dist/em/docBody.js +43 -0
  14. package/dist/em/docBody.js.map +1 -0
  15. package/dist/em/exportDoc.d.ts +135 -0
  16. package/dist/em/exportDoc.js +22 -0
  17. package/dist/em/exportDoc.js.map +1 -0
  18. package/dist/em/run.d.ts +41 -0
  19. package/dist/em/run.js +99 -0
  20. package/dist/em/run.js.map +1 -0
  21. package/dist/em/statusDoc.d.ts +56 -0
  22. package/dist/em/statusDoc.js +11 -0
  23. package/dist/em/statusDoc.js.map +1 -0
  24. package/dist/pages/html.d.ts +11 -0
  25. package/dist/pages/html.js +84 -0
  26. package/dist/pages/html.js.map +1 -0
  27. package/dist/pages/index.d.ts +3 -0
  28. package/dist/pages/index.js +128 -0
  29. package/dist/pages/index.js.map +1 -0
  30. package/dist/pages/model.d.ts +17 -0
  31. package/dist/pages/model.js +54 -0
  32. package/dist/pages/model.js.map +1 -0
  33. package/dist/pages/slice.d.ts +16 -0
  34. package/dist/pages/slice.js +144 -0
  35. package/dist/pages/slice.js.map +1 -0
  36. package/dist/pages/types.d.ts +22 -0
  37. package/dist/pages/types.js +4 -0
  38. package/dist/pages/types.js.map +1 -0
  39. package/dist/refs.d.ts +36 -0
  40. package/dist/refs.js +69 -0
  41. package/dist/refs.js.map +1 -0
  42. package/dist/slug.d.ts +5 -0
  43. package/dist/slug.js +30 -0
  44. package/dist/slug.js.map +1 -0
  45. package/package.json +60 -0
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Per-model page: the model's full diagram plus its slice table — the
3
+ // drill-down step between the multi-model landing index and an individual
4
+ // slice page (model -> slice -> doc, per MIL-162's "state up front" design).
5
+ import { escapeHtml, layout } from "./html.js";
6
+ // Note: this page's slice links are relative to the model directory itself
7
+ // ("slices/<key>.html"), not the site-root-relative form src/refs.ts's
8
+ // sliceUrl() returns (which is for cross-page links, e.g. the landing
9
+ // page's cross-model-links table) — so this page builds its own hrefs
10
+ // rather than reusing that helper.
11
+ const PATTERN_LABEL = {
12
+ "state-change": "State Change",
13
+ "state-view": "State View",
14
+ automation: "Automation",
15
+ translation: "Translation",
16
+ unclassified: "Unclassified",
17
+ };
18
+ function statusBadge(hasDoc, status) {
19
+ if (!hasDoc)
20
+ return `<span class="badge">no doc</span>`;
21
+ if (!status)
22
+ return `<span class="badge">unknown</span>`;
23
+ const cls = status === "implemented" ? "ok" : "";
24
+ return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
25
+ }
26
+ function driftBadge(drift) {
27
+ if (!drift)
28
+ return "";
29
+ const cls = drift === "in-sync" ? "ok" : drift === "never-implemented" ? "warn" : "bad";
30
+ return `<span class="badge ${cls}">${escapeHtml(drift)}</span>`;
31
+ }
32
+ export function renderModelPage(args) {
33
+ const { modelName, file, diagramFile, slices } = args;
34
+ const rows = slices
35
+ .map((s) => ` <tr id="${escapeHtml(s.key)}">
36
+ <td><a href="slices/${escapeHtml(s.key)}.html">${escapeHtml(s.name)}</a></td>
37
+ <td class="pattern">${escapeHtml(PATTERN_LABEL[s.pattern])}</td>
38
+ <td>${statusBadge(s.hasDoc, s.status)}</td>
39
+ <td>${driftBadge(s.driftSignal)}</td>
40
+ </tr>`)
41
+ .join("\n");
42
+ const body = ` <p><code>${escapeHtml(file)}</code></p>
43
+ <h1>${escapeHtml(modelName)}</h1>
44
+ <div class="diagram-frame"><object class="diagram" type="image/svg+xml" data="${escapeHtml(diagramFile)}"></object></div>
45
+ <p class="full-diagram-link"><a href="${escapeHtml(diagramFile)}">Open full diagram &rarr;</a></p>
46
+ <table>
47
+ <thead><tr><th>Slice</th><th>Pattern</th><th>Status</th><th>Drift</th></tr></thead>
48
+ <tbody>
49
+ ${rows}
50
+ </tbody>
51
+ </table>`;
52
+ return layout(`${modelName} — em portal`, body, "../index.html", [{ label: modelName, href: "." }]);
53
+ }
54
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/pages/model.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,sEAAsE;AACtE,0EAA0E;AAC1E,6EAA6E;AAE7E,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAE/C,2EAA2E;AAC3E,uEAAuE;AACvE,sEAAsE;AACtE,sEAAsE;AACtE,mCAAmC;AAEnC,MAAM,aAAa,GAAiC;IAClD,cAAc,EAAE,cAAc;IAC9B,YAAY,EAAE,YAAY;IAC1B,UAAU,EAAE,YAAY;IACxB,WAAW,EAAE,aAAa;IAC1B,YAAY,EAAE,cAAc;CAC7B,CAAC;AAEF,SAAS,WAAW,CAAC,MAAe,EAAE,MAAqB;IACzD,IAAI,CAAC,MAAM;QAAE,OAAO,mCAAmC,CAAC;IACxD,IAAI,CAAC,MAAM;QAAE,OAAO,oCAAoC,CAAC;IACzD,MAAM,GAAG,GAAG,MAAM,KAAK,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACjD,OAAO,sBAAsB,GAAG,KAAK,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC;AACnE,CAAC;AAED,SAAS,UAAU,CAAC,KAAoB;IACtC,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,MAAM,GAAG,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK,mBAAmB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;IACxF,OAAO,sBAAsB,GAAG,KAAK,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC;AAClE,CAAC;AAmBD,MAAM,UAAU,eAAe,CAAC,IAAmB;IACjD,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM;SAChB,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC;8BACjB,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;8BAC7C,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;cACpD,WAAW,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;cAC/B,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC;YAC3B,CACP;SACA,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,IAAI,GAAG,gBAAgB,UAAU,CAAC,IAAI,CAAC;UACrC,UAAU,CAAC,SAAS,CAAC;oFACqD,UAAU,CAAC,WAAW,CAAC;4CAC/D,UAAU,CAAC,WAAW,CAAC;;;;EAIjE,IAAI;;aAEO,CAAC;IAEZ,OAAO,MAAM,CAAC,GAAG,SAAS,cAAc,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;AACtG,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { EmSlice, SliceDocJoin, SlicePattern } from "../em/exportDoc.js";
2
+ export interface SlicePageArgs {
3
+ modelName: string;
4
+ modelKey: string;
5
+ diagramFile: string;
6
+ sliceDiagramFile: string;
7
+ slice: EmSlice;
8
+ pattern: SlicePattern;
9
+ doc: SliceDocJoin;
10
+ /** Rendered markdown body of the bound doc (src/em/docBody.ts) — `em
11
+ * export`'s own `doc` field never carries this (frontmatter only). `null`
12
+ * when no doc is bound, the bound file couldn't be read, or it has no
13
+ * body content after its frontmatter block. */
14
+ docBodyHtml: string | null;
15
+ }
16
+ export declare function renderSlicePage(args: SlicePageArgs): string;
@@ -0,0 +1,144 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Per-slice page: this slice's own diagram, its rendered doc, driftSignal,
3
+ // and a link to wherever it was implemented (MIL-172's scope: "diagram,
4
+ // rendered doc, driftSignal, PR link"). MIL-173: every element row also
5
+ // carries its own `em export` ref as a DOM id plus a visible, copyable
6
+ // deep-link permalink (src/refs.ts) — the URL scheme an agent's `em query`/
7
+ // MCP citation and a stakeholder's portal link share.
8
+ import { escapeHtml, layout } from "./html.js";
9
+ import { elementDeepLink } from "../refs.js";
10
+ const PATTERN_LABEL = {
11
+ "state-change": "State Change",
12
+ "state-view": "State View",
13
+ automation: "Automation",
14
+ translation: "Translation",
15
+ unclassified: "Unclassified",
16
+ };
17
+ function statusBadge(status) {
18
+ if (!status)
19
+ return `<span class="badge">unknown</span>`;
20
+ const cls = status === "implemented" ? "ok" : "";
21
+ return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
22
+ }
23
+ function driftBadge(drift) {
24
+ if (!drift)
25
+ return `<span class="badge">n/a</span>`;
26
+ const cls = drift === "in-sync" ? "ok" : drift === "never-implemented" ? "warn" : "bad";
27
+ return `<span class="badge ${cls}">${escapeHtml(drift)}</span>`;
28
+ }
29
+ /** Renders a doc's `implementedIn` value as a link when it looks like a URL
30
+ * (a PR/commit link — the common case per docs/slice-doc-schema.md), or as
31
+ * plain code text otherwise (a bare file path — no portal-side path
32
+ * resolution is attempted, since the target repo isn't necessarily the one
33
+ * the portal was built from). */
34
+ function implementedInHtml(implementedIn) {
35
+ if (!implementedIn)
36
+ return `<span class="no-doc">not linked</span>`;
37
+ if (/^https?:\/\//.test(implementedIn)) {
38
+ return `<a href="${escapeHtml(implementedIn)}">${escapeHtml(implementedIn)}</a>`;
39
+ }
40
+ return `<code>${escapeHtml(implementedIn)}</code>`;
41
+ }
42
+ /** `ratifiedBy`/`ratifiedOn` (schema 1.8, MIL-165) — who signed off on this
43
+ * doc's current status/version, and when. Both are hand-filled and often
44
+ * absent even on an `implemented` doc predating the feature (or ratified by
45
+ * hand before it existed) — that's routine, not a gap, so it renders as a
46
+ * plain "not recorded" note rather than a warning badge. */
47
+ function ratifiedByHtml(ratifiedBy, ratifiedOn) {
48
+ if (!ratifiedBy)
49
+ return `<span class="no-doc">not recorded</span>`;
50
+ return ratifiedOn ? `${escapeHtml(ratifiedBy)} <span class="pattern">(${escapeHtml(ratifiedOn)})</span>` : escapeHtml(ratifiedBy);
51
+ }
52
+ /** `owner`/`tracking` (schema 1.9, MIL-171) — rendered as one optional line
53
+ * beneath the stat row, omitted entirely when neither is set (same
54
+ * "nothing to report" convention `em status`'s own text report uses for
55
+ * its doc-issues line), rather than two more always-present "not set"
56
+ * tiles cluttering the common case. */
57
+ function ownerTrackingHtml(owner, tracking) {
58
+ if (!owner && !tracking)
59
+ return "";
60
+ const parts = [];
61
+ if (owner)
62
+ parts.push(`Owner: <strong>${escapeHtml(owner)}</strong>`);
63
+ if (tracking) {
64
+ parts.push(/^https?:\/\//.test(tracking)
65
+ ? `Tracking: <a href="${escapeHtml(tracking)}">${escapeHtml(tracking)}</a>`
66
+ : `Tracking: <code>${escapeHtml(tracking)}</code>`);
67
+ }
68
+ return ` <p>${parts.join(" &middot; ")}</p>\n`;
69
+ }
70
+ function formatFields(el) {
71
+ if (!el.fields || el.fields.length === 0)
72
+ return "";
73
+ return el.fields.map((f) => (f.type ? `${f.name}: ${f.type}` : f.name)).join(", ");
74
+ }
75
+ function elementDetail(el) {
76
+ const parts = [];
77
+ if (el.persona)
78
+ parts.push(`@${el.persona}`);
79
+ if (el.context)
80
+ parts.push(`@${el.context}`);
81
+ if (el.from && el.from.length > 0)
82
+ parts.push(`from ${el.from.map((f) => f.name).join(", ")}`);
83
+ if (el.again)
84
+ parts.push("again");
85
+ if (el.public)
86
+ parts.push("public");
87
+ const fields = formatFields(el);
88
+ if (fields)
89
+ parts.push(`{ ${fields} }`);
90
+ return parts.map(escapeHtml).join(" &middot; ");
91
+ }
92
+ function renderElementsTable(elements, modelKey) {
93
+ const rows = elements
94
+ .map((el) => {
95
+ const annotations = [];
96
+ if (el.issue)
97
+ annotations.push(`<div><span class="badge warn">issue</span> ${escapeHtml(el.issue)}</div>`);
98
+ if (el.divergence)
99
+ annotations.push(`<div><span class="badge">divergence</span> ${escapeHtml(el.divergence)}</div>`);
100
+ // The fragment jump target for THIS page is always local ("#<ref>") — a
101
+ // relative deep link never needs the "<model-key>/slices/<key>.html"
102
+ // prefix when it's already pointing at the page it's sitting on. The
103
+ // full portable citation (what elementDeepLink returns) is shown as
104
+ // the visible, copyable permalink text instead, so a reader can grab
105
+ // the whole address without leaving the page.
106
+ const deepLink = elementDeepLink(modelKey, el.ref);
107
+ return ` <tr id="${escapeHtml(el.ref)}">
108
+ <td>${escapeHtml(el.kind)}</td>
109
+ <td>${escapeHtml(el.name)}</td>
110
+ <td>${elementDetail(el)}${annotations.join("")}</td>
111
+ <td class="permalink"><a href="#${escapeHtml(el.ref)}" title="Deep link: ${escapeHtml(deepLink)}">#${escapeHtml(el.ref)}</a></td>
112
+ </tr>`;
113
+ })
114
+ .join("\n");
115
+ return ` <table>
116
+ <thead><tr><th>Kind</th><th>Name</th><th>Detail</th><th>Ref</th></tr></thead>
117
+ <tbody>
118
+ ${rows}
119
+ </tbody>
120
+ </table>`;
121
+ }
122
+ export function renderSlicePage(args) {
123
+ const { modelName, diagramFile, sliceDiagramFile, slice, pattern, doc, docBodyHtml } = args;
124
+ const docSection = docBodyHtml
125
+ ? ` <div class="doc">${docBodyHtml}</div>`
126
+ : ` <p class="no-doc">${doc.found ? `Doc bound at <code>${escapeHtml(doc.path)}</code> but has no rendered body.` : `No slice doc bound. Expected at <code>${escapeHtml(doc.path)}</code>.`}</p>`;
127
+ const body = ` <h1>${escapeHtml(slice.name)}</h1>
128
+ <p class="pattern">${escapeHtml(PATTERN_LABEL[pattern])}</p>
129
+ <div class="stat-row">
130
+ <div class="stat"><span class="label">Status</span>${statusBadge(doc.status)}</div>
131
+ <div class="stat"><span class="label">Drift signal</span>${driftBadge(doc.driftSignal)}</div>
132
+ <div class="stat"><span class="label">Implemented in</span>${implementedInHtml(doc.implementedIn)}</div>
133
+ <div class="stat"><span class="label">Ratified by</span>${ratifiedByHtml(doc.ratifiedBy, doc.ratifiedOn)}</div>
134
+ </div>
135
+ ${ownerTrackingHtml(doc.owner, doc.tracking)} <div class="diagram-frame"><object class="diagram" type="image/svg+xml" data="${escapeHtml(sliceDiagramFile)}"></object></div>
136
+ <p class="full-diagram-link"><a href="${escapeHtml(diagramFile)}">View full model diagram &rarr;</a></p>
137
+ ${renderElementsTable(slice.elements, args.modelKey)}
138
+ ${docSection}`;
139
+ return layout(`${slice.name} — ${modelName}`, body, "../../index.html", [
140
+ { label: modelName, href: ".." },
141
+ { label: slice.name, href: "." },
142
+ ]);
143
+ }
144
+ //# sourceMappingURL=slice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slice.js","sourceRoot":"","sources":["../../src/pages/slice.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { SlicePattern } from "../em/exportDoc.js";
2
+ import { CrossModelLink } from "../crossModel.js";
3
+ export interface SliceSummary {
4
+ key: string;
5
+ name: string;
6
+ pattern: SlicePattern;
7
+ hasDoc: boolean;
8
+ status: string | null;
9
+ driftSignal: string | null;
10
+ }
11
+ export interface ModelSummary {
12
+ key: string;
13
+ name: string;
14
+ file: string;
15
+ sliceCount: number;
16
+ slices: SliceSummary[];
17
+ }
18
+ export interface PortalSummary {
19
+ title: string;
20
+ models: ModelSummary[];
21
+ crossModelLinks: CrossModelLink[];
22
+ }
@@ -0,0 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Shared summary shapes passed between build.ts and the page builders.
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/pages/types.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,uEAAuE"}
package/dist/refs.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ export interface ParsedElementRef {
2
+ sliceKey: string;
3
+ kind: string;
4
+ slug: string;
5
+ }
6
+ /** Splits an `em export` element ref (`<sliceKey>/<kind>.<slug>`) into its
7
+ * three parts. `sliceKey` never contains `/` or `.` (kebab-slug, dedupe
8
+ * suffix `~2`/`~3`/... only); `kind` is a single bare word (`ui`, `command`,
9
+ * `event`, ...) and never contains `.`, so splitting on the FIRST `.` after
10
+ * the slice-key boundary is always unambiguous even though `slug` itself
11
+ * can carry a `~2`-style suffix. Throws on a string that doesn't have the
12
+ * `/` + `.` shape at all — a caller should never see that for a ref that
13
+ * actually came from `em export`. */
14
+ export declare function parseElementRef(ref: string): ParsedElementRef;
15
+ /** The page URL for a whole slice, relative to the site root. */
16
+ export declare function sliceUrl(modelKey: string, sliceKey: string): string;
17
+ /** The full portable deep link to one element: the slice page's URL plus a
18
+ * `#`-fragment of its own `em export` ref. Portable in the sense that it's
19
+ * exactly what you'd bookmark, paste into a doc, or hand to another tool —
20
+ * resolving it never requires knowing anything beyond the model key (a
21
+ * portal-build-time choice) and the ref itself (an em-export-time one). */
22
+ export declare function elementDeepLink(modelKey: string, ref: string): string;
23
+ export interface ParsedDeepLink {
24
+ modelKey: string;
25
+ sliceKey: string;
26
+ /** The element ref's `<kind>.<slug>` fragment, or `null` for a slice-level
27
+ * link with no element fragment. */
28
+ elementRef: string | null;
29
+ }
30
+ /** The inverse of `sliceUrl`/`elementDeepLink` — parses a portal-relative
31
+ * deep link back into its model/slice/element parts. Returns `null` for
32
+ * anything that isn't shaped like a page this portal generates (the site's
33
+ * own `index.html`, an external URL, ...), rather than throwing — a caller
34
+ * resolving an arbitrary string should treat "not a portal deep link" as a
35
+ * normal outcome. */
36
+ export declare function parseDeepLink(link: string): ParsedDeepLink | null;
package/dist/refs.js ADDED
@@ -0,0 +1,69 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // MIL-173: the portal's URL scheme, built entirely on `em export`'s own
3
+ // stable identifiers — a slice's `key` and an element's `ref`
4
+ // (`<sliceKey>/<kind>.<slug>`, docs/cli.md in the em repo). One addressing
5
+ // scheme for both audiences: an agent citing a ref via `em query`/MCP and a
6
+ // stakeholder clicking a portal link name the same element, and the scheme
7
+ // survives re-renders and model growth because it depends only on export's
8
+ // ref-stability guarantee (inserting/reordering slices never changes an
9
+ // existing ref) — never on layout, coordinates, or this portal's own
10
+ // directory-naming choices for a model (`src/slug.ts`, portal-local, not
11
+ // part of the addressing scheme itself).
12
+ //
13
+ // A slice page's URL already IS that slice's export key
14
+ // (`<model-key>/slices/<sliceKey>.html`) — no separate per-element page is
15
+ // needed, since an element's ref always starts with its own slice's key.
16
+ // An element-level deep link is that same page plus a `#`-fragment of the
17
+ // element's full ref, so `<model-key>/slices/<sliceKey>.html#<ref>` and
18
+ // `<model-key>/<ref>` carry the exact same three pieces of information
19
+ // (model, slice, element) — just spelled with `/slices/` and `.html#`
20
+ // standing in for the ref's own internal `/`.
21
+ /** Splits an `em export` element ref (`<sliceKey>/<kind>.<slug>`) into its
22
+ * three parts. `sliceKey` never contains `/` or `.` (kebab-slug, dedupe
23
+ * suffix `~2`/`~3`/... only); `kind` is a single bare word (`ui`, `command`,
24
+ * `event`, ...) and never contains `.`, so splitting on the FIRST `.` after
25
+ * the slice-key boundary is always unambiguous even though `slug` itself
26
+ * can carry a `~2`-style suffix. Throws on a string that doesn't have the
27
+ * `/` + `.` shape at all — a caller should never see that for a ref that
28
+ * actually came from `em export`. */
29
+ export function parseElementRef(ref) {
30
+ const slashIndex = ref.indexOf("/");
31
+ if (slashIndex === -1) {
32
+ throw new Error(`not an em export element ref (missing "/"): ${ref}`);
33
+ }
34
+ const sliceKey = ref.slice(0, slashIndex);
35
+ const rest = ref.slice(slashIndex + 1);
36
+ const dotIndex = rest.indexOf(".");
37
+ if (dotIndex === -1) {
38
+ throw new Error(`not an em export element ref (missing "." after "/"): ${ref}`);
39
+ }
40
+ return { sliceKey, kind: rest.slice(0, dotIndex), slug: rest.slice(dotIndex + 1) };
41
+ }
42
+ /** The page URL for a whole slice, relative to the site root. */
43
+ export function sliceUrl(modelKey, sliceKey) {
44
+ return `${modelKey}/slices/${sliceKey}.html`;
45
+ }
46
+ /** The full portable deep link to one element: the slice page's URL plus a
47
+ * `#`-fragment of its own `em export` ref. Portable in the sense that it's
48
+ * exactly what you'd bookmark, paste into a doc, or hand to another tool —
49
+ * resolving it never requires knowing anything beyond the model key (a
50
+ * portal-build-time choice) and the ref itself (an em-export-time one). */
51
+ export function elementDeepLink(modelKey, ref) {
52
+ const { sliceKey } = parseElementRef(ref);
53
+ return `${sliceUrl(modelKey, sliceKey)}#${ref}`;
54
+ }
55
+ const DEEP_LINK_PATTERN = /^([^/]+)\/slices\/([^/]+)\.html(?:#(.+))?$/;
56
+ /** The inverse of `sliceUrl`/`elementDeepLink` — parses a portal-relative
57
+ * deep link back into its model/slice/element parts. Returns `null` for
58
+ * anything that isn't shaped like a page this portal generates (the site's
59
+ * own `index.html`, an external URL, ...), rather than throwing — a caller
60
+ * resolving an arbitrary string should treat "not a portal deep link" as a
61
+ * normal outcome. */
62
+ export function parseDeepLink(link) {
63
+ const match = DEEP_LINK_PATTERN.exec(link);
64
+ if (!match)
65
+ return null;
66
+ const [, modelKey, sliceKey, elementRef] = match;
67
+ return { modelKey, sliceKey, elementRef: elementRef ?? null };
68
+ }
69
+ //# sourceMappingURL=refs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refs.js","sourceRoot":"","sources":["../src/refs.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,wEAAwE;AACxE,8DAA8D;AAC9D,2EAA2E;AAC3E,4EAA4E;AAC5E,2EAA2E;AAC3E,2EAA2E;AAC3E,wEAAwE;AACxE,qEAAqE;AACrE,yEAAyE;AACzE,yCAAyC;AACzC,EAAE;AACF,wDAAwD;AACxD,2EAA2E;AAC3E,yEAAyE;AACzE,0EAA0E;AAC1E,wEAAwE;AACxE,uEAAuE;AACvE,sEAAsE;AACtE,8CAA8C;AAQ9C;;;;;;;sCAOsC;AACtC,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,+CAA+C,GAAG,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,yDAAyD,GAAG,EAAE,CAAC,CAAC;IAClF,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC;AACrF,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,QAAgB;IACzD,OAAO,GAAG,QAAQ,WAAW,QAAQ,OAAO,CAAC;AAC/C,CAAC;AAED;;;;4EAI4E;AAC5E,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,GAAW;IAC3D,MAAM,EAAE,QAAQ,EAAE,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IAC1C,OAAO,GAAG,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,GAAG,EAAE,CAAC;AAClD,CAAC;AAUD,MAAM,iBAAiB,GAAG,4CAA4C,CAAC;AAEvE;;;;;sBAKsB;AACtB,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,GAAG,KAAK,CAAC;IACjD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,IAAI,IAAI,EAAE,CAAC;AAChE,CAAC"}
package/dist/slug.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare function kebabSlug(name: string): string;
2
+ /** Returns a slug unique within `used`, suffixing `~2`, `~3`, ... on collision
3
+ * (same separator em's own dedupe() uses, for a familiar convention) and
4
+ * records the chosen value into `used` before returning it. */
5
+ export declare function dedupe(candidate: string, used: Set<string>): string;
package/dist/slug.js ADDED
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Portal-local slug/dedupe helpers for naming output directories (one per
3
+ // input model, mirroring `em catalog`'s convention). Deliberately NOT shared
4
+ // code with em's own src/util/slug.ts — em-portal is a separate package with
5
+ // no access to em's internals (see docs/decisions/mil-162-teachable-navigator.md
6
+ // in the em repo); slice keys and element refs always come straight from
7
+ // `em export`'s own JSON instead of being re-derived here.
8
+ export function kebabSlug(name) {
9
+ return name
10
+ .trim()
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, "-")
13
+ .replace(/^-+|-+$/g, "") || "model";
14
+ }
15
+ /** Returns a slug unique within `used`, suffixing `~2`, `~3`, ... on collision
16
+ * (same separator em's own dedupe() uses, for a familiar convention) and
17
+ * records the chosen value into `used` before returning it. */
18
+ export function dedupe(candidate, used) {
19
+ if (!used.has(candidate)) {
20
+ used.add(candidate);
21
+ return candidate;
22
+ }
23
+ let n = 2;
24
+ while (used.has(`${candidate}~${n}`))
25
+ n++;
26
+ const deduped = `${candidate}~${n}`;
27
+ used.add(deduped);
28
+ return deduped;
29
+ }
30
+ //# sourceMappingURL=slug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slug.js","sourceRoot":"","sources":["../src/slug.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,0EAA0E;AAC1E,6EAA6E;AAC7E,6EAA6E;AAC7E,iFAAiF;AACjF,yEAAyE;AACzE,2DAA2D;AAE3D,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI;SACR,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC;AACxC,CAAC;AAED;;gEAEgE;AAChE,MAAM,UAAU,MAAM,CAAC,SAAiB,EAAE,IAAiB;IACzD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QACzB,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACpB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,SAAS,IAAI,CAAC,EAAE,CAAC;QAAE,CAAC,EAAE,CAAC;IAC1C,MAAM,OAAO,GAAG,GAAG,SAAS,IAAI,CAAC,EAAE,CAAC;IACpC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClB,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@milehimikey/em-portal",
3
+ "version": "0.1.0",
4
+ "description": "Stakeholder-facing static portal for em event models — a multi-model, read-only browser built from `em export`/`em status --json`",
5
+ "type": "module",
6
+ "bin": {
7
+ "em-portal": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/cli.js",
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "typecheck": "tsc -p tsconfig.json --noEmit",
18
+ "dev": "tsx src/cli.ts",
19
+ "em-portal": "tsx src/cli.ts",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "pretest": "npm run build",
23
+ "prepublishOnly": "npm run build && npm run typecheck && npm test"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "license": "MIT",
29
+ "author": "milehimikey",
30
+ "homepage": "https://github.com/milehimikey/em-portal#readme",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/milehimikey/em-portal.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/milehimikey/em-portal/issues"
37
+ },
38
+ "keywords": [
39
+ "event-modeling",
40
+ "portal",
41
+ "static-site",
42
+ "dashboard",
43
+ "cli"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "commander": "^12.1.0",
50
+ "marked": "^18.0.11"
51
+ },
52
+ "devDependencies": {
53
+ "@milehimikey/em": "file:vendor/milehimikey-em-1.8.1.tgz",
54
+ "@types/node": "^22.0.0",
55
+ "jsdom": "^26.1.0",
56
+ "tsx": "^4.19.0",
57
+ "typescript": "^5.6.0",
58
+ "vitest": "^4.1.10"
59
+ }
60
+ }