@miadi/stateloom-web 0.1.0 → 0.1.2

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 (88) hide show
  1. package/package.json +1 -1
  2. package/server/.next/BUILD_ID +1 -1
  3. package/server/.next/build-manifest.json +4 -4
  4. package/server/.next/prerender-manifest.json +3 -3
  5. package/server/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/server/.next/server/app/_global-error/page.js +1 -1
  7. package/server/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/server/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/server/.next/server/app/_global-error.html +2 -2
  10. package/server/.next/server/app/_global-error.rsc +1 -1
  11. package/server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/server/.next/server/app/_not-found/page/build-manifest.json +2 -2
  17. package/server/.next/server/app/_not-found/page.js +1 -1
  18. package/server/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/server/.next/server/app/_not-found.html +1 -1
  21. package/server/.next/server/app/_not-found.rsc +2 -2
  22. package/server/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  23. package/server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  24. package/server/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  25. package/server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  26. package/server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  27. package/server/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  28. package/server/.next/server/app/api/file/route.js.nft.json +1 -1
  29. package/server/.next/server/app/api/rispec/route.js.nft.json +1 -1
  30. package/server/.next/server/app/api/watch/route.js +3 -2
  31. package/server/.next/server/app/api/watch/route.js.nft.json +1 -1
  32. package/server/.next/server/app/index.html +1 -1
  33. package/server/.next/server/app/index.rsc +3 -3
  34. package/server/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/server/.next/server/app/index.segments/_full.segment.rsc +3 -3
  36. package/server/.next/server/app/index.segments/_head.segment.rsc +1 -1
  37. package/server/.next/server/app/index.segments/_index.segment.rsc +2 -2
  38. package/server/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/server/.next/server/app/page/build-manifest.json +2 -2
  40. package/server/.next/server/app/page.js +1 -1
  41. package/server/.next/server/app/page.js.nft.json +1 -1
  42. package/server/.next/server/app/page_client-reference-manifest.js +1 -1
  43. package/server/.next/server/chunks/[root-of-the-server]__2046b812._.js +2 -2
  44. package/server/.next/server/chunks/[root-of-the-server]__87bf4b91._.js +1 -1
  45. package/server/.next/server/chunks/[root-of-the-server]__95b05635._.js +8 -0
  46. package/server/.next/server/chunks/ssr/[root-of-the-server]__2955c0d8._.js +3 -0
  47. package/server/.next/server/chunks/ssr/{[root-of-the-server]__b4e7f753._.js → [root-of-the-server]__385c85a7._.js} +2 -2
  48. package/server/.next/server/chunks/ssr/[root-of-the-server]__a977f174._.js +3 -0
  49. package/server/.next/server/chunks/ssr/[root-of-the-server]__bfd16096._.js +3 -0
  50. package/server/.next/server/chunks/ssr/[root-of-the-server]__edbb995c._.js +4 -4
  51. package/server/.next/server/chunks/ssr/{[root-of-the-server]__9cafe12a._.js → [root-of-the-server]__f25b51be._.js} +2 -2
  52. package/server/.next/server/middleware-build-manifest.js +2 -2
  53. package/server/.next/server/pages/404.html +1 -1
  54. package/server/.next/server/pages/500.html +2 -2
  55. package/server/.next/server/server-reference-manifest.js +1 -1
  56. package/server/.next/server/server-reference-manifest.json +1 -1
  57. package/server/.next/static/chunks/0a0f393b77d3422b.css +2 -0
  58. package/server/.next/static/chunks/{8a03c0d2436d4c0f.js → 662d1b0ce457c16b.js} +1 -1
  59. package/server/.next/static/chunks/aea5b1716c68391c.js +1 -0
  60. package/server/.next/static/chunks/e29b67abadc7c68a.js +10 -0
  61. package/server/.next/static/chunks/{turbopack-8f57da603e40457d.js → turbopack-34fc7de1568e2a7f.js} +1 -1
  62. package/server/package-lock.json +38 -5
  63. package/server/package.json +2 -1
  64. package/server/src/app/api/file/route.ts +20 -6
  65. package/server/src/app/api/watch/route.ts +11 -3
  66. package/server/src/app/globals.css +7 -64
  67. package/server/src/app/layout.tsx +3 -0
  68. package/server/src/components/BridgeProvider.tsx +10 -3
  69. package/server/src/components/Canvas.tsx +78 -1325
  70. package/server/src/components/DocSwitcher.tsx +84 -0
  71. package/server/src/components/SettingsPanel.tsx +11 -6
  72. package/server/src/components/SocketBridgeProvider.tsx +11 -4
  73. package/server/src/components/Toolbar.tsx +11 -3
  74. package/server/src/lib/docParam.ts +51 -0
  75. package/server/src/lib/layoutMemory.ts +20 -2
  76. package/server/src/lib/projectFile.ts +68 -1
  77. package/server/src/types/definition.ts +12 -2
  78. package/server/stateloom-web.json +2 -2
  79. package/server/tsconfig.tsbuildinfo +1 -1
  80. package/server/.next/server/chunks/[root-of-the-server]__0a18d740._.js +0 -8
  81. package/server/.next/server/chunks/ssr/[root-of-the-server]__517c9b38._.js +0 -3
  82. package/server/.next/server/chunks/ssr/[root-of-the-server]__632c82d7._.js +0 -3
  83. package/server/.next/server/chunks/ssr/[root-of-the-server]__ecf19999._.js +0 -3
  84. package/server/.next/static/chunks/367d28d76d2be05f.js +0 -10
  85. package/server/.next/static/chunks/b6bd7719e3445512.css +0 -1
  86. /package/server/.next/static/{GkITWCpEKNsMrqxlkqzGB → Q7HQy4udl4A_DaAl9bdUt}/_buildManifest.js +0 -0
  87. /package/server/.next/static/{GkITWCpEKNsMrqxlkqzGB → Q7HQy4udl4A_DaAl9bdUt}/_clientMiddlewareManifest.json +0 -0
  88. /package/server/.next/static/{GkITWCpEKNsMrqxlkqzGB → Q7HQy4udl4A_DaAl9bdUt}/_ssgManifest.js +0 -0
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { navigateToDoc, useRequestedDoc } from "@/lib/docParam";
5
+ import { rememberedDocs } from "@/lib/layoutMemory";
6
+
7
+ /**
8
+ * A human switches diagrams without an agent and without a terminal
9
+ * (chart_1785683062725). Presentation over state that already exists: recents
10
+ * are the documents this browser holds layout memory for, plus a free path
11
+ * field. Choosing navigates via `?doc=` — the providers re-key (step 3), the
12
+ * server allowlist guards (step 1), no reload, no new transport.
13
+ */
14
+ export default function DocSwitcher() {
15
+ const requested = useRequestedDoc();
16
+ const [open, setOpen] = useState(false);
17
+ const [typed, setTyped] = useState("");
18
+
19
+ const go = (doc: string | null) => {
20
+ setOpen(false);
21
+ setTyped("");
22
+ navigateToDoc(doc);
23
+ };
24
+
25
+ const recents = open
26
+ ? rememberedDocs().filter((d) => d.startsWith("/") && d !== requested)
27
+ : [];
28
+
29
+ return (
30
+ <span className="relative inline-flex">
31
+ <button
32
+ onClick={() => setOpen((v) => !v)}
33
+ className="toolbar-btn"
34
+ title={requested ? `Document: ${requested}` : "Switch document"}
35
+ >
36
+ ⇄
37
+ </button>
38
+ {open && (
39
+ <div className="absolute left-0 top-full z-50 mt-1 w-72 rounded border border-gray-700 bg-gray-900 p-2 shadow-lg">
40
+ <div className="mb-1 text-[10px] uppercase tracking-wide text-gray-500">
41
+ switch document
42
+ </div>
43
+ {requested && (
44
+ <button
45
+ onClick={() => go(null)}
46
+ className="block w-full truncate rounded px-1.5 py-1 text-left text-xs text-gray-300 hover:bg-gray-800"
47
+ title="Return to the default project file"
48
+ >
49
+ ⌂ default document
50
+ </button>
51
+ )}
52
+ {recents.map((d) => (
53
+ <button
54
+ key={d}
55
+ onClick={() => go(d)}
56
+ className="block w-full truncate rounded px-1.5 py-1 text-left text-xs text-gray-300 hover:bg-gray-800"
57
+ title={d}
58
+ >
59
+ {d.split("/").pop()}
60
+ <span className="ml-1 text-[10px] text-gray-600">{d}</span>
61
+ </button>
62
+ ))}
63
+ <form
64
+ onSubmit={(e) => {
65
+ e.preventDefault();
66
+ if (typed.trim()) go(typed.trim());
67
+ }}
68
+ className="mt-1 flex gap-1"
69
+ >
70
+ <input
71
+ value={typed}
72
+ onChange={(e) => setTyped(e.target.value)}
73
+ placeholder="/absolute/path/to/doc.smdf.json"
74
+ className="min-w-0 flex-1 rounded border border-gray-700 bg-gray-950 px-1.5 py-1 text-xs text-gray-200 placeholder:text-gray-600"
75
+ />
76
+ <button type="submit" className="toolbar-btn" title="Open this document">
77
+ →
78
+ </button>
79
+ </form>
80
+ </div>
81
+ )}
82
+ </span>
83
+ );
84
+ }
@@ -55,11 +55,13 @@ export default function SettingsPanel() {
55
55
  <span className="text-xs text-gray-500">Context Class</span>
56
56
  <input
57
57
  className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-200"
58
- value={context?.className ?? ""}
58
+ value={context?.class ?? ""}
59
59
  placeholder="e.g. MyStateMachineContext"
60
60
  onChange={(e) =>
61
61
  updateSettings({
62
- context: { ...context, className: e.target.value || undefined },
62
+ // `class`, not `className`: the generator names the context from
63
+ // `settings.context?.class`, and nothing has ever read the other.
64
+ context: { ...context, class: e.target.value || undefined },
63
65
  })
64
66
  }
65
67
  />
@@ -97,15 +99,18 @@ export default function SettingsPanel() {
97
99
  <h4 className="text-xs font-semibold text-gray-500">Object References</h4>
98
100
  <textarea
99
101
  className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-200 h-12 font-mono"
100
- value={(settings.objects ?? []).map((o) => `${o.name}: ${o.type}`).join("\n")}
101
- placeholder="name: type (one per line)"
102
+ value={(settings.objects ?? []).map((o) => `${o.instance}: ${o.class}`).join("\n")}
103
+ placeholder="instance: Class (one per line)"
102
104
  onChange={(e) => {
105
+ // `instance: Class` — the pair the generator constructs the context
106
+ // with. Written as `{ name, type }` until now, which is a shape
107
+ // `codegen.ts` has never read.
103
108
  const objects = e.target.value
104
109
  .split("\n")
105
110
  .filter((l) => l.includes(":"))
106
111
  .map((l) => {
107
- const [name, type] = l.split(":").map((s) => s.trim());
108
- return { name, type };
112
+ const [instance, cls] = l.split(":").map((s) => s.trim());
113
+ return { instance, class: cls };
109
114
  });
110
115
  updateSettings({ objects });
111
116
  }}
@@ -11,6 +11,7 @@ import {
11
11
  type StateMachineDefinition,
12
12
  } from "@miadi/stateloom-protocol";
13
13
  import { useDesignerStore } from "@/store/useDesignerStore";
14
+ import { docQuery, useRequestedDoc } from "@/lib/docParam";
14
15
 
15
16
  const OUTBOUND_DEBOUNCE_MS = 60;
16
17
 
@@ -26,6 +27,9 @@ const OUTBOUND_DEBOUNCE_MS = 60;
26
27
  */
27
28
  export default function SocketBridgeProvider({ url }: { url: string }) {
28
29
  const presence = useDesignerStore((s) => s.presence);
30
+ // The requested `?doc=` (step 2): passed through to the file API, which
31
+ // resolves and guards it; the join below keys the room by the RESOLVED path.
32
+ const requested = useRequestedDoc();
29
33
 
30
34
  useEffect(() => {
31
35
  if (!url) return;
@@ -42,7 +46,7 @@ export default function SocketBridgeProvider({ url }: { url: string }) {
42
46
  // Learn the docId (the resolved project-file path) from the file API.
43
47
  let docId = "";
44
48
  try {
45
- const r = await fetch("/api/file", { cache: "no-store" });
49
+ const r = await fetch(`/api/file${docQuery(requested)}`, { cache: "no-store" });
46
50
  if (r.ok) docId = (await r.json())?.path ?? "";
47
51
  } catch {
48
52
  // Connect anyway; the hub keys the doc by whatever id we send.
@@ -115,9 +119,12 @@ export default function SocketBridgeProvider({ url }: { url: string }) {
115
119
  if (unsub) unsub();
116
120
  if (session) session.disconnect();
117
121
  };
118
- // Re-run when the URL arrives: DesignBridge resolves it asynchronously from
119
- // /api/config, so the first render can legitimately have a different value.
120
- }, [url]);
122
+ // Re-run when the URL arrives (DesignBridge resolves it asynchronously from
123
+ // /api/config) AND when the requested document changes (step 3): the
124
+ // cleanup disconnects the old session, the new run joins the new room —
125
+ // switching needs no reload. applyRemote already rebuilds everything from
126
+ // an arbitrary definition, so the arriving full-def repaints the canvas.
127
+ }, [url, requested]);
121
128
 
122
129
  if (presence.length === 0) return null;
123
130
 
@@ -9,6 +9,8 @@ import {
9
9
  type ExportFormat,
10
10
  } from "@/lib/exportImage";
11
11
  import type { StateDef } from "@/types/definition";
12
+ import DocSwitcher from "./DocSwitcher";
13
+ import { docQuery, useRequestedDoc } from "@/lib/docParam";
12
14
 
13
15
  export default function Toolbar() {
14
16
  const definition = useDesignerStore((s) => s.definition);
@@ -36,6 +38,11 @@ export default function Toolbar() {
36
38
  const setRemoteMtime = useDesignerStore((s) => s.setRemoteMtime);
37
39
  const applyRemote = useDesignerStore((s) => s.applyRemote);
38
40
 
41
+ // Save and reload must follow the SAME document the page is viewing —
42
+ // without this, 💾 on a switched document would silently write the default
43
+ // file (the exact mismatch the allowlist exists to prevent).
44
+ const requested = useRequestedDoc();
45
+
39
46
  const fileInputRef = useRef<HTMLInputElement>(null);
40
47
  const [addingState, setAddingState] = useState(false);
41
48
  const [newStateName, setNewStateName] = useState("");
@@ -63,7 +70,7 @@ export default function Toolbar() {
63
70
  const json = exportJson();
64
71
  setSaving(true);
65
72
  try {
66
- const r = await fetch("/api/file", {
73
+ const r = await fetch(`/api/file${docQuery(requested)}`, {
67
74
  method: "PUT",
68
75
  headers: { "Content-Type": "application/json" },
69
76
  body: JSON.stringify({ content: json }),
@@ -115,7 +122,7 @@ export default function Toolbar() {
115
122
 
116
123
  const handleReloadFromDisk = async () => {
117
124
  try {
118
- const r = await fetch("/api/file", { cache: "no-store" });
125
+ const r = await fetch(`/api/file${docQuery(requested)}`, { cache: "no-store" });
119
126
  if (!r.ok) {
120
127
  setRemoteStatus("error", "Reload failed");
121
128
  return;
@@ -198,11 +205,12 @@ export default function Toolbar() {
198
205
  className="hidden"
199
206
  />
200
207
 
201
- {/* File name + bridge status */}
208
+ {/* File name + document switcher + bridge status */}
202
209
  <span className="text-xs text-gray-400 mr-1 truncate max-w-[50vw] md:max-w-none">
203
210
  {fileName ?? "untitled.smdf.json"}
204
211
  {dirty && <span className="text-yellow-500 ml-0.5">●</span>}
205
212
  </span>
213
+ <DocSwitcher />
206
214
  {remoteStatus === "synced" && (
207
215
  <span className="text-[10px] text-emerald-500" title="In sync with disk">⌁ synced</span>
208
216
  )}
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ /**
6
+ * The one client-side source of the requested document (chart steps 2/3/6 of
7
+ * chart_1785683010290): the `?doc=` URL parameter. The browser only ever
8
+ * PASSES the request through — resolution and the root allowlist live on the
9
+ * server (lib/projectFile.ts), and the browser learns the resolved path from
10
+ * the API response exactly as before. No parameter → the default document,
11
+ * byte-identical to the old behavior.
12
+ */
13
+
14
+ const DOC_CHANGE_EVENT = "stateloom:doc-change";
15
+
16
+ export function requestedDoc(): string | null {
17
+ if (typeof window === "undefined") return null;
18
+ return new URLSearchParams(window.location.search).get("doc");
19
+ }
20
+
21
+ /** Query-string tail for API calls: "?doc=…" or "". */
22
+ export function docQuery(doc: string | null): string {
23
+ return doc ? `?doc=${encodeURIComponent(doc)}` : "";
24
+ }
25
+
26
+ /** Switch documents without a reload (step 6): rewrite the URL, tell the
27
+ * providers. History gets an entry so back/forward walk documents too. */
28
+ export function navigateToDoc(doc: string | null): void {
29
+ const url = new URL(window.location.href);
30
+ if (doc) url.searchParams.set("doc", doc);
31
+ else url.searchParams.delete("doc");
32
+ window.history.pushState({}, "", url);
33
+ window.dispatchEvent(new Event(DOC_CHANGE_EVENT));
34
+ }
35
+
36
+ /** The requested doc as reactive state: updates on picker navigation and on
37
+ * back/forward. Server render sees null; the first client effect corrects. */
38
+ export function useRequestedDoc(): string | null {
39
+ const [doc, setDoc] = useState<string | null>(null);
40
+ useEffect(() => {
41
+ const update = () => setDoc(requestedDoc());
42
+ update();
43
+ window.addEventListener("popstate", update);
44
+ window.addEventListener(DOC_CHANGE_EVENT, update);
45
+ return () => {
46
+ window.removeEventListener("popstate", update);
47
+ window.removeEventListener(DOC_CHANGE_EVENT, update);
48
+ };
49
+ }, []);
50
+ return doc;
51
+ }
@@ -36,6 +36,21 @@ export interface LayoutMemory {
36
36
  viewport: Viewport;
37
37
  }
38
38
 
39
+ /** Documents this browser holds layout memory for — the picker's recents
40
+ * (chart_1785683062725: presentation over state that already exists). */
41
+ export function rememberedDocs(): string[] {
42
+ try {
43
+ const docs: string[] = [];
44
+ for (let i = 0; i < window.localStorage.length; i++) {
45
+ const k = window.localStorage.key(i);
46
+ if (k?.startsWith(KEY_PREFIX)) docs.push(k.slice(KEY_PREFIX.length));
47
+ }
48
+ return docs.sort();
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
39
54
  export function memoryKey(docId: string): string {
40
55
  return `${KEY_PREFIX}${docId}`;
41
56
  }
@@ -108,10 +123,13 @@ export function writeMemory(
108
123
  }
109
124
  }
110
125
 
111
- /** The document this browser is remembering for: the resolved project path. */
126
+ /** The document this browser is remembering for: the resolved project path.
127
+ * The requested `?doc=` rides along (step 2) so memory keys follow the
128
+ * document the page actually asked for, not always the default. */
112
129
  async function resolveDocId(): Promise<string> {
113
130
  try {
114
- const r = await fetch("/api/file", { cache: "no-store" });
131
+ const { requestedDoc, docQuery } = await import("./docParam");
132
+ const r = await fetch(`/api/file${docQuery(requestedDoc())}`, { cache: "no-store" });
115
133
  if (r.ok) {
116
134
  const path = (await r.json())?.path;
117
135
  if (typeof path === "string" && path.length > 0) return path;
@@ -1,6 +1,73 @@
1
- import { resolve } from "path";
1
+ import { existsSync, realpathSync } from "fs";
2
+ import { dirname, isAbsolute, resolve, sep } from "path";
2
3
  import { envAlias } from "@miadi/stateloom-protocol";
3
4
 
4
5
  export function getProjectFilePath(): string {
5
6
  return resolve(envAlias("PROJECT_FILE") ?? "./statemachine.smdf.json");
6
7
  }
8
+
9
+ /**
10
+ * Optional-document resolution for the file API (chart chart_1785683022927).
11
+ *
12
+ * The guard is not deferrable and ships in the same change as the parameter:
13
+ * the canvas binds beyond loopback and PUT /api/file writes to disk, so an
14
+ * unconstrained path parameter would be arbitrary file write across the
15
+ * tailnet. A `doc` is admitted only when it is an absolute `.json` path whose
16
+ * real location sits under an allowed root.
17
+ *
18
+ * Roots come from STATELOOM_DOC_ROOTS (SMCRAFT_ twin honored), colon-
19
+ * separated absolute paths. Unset, the allowlist is exactly the directory of
20
+ * the default project file — the pre-parameter surface, unchanged.
21
+ */
22
+ export function allowedDocRoots(): string[] {
23
+ const raw = envAlias("DOC_ROOTS");
24
+ const roots = raw
25
+ ? raw.split(":").map((r) => r.trim()).filter(Boolean)
26
+ : [dirname(getProjectFilePath())];
27
+ const real: string[] = [];
28
+ for (const root of roots) {
29
+ try {
30
+ real.push(realpathSync(resolve(root)));
31
+ } catch {
32
+ // A configured root that does not exist admits nothing — skipped, not fatal.
33
+ }
34
+ }
35
+ return real;
36
+ }
37
+
38
+ export type DocResolution =
39
+ | { ok: true; path: string }
40
+ | { ok: false; error: string };
41
+
42
+ /** Resolve an optional `doc` request parameter to an admitted absolute path.
43
+ * No doc → the default project file, byte-identical to the old behavior. */
44
+ export function resolveDocPath(doc: string | null | undefined): DocResolution {
45
+ if (!doc) return { ok: true, path: getProjectFilePath() };
46
+
47
+ if (!isAbsolute(doc)) {
48
+ return { ok: false, error: "doc must be an absolute path" };
49
+ }
50
+ if (!doc.endsWith(".json")) {
51
+ return { ok: false, error: "doc must name a .json document" };
52
+ }
53
+
54
+ const lexical = resolve(doc);
55
+ // Realpath the file itself when it exists (GET/PUT of a known doc), else its
56
+ // parent (PUT may create the file) — symlinks cannot smuggle a path outside
57
+ // the allowlist either way. A doc whose parent does not exist is refused.
58
+ let anchored: string;
59
+ try {
60
+ anchored = existsSync(lexical)
61
+ ? realpathSync(lexical)
62
+ : resolve(realpathSync(dirname(lexical)), lexical.slice(lexical.lastIndexOf(sep) + 1));
63
+ } catch {
64
+ return { ok: false, error: "doc directory does not exist" };
65
+ }
66
+
67
+ for (const root of allowedDocRoots()) {
68
+ if (anchored === root || anchored.startsWith(root + sep)) {
69
+ return { ok: true, path: anchored };
70
+ }
71
+ }
72
+ return { ok: false, error: "doc is outside the allowed roots" };
73
+ }
@@ -10,8 +10,11 @@ export interface ObjectRef {
10
10
  }
11
11
 
12
12
  export interface ContextConfig {
13
+ /** The generated context's class name — what `ts/src/codegen.ts` reads. */
13
14
  class?: string;
14
15
  instance?: string;
16
+ /** The designer's own extra: a base the generated context extends. */
17
+ baseClass?: string;
15
18
  }
16
19
 
17
20
  export interface SettingsModel {
@@ -19,8 +22,15 @@ export interface SettingsModel {
19
22
  name?: string;
20
23
  description?: string;
21
24
  asynchronous: boolean;
22
- objects?: { name: string; type: string }[];
23
- context?: { className?: string; baseClass?: string };
25
+ /**
26
+ * Objects the generated context is constructed with. `ObjectRef` is what
27
+ * `ts/src/codegen.ts` actually reads (`obj.instance`, `obj.class`); the
28
+ * `{ name, type }` pair this field used to carry was a shape no generator
29
+ * could consume, so an "Object References" line typed into the designer was
30
+ * silently dropped on the way to code.
31
+ */
32
+ objects?: ObjectRef[];
33
+ context?: ContextConfig;
24
34
  imports?: string[];
25
35
  using?: string[];
26
36
  targetLanguage?: string;
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.1.0",
3
- "designer": "0.1.0",
2
+ "package": "0.1.2",
3
+ "designer": "0.1.2",
4
4
  "next": "16.1.6"
5
5
  }