@nanobpm/nano-workforce 0.139.0 → 0.139.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.139.2](https://github.com/nanobpm/nano-workforce/compare/v0.139.1...v0.139.2) (2026-08-24)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **delivery-graphs:** stop the in-flight grid headers wrapping to 0-width columns ([#538](https://github.com/nanobpm/nano-workforce/issues/538)) ([#539](https://github.com/nanobpm/nano-workforce/issues/539)) ([c52e85b](https://github.com/nanobpm/nano-workforce/commit/c52e85bf38edef680914df0336ff95c98a364059))
6
+
7
+ ## [0.139.1](https://github.com/nanobpm/nano-workforce/compare/v0.139.0...v0.139.1) (2026-08-24)
8
+
9
+ ### Bug Fixes
10
+
11
+ * **delivery-graphs:** anchor App-View door defaults to import.meta.url ([#536](https://github.com/nanobpm/nano-workforce/issues/536)) ([#537](https://github.com/nanobpm/nano-workforce/issues/537)) ([58cd57e](https://github.com/nanobpm/nano-workforce/commit/58cd57e7f35937a9d1720e2fd4d938f70ce8f304)), closes [#279](https://github.com/nanobpm/nano-workforce/issues/279) [#467](https://github.com/nanobpm/nano-workforce/issues/467) [#279-only](https://github.com/nanobpm/nano-workforce/issues/279-only) [#279](https://github.com/nanobpm/nano-workforce/issues/279) [#279](https://github.com/nanobpm/nano-workforce/issues/279)
12
+
1
13
  ## [0.139.0](https://github.com/nanobpm/nano-workforce/compare/v0.138.3...v0.139.0) (2026-08-24)
2
14
 
3
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.139.0",
3
+ "version": "0.139.2",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",
@@ -15,9 +15,18 @@
15
15
 
16
16
  import { DG_COMPOSE_FILL_MESSAGE } from "./mount.js";
17
17
 
18
- // The read behind the list: every saved library entry, newest first (base-relative a leading-slash
19
- // path resolves against the console iframe ORIGIN, not the app-view base, and 404s the door, #279).
20
- const DEFAULT_LIBRARY_URL = "app/api/delivery-graph/library";
18
+ // The read behind the list: every saved library entry, newest first. Anchored to THIS MODULE's url
19
+ // (import.meta.url), NOT the document base. The library App-View shell (library-embed.html /
20
+ // library-standalone.html) and therefore this mount.js — is served ONE DIRECTORY DEEP at
21
+ // `<appMount>/delivery-graphs/`, while the API is a sibling of that dir at `<appMount>/app/api/…`. A
22
+ // document-base-relative default (`"app/api/delivery-graph/library"`) resolves against the
23
+ // `…/delivery-graphs/` shell base to `…/delivery-graphs/app/api/delivery-graph/library` → 404 on EVERY
24
+ // surface (standalone, local urban-SPA App-View, and the Studio console App-View, which never injects
25
+ // window.__NANO_APP_VIEW__ so this default is what runs). A leading-slash absolute is worse still:
26
+ // through Studio it resolves against the console ORIGIN, not the app-view base (#279). `../app/api/…`
27
+ // off import.meta.url steps up out of `/delivery-graphs/` and lands on `<appMount>/app/api/…` on all
28
+ // surfaces regardless of the document base — the same fix the preview/stage doors ship (#467/#536).
29
+ const DEFAULT_LIBRARY_URL = new URL("../app/api/delivery-graph/library", import.meta.url).href;
21
30
 
22
31
  // How often the list re-polls so a freshly-saved (or just-deleted) entry appears (or drops off)
23
32
  // without a manual refresh — mirrors the 5s cadence the staged list uses.
@@ -145,10 +154,11 @@ export function mountDeliveryGraphLibrary(host, config = {}) {
145
154
  ...(config.hookSecret && isSameOrigin(url) ? { "x-hook-secret": config.hookSecret } : {}),
146
155
  });
147
156
 
148
- // The delete door is the per-entry path under the list door: DELETE app/api/delivery-graph/library/<id>.
157
+ // The delete door is the per-entry path under the list door: DELETE .../delivery-graph/library/<id>.
149
158
  // Append the id to the path only, preserving any query/hash on the configured door so a
150
159
  // `?library=` override carrying a search string or fragment still resolves the per-entry URL.
151
- // Kept base-relative (no `new URL`) to preserve the App-View resolution class (#279).
160
+ // Derived by string-appending to `libraryUrl` (no fresh `new URL`) so it inherits the same
161
+ // module-anchored (`../app/api/…`, #467/#536) resolution as the list door it hangs off.
152
162
  const entryUrl = (id) => {
153
163
  const [beforeHash, hash = ""] = libraryUrl.split("#");
154
164
  const [path, search = ""] = beforeHash.split("?");
@@ -15,14 +15,23 @@
15
15
  // View) and standalone on a phone — only the host element and injected endpoint config differ. The app
16
16
  // has no browser build step, so this consumes the preview/stage doors straight off the wire.
17
17
 
18
- const DEFAULT_PREVIEW_URL = "app/api/actions/delivery-graph/preview";
19
- const DEFAULT_STAGE_URL = "app/api/actions/delivery-graph/stage";
18
+ // Door defaults are anchored to THIS MODULE's url (import.meta.url), NOT the document base. The compose
19
+ // shell (embed.html / standalone.html) — and therefore this mount.js — is served ONE DIRECTORY DEEP at
20
+ // `<appMount>/delivery-graphs/`, while the preview/stage doors are siblings of that dir at
21
+ // `<appMount>/app/api/…`. A document-base-relative default resolves against the `…/delivery-graphs/`
22
+ // shell base → `…/delivery-graphs/app/api/…` → 404 on every surface (the Studio console App-View never
23
+ // injects window.__NANO_APP_VIEW__, so the default is what runs); a leading-slash absolute resolves
24
+ // against the console ORIGIN through Studio (#279). `../app/api/…` off import.meta.url steps up out of
25
+ // `/delivery-graphs/` onto `<appMount>/app/api/…` on all surfaces — the cockpit's #467 fix.
26
+ const DEFAULT_PREVIEW_URL = new URL("../app/api/actions/delivery-graph/preview", import.meta.url).href;
27
+ const DEFAULT_STAGE_URL = new URL("../app/api/actions/delivery-graph/stage", import.meta.url).href;
20
28
 
21
29
  // The filesystem IMPORT door (issue #524, epic #519 S5). The Import control below reads a chosen
22
30
  // `.json` file's text client-side and POSTs it here; the door validates + compiles it and persists it
23
- // to the library with `source: imported`. Base-relative like the preview/stage defaults (App-View #279
24
- // resolution class — a leading-slash path 404s).
25
- const DEFAULT_IMPORT_URL = "app/api/actions/delivery-graph/library/import";
31
+ // to the library with `source: imported`. Module-anchored off import.meta.url like the preview/stage
32
+ // defaults (App-View #279/#467/#536 resolution class — a document-base-relative default 404s under the
33
+ // `…/delivery-graphs/` shell base, a leading-slash absolute resolves against the console origin).
34
+ const DEFAULT_IMPORT_URL = new URL("../app/api/actions/delivery-graph/library/import", import.meta.url).href;
26
35
 
27
36
  // The INBOUND reuse-fill seam (issue #523, epic #519 S4). Until now the compose textarea (`#dg-json`)
28
37
  // had NO inbound prefill path — its value was set only by "Load example" or the operator typing. The
@@ -17,21 +17,31 @@
17
17
  // demand×supply board (pages/board/mount.js): the SAME module mounts embedded in the console (App View)
18
18
  // and standalone — only the host element and injected endpoint config differ.
19
19
 
20
- // The read behind the list: every live staged proposal, newest first (base-relative — a leading-slash
21
- // path resolves against the console iframe ORIGIN, not the app-view base, and 404s the door, #279).
22
- const DEFAULT_STAGED_URL = "app/api/delivery-graph/staged";
20
+ // The door defaults are anchored to THIS MODULE's url (import.meta.url), NOT the document base. The
21
+ // staged App-View shell (staged-embed.html / staged-standalone.html) and therefore this mount.js
22
+ // is served ONE DIRECTORY DEEP at `<appMount>/delivery-graphs/`, while the API is a sibling of that
23
+ // dir at `<appMount>/app/api/…`. A document-base-relative default (`"app/api/delivery-graph/staged"`)
24
+ // resolves against the `…/delivery-graphs/` shell base to `…/delivery-graphs/app/api/delivery-graph/
25
+ // staged` → 404 on EVERY surface (standalone, local urban-SPA App-View, and the Studio console
26
+ // App-View, which never injects window.__NANO_APP_VIEW__ so this default is what runs) — the
27
+ // "Could not load staged proposals." bug. A leading-slash absolute is worse still: through Studio it
28
+ // resolves against the console ORIGIN, not the app-view base (#279). `../app/api/…` off
29
+ // import.meta.url steps up out of `/delivery-graphs/` and lands on `<appMount>/app/api/…` on all
30
+ // three surfaces regardless of the document base — the same fix the cockpit shipped for #467.
31
+ // The read behind the list: every live staged proposal, newest first.
32
+ const DEFAULT_STAGED_URL = new URL("../app/api/delivery-graph/staged", import.meta.url).href;
23
33
  // The operator dispatch door: POST { digest } → launches the staged graph engine-natively (#460).
24
- const DEFAULT_DISPATCH_URL = "app/api/actions/delivery-graph/dispatch";
34
+ const DEFAULT_DISPATCH_URL = new URL("../app/api/actions/delivery-graph/dispatch", import.meta.url).href;
25
35
  // The operator dismiss door: POST { digest } → discards a staged proposal as noise, flipping it to the
26
36
  // terminal `dismissed` status so it drops off the staged list (#520). Launches nothing.
27
- const DEFAULT_DISMISS_URL = "app/api/actions/delivery-graph/dismiss";
37
+ const DEFAULT_DISMISS_URL = new URL("../app/api/actions/delivery-graph/dismiss", import.meta.url).href;
28
38
  // The read-only DI preview door: recompiles a staged proposal's BPMN (with diagram interchange) so its
29
39
  // generated diagram can be rendered in the host explorer BEFORE dispatch. No deploy, no dispatch.
30
- const DEFAULT_PROPOSAL_BPMN_URL = "app/api/actions/delivery-graph/proposal-bpmn";
40
+ const DEFAULT_PROPOSAL_BPMN_URL = new URL("../app/api/actions/delivery-graph/proposal-bpmn", import.meta.url).href;
31
41
  // The save-to-library door: POST { name, digest } → copies this staged proposal's already-stored graph
32
42
  // into the reusable library (issue #523, save-from-digest → source `from-staged`). Persists a library
33
43
  // entry; it never dispatches or re-stages, so the #460 operator boundary holds.
34
- const DEFAULT_SAVE_LIBRARY_URL = "app/api/actions/delivery-graph/library/save";
44
+ const DEFAULT_SAVE_LIBRARY_URL = new URL("../app/api/actions/delivery-graph/library/save", import.meta.url).href;
35
45
 
36
46
  // How often the list re-polls the read door so a freshly-staged (or just-dispatched) proposal appears
37
47
  // (or drops off) without a manual refresh — mirrors the 5s cadence the old declarative grid used.
@@ -128,15 +128,15 @@
128
128
  { "label": "All", "filter": [] }
129
129
  ],
130
130
  "columns": [
131
- { "field": "title", "template": "{{title}}", "header": "Graph", "subtitleField": "run_key", "truncate": true, "width": "28%", "link": { "kind": "page", "page": "delivery-graph-detail", "keyField": "run_key" } },
132
- { "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
133
- { "field": "process_key", "header": "Instance", "width": "9rem", "truncate": true, "link": { "kind": "processExplorer", "keyField": "process_key" } },
134
- { "field": "phase", "header": "Phase", "truncate": true, "width": "26%" },
135
- { "field": "node_count", "header": "Nodes" },
136
- { "field": "human_node_count", "header": "Human" },
137
- { "field": "side_effect_count", "header": "Side effects" },
138
- { "field": "created_at", "header": "Dispatched", "width": "9rem", "format": "datetime" },
139
- { "field": "updated_at", "header": "Updated", "width": "9rem", "format": "datetime" }
131
+ { "field": "title", "template": "{{title}}", "header": "Graph", "subtitleField": "run_key", "truncate": true, "width": "20%", "link": { "kind": "page", "page": "delivery-graph-detail", "keyField": "run_key" } },
132
+ { "field": "status", "header": "Status", "truncate": true, "width": "12%", "link": { "kind": "processExplorer", "keyField": "process_key" } },
133
+ { "field": "process_key", "header": "Instance", "width": "9%", "truncate": true, "link": { "kind": "processExplorer", "keyField": "process_key" } },
134
+ { "field": "phase", "header": "Phase", "truncate": true, "width": "15%" },
135
+ { "field": "node_count", "header": "Nodes", "width": "5%" },
136
+ { "field": "human_node_count", "header": "Human", "width": "5%" },
137
+ { "field": "side_effect_count", "header": "Effects", "width": "7%" },
138
+ { "field": "created_at", "header": "Dispatched", "width": "11%", "truncate": true, "format": "datetime" },
139
+ { "field": "updated_at", "header": "Updated", "width": "11%", "truncate": true, "format": "datetime" }
140
140
  ],
141
141
  "detail": {
142
142
  "fields": [
@@ -6,12 +6,13 @@
6
6
  // Stage are SEPARATE operator actions (#516): Preview compiles without persisting; Stage persists a
7
7
  // proposal. Dispatch is deliberately NOT in this view (issue #460): it is an OPERATOR row-action on the
8
8
  // Staged proposals grid on the same page. This test pins the wiring so it can't silently regress: the
9
- // sidecars exist, mount.js hits the preview + stage doors with base-relative defaults (the #279
10
- // App-View resolution class — a leading-slash path 404s), it renders each preview facet, its compose
11
- // panel is collapsible, and it exposes NO dispatch/approval affordance (the self-approval hole #460
12
- // closes).
9
+ // sidecars exist, mount.js hits the preview + stage doors with MODULE-ANCHORED defaults (`new
10
+ // URL("../app/api/…", import.meta.url)`, the #467 App-View resolution class — this module is served one
11
+ // dir deep under `/delivery-graphs/`, so a document-base-relative or leading-slash default 404s; see
12
+ // test/delivery-graphs-staged-embed.test.ts and #536), it renders each preview facet, its compose panel
13
+ // is collapsible, and it exposes NO dispatch/approval affordance (the self-approval hole #460 closes).
13
14
  import { test } from "node:test";
14
- import { assert } from "#test-assert";
15
+ import { assert, assertEquals } from "#test-assert";
15
16
  import { readFileSync } from "node:fs";
16
17
  import { parseAndCompileText } from "../app/deliveryGraphTextIngress.ts";
17
18
  import { EXAMPLE_GRAPH } from "../pages/delivery-graphs/mount.js";
@@ -24,15 +25,41 @@ const STANDALONE_HTML = readFileSync(`${DIR}/standalone.html`, "utf8");
24
25
  const CSS = readFileSync(`${DIR}/delivery-graphs.css`, "utf8");
25
26
  const PAGE_JSON = readFileSync(`${ROOT}pages/delivery-graphs.page.json`, "utf8");
26
27
 
27
- // Pull the string default out of `const <name> = config.<name> ?? <CONST>;` (a module const).
28
- function defaultUrl(name: string): string {
28
+ // The REAL served location of mount.js on each surface: one directory deep under `/delivery-graphs/`.
29
+ const STANDALONE_MOUNT = "http://127.0.0.1:3000/delivery-graphs/mount.js";
30
+ const STUDIO_MOUNT = "http://studio-host:8080/console/app-view/Workforce/delivery-graphs/mount.js";
31
+
32
+ // Pull the module-anchored default spec out of `const <name> = config.<field> ?? <CONST>;` where the
33
+ // CONST is declared `const <CONST> = new URL("<spec>", import.meta.url).href;`.
34
+ function defaultSpec(name: string): string {
29
35
  const m = MOUNT_JS.match(new RegExp(`${name}\\s*=\\s*config\\.\\w+\\s*\\?\\?\\s*(\\w+);`));
30
36
  assert(m, `mount.js must default ${name} from config with a fallback constant`);
31
- const constM = MOUNT_JS.match(new RegExp(`const ${m![1]}\\s*=\\s*"([^"]*)"`));
32
- assert(constM, `mount.js must declare the ${m![1]} fallback as a string literal`);
37
+ const constM = MOUNT_JS.match(
38
+ new RegExp(`const ${m![1]}\\s*=\\s*new URL\\(\\s*"([^"]*)"\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\.href`),
39
+ );
40
+ assert(
41
+ constM,
42
+ `mount.js must default ${m![1]} to new URL("<spec>", import.meta.url) so the door is anchored to the ` +
43
+ `module's own served location, not the document base (#467/#536)`,
44
+ );
33
45
  return constM![1];
34
46
  }
35
47
 
48
+ // A module-anchored door default must (a) not be absolute (#279), (b) step up out of /delivery-graphs/
49
+ // (#467), and (c) resolve onto <appMount>/app/api/… both standalone AND inside the Studio iframe.
50
+ function assertModuleAnchored(name: string, endpoint: string): void {
51
+ const spec = defaultSpec(name);
52
+ assert(!spec.startsWith("/"), `default ${name} spec "${spec}" must not be absolute (resolves against console origin, #279)`);
53
+ assert(spec.startsWith("../"), `default ${name} spec "${spec}" must step up out of /delivery-graphs/ (#467)`);
54
+ assert(spec.endsWith(endpoint), `default ${name} spec "${spec}" must hit the ${endpoint} door`);
55
+ assertEquals(new URL(spec, STANDALONE_MOUNT).href, `http://127.0.0.1:3000/${endpoint}`, `default ${name} must resolve to the app root (#467)`);
56
+ assertEquals(
57
+ new URL(spec, STUDIO_MOUNT).href,
58
+ `http://studio-host:8080/console/app-view/Workforce/${endpoint}`,
59
+ `default ${name} must resolve under the app-view base, not the console origin (#279) nor the /delivery-graphs/ base (#467/#536)`,
60
+ );
61
+ }
62
+
36
63
  test("#441: the delivery-graphs App View mounts the same module standalone and embedded", () => {
37
64
  assert(/mountDeliveryGraphs/.test(MOUNT_JS), "mount.js must export mountDeliveryGraphs");
38
65
  for (const [file, html] of [["embed.html", EMBED_HTML], ["standalone.html", STANDALONE_HTML]] as const) {
@@ -41,13 +68,9 @@ test("#441: the delivery-graphs App View mounts the same module standalone and e
41
68
  }
42
69
  });
43
70
 
44
- test("#516: mount.js wires SEPARATE preview and stage doors (base-relative)", () => {
45
- const previewUrl = defaultUrl("previewUrl");
46
- assert(previewUrl.endsWith("actions/delivery-graph/preview"), `previewUrl default "${previewUrl}" must hit the previewDeliveryGraph door`);
47
- assert(!previewUrl.startsWith("/"), `default previewUrl "${previewUrl}" must be base-relative (App-View #279 resolution class)`);
48
- const stageUrl = defaultUrl("stageUrl");
49
- assert(stageUrl.endsWith("actions/delivery-graph/stage"), `stageUrl default "${stageUrl}" must hit the stageDeliveryGraph door`);
50
- assert(!stageUrl.startsWith("/"), `default stageUrl "${stageUrl}" must be base-relative (App-View #279 resolution class)`);
71
+ test("#516/#467: mount.js wires SEPARATE preview and stage doors, module-anchored to the app root", () => {
72
+ assertModuleAnchored("previewUrl", "app/api/actions/delivery-graph/preview");
73
+ assertModuleAnchored("stageUrl", "app/api/actions/delivery-graph/stage");
51
74
  // Preview and Stage are distinct buttons wired to distinct actions.
52
75
  assert(/id="dg-preview"/.test(MOUNT_JS) && /id="dg-stage"/.test(MOUNT_JS), "mount.js must render distinct Preview and Stage buttons");
53
76
  assert(/submit\(previewUrl,\s*false\)/.test(MOUNT_JS), "the Preview button must submit to the preview door WITHOUT staging");
@@ -2,7 +2,7 @@
2
2
  // epic #519 S5). The Import control is ADDED into the PRE-EXISTING compose mount (pages/delivery-graphs/
3
3
  // mount.js — the one #523 reshaped), alongside #523's inbound reuse-fill seam. It must: render an
4
4
  // `<input type=file accept=.json>`, read the picked file's text client-side, POST it to the
5
- // importToLibrary door (base-relative, App-View #279 resolution class), route a successful import back
5
+ // importToLibrary door (module-anchored relative spec, App-View #279 + #467/#536 resolution class), route a successful import back
6
6
  // through #523's SINGLE `fillComposer()` seam (no second inbound fill path), and render path-qualified
7
7
  // compile errors inline on a 400. This test pins that wiring so it can't silently regress.
8
8
  import { test } from "node:test";
@@ -16,12 +16,22 @@ const CSS = readFileSync(`${DIR}/delivery-graphs.css`, "utf8");
16
16
  const EMBED_HTML = readFileSync(`${DIR}/embed.html`, "utf8");
17
17
  const STANDALONE_HTML = readFileSync(`${DIR}/standalone.html`, "utf8");
18
18
 
19
- // Pull the string default out of `const <name> = config.<name> ?? <CONST>;` (a module const).
20
- function defaultUrl(name: string): string {
19
+ // Pull the module-anchored default URL *spec* (the "<spec>" literal inside
20
+ // `new URL("<spec>", import.meta.url)`, e.g. "../app/api/…") out of
21
+ // `const <name> = config.<name> ?? <CONST>;` where the CONST is declared
22
+ // `const <CONST> = new URL("<spec>", import.meta.url).href;`. This is the unresolved
23
+ // relative spec, not a resolved URL string — hence `Spec`, not `Url`.
24
+ function defaultSpec(name: string): string {
21
25
  const m = MOUNT_JS.match(new RegExp(`${name}\\s*=\\s*config\\.\\w+\\s*\\?\\?\\s*(\\w+);`));
22
26
  assert(m, `mount.js must default ${name} from config with a fallback constant`);
23
- const constM = MOUNT_JS.match(new RegExp(`const ${m![1]}\\s*=\\s*"([^"]*)"`));
24
- assert(constM, `mount.js must declare the ${m![1]} fallback as a string literal`);
27
+ const constM = MOUNT_JS.match(
28
+ new RegExp(`const ${m![1]}\\s*=\\s*new URL\\(\\s*"([^"]*)"\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\.href`),
29
+ );
30
+ assert(
31
+ constM,
32
+ `mount.js must default ${m![1]} to new URL("<spec>", import.meta.url) so the door is anchored ` +
33
+ `to the module's own served location, not the document base (#467/#536)`,
34
+ );
25
35
  return constM![1];
26
36
  }
27
37
 
@@ -31,13 +41,21 @@ test("#524: the compose mount renders a file-input Import control accepting .jso
31
41
  assert(/accept="[^"]*\.json[^"]*"/.test(MOUNT_JS), "the Import file input must accept .json files");
32
42
  });
33
43
 
34
- test("#524: Import wires the importToLibrary door (base-relative), reading the file text client-side", () => {
35
- const importUrl = defaultUrl("importUrl");
44
+ test("#524: Import wires the importToLibrary door (module-anchored relative), reading the file text client-side", () => {
45
+ const importSpec = defaultSpec("importUrl");
46
+ assert(
47
+ importSpec.endsWith("actions/delivery-graph/library/import"),
48
+ `importUrl default spec "${importSpec}" must hit the importToLibrary door`,
49
+ );
50
+ assert(!importSpec.startsWith("/"), `default importUrl spec "${importSpec}" must be relative, not absolute (App-View #279 + #467/#536 resolution class)`);
51
+ // Pin the MODULE-anchored invariant, not merely "not absolute": the spec must step UP out of the
52
+ // /delivery-graphs/ shell base with "../" (resolved against import.meta.url), so a regression back to a
53
+ // document-base-relative "app/api/…" — which 404s under the shell base (#467/#536) — fails the guard.
36
54
  assert(
37
- importUrl.endsWith("actions/delivery-graph/library/import"),
38
- `importUrl default "${importUrl}" must hit the importToLibrary door`,
55
+ importSpec.startsWith("../"),
56
+ `default importUrl spec "${importSpec}" must be module-anchored with a "../" prefix (steps out of the ` +
57
+ `/delivery-graphs/ shell base off import.meta.url), not document-base-relative (#467/#536)`,
39
58
  );
40
- assert(!importUrl.startsWith("/"), `default importUrl "${importUrl}" must be base-relative (App-View #279 resolution class)`);
41
59
  // The file's text is read CLIENT-SIDE and POSTed as graphJson to the import door.
42
60
  assert(/\.text\(\)/.test(MOUNT_JS), "mount.js must read the selected file's text client-side via File.text()");
43
61
  assert(/post\(importUrl,\s*\{\s*graphJson:/.test(MOUNT_JS), "the Import handler must POST the file text as graphJson to the import door");
@@ -6,8 +6,8 @@
6
6
  // compose mount's NEW inbound fill seam over the shared `deliveryGraph.compose.fill` host-bridge
7
7
  // message. Plus a Save-to-library affordance on the staged App-View (save-from-digest) and a
8
8
  // Save-to-library row action on the dispatched/history grid. This test pins that wiring so it cannot
9
- // silently regress: the sidecars mount the same module, the door defaults are base-relative (the #279
10
- // App-View resolution class), Reuse posts the ONE shared fill message, Delete hits the per-entry door,
9
+ // silently regress: the sidecars mount the same module, the door defaults are module-anchored off
10
+ // import.meta.url (the #279/#467/#536 App-View resolution class), Reuse posts the ONE shared fill message, Delete hits the per-entry door,
11
11
  // the compose mount adds the inbound fill listener, and the page carries the new Library App-View node.
12
12
  import { test } from "node:test";
13
13
  import { assert, assertEquals, assertStringIncludes } from "#test-assert";
@@ -23,15 +23,21 @@ const EMBED_HTML = readFileSync(`${DIR}/library-embed.html`, "utf8");
23
23
  const STANDALONE_HTML = readFileSync(`${DIR}/library-standalone.html`, "utf8");
24
24
  const PAGE_JSON = readFileSync(`${ROOT}pages/delivery-graphs.page.json`, "utf8");
25
25
 
26
- // Pull the string default out of `const <name> = config.<field> ?? <CONST>;` (or a module const).
27
- function defaultUrl(src: string, name: string): string {
26
+ // Pull a MODULE-ANCHORED default spec out of `const <name> = config.<field> ?? <CONST>;` where the
27
+ // CONST is declared `const <CONST> = new URL("<spec>", import.meta.url).href;` (#467/#536).
28
+ function defaultSpec(src: string, name: string): string {
28
29
  const m = src.match(new RegExp(`${name}\\s*=\\s*config\\.\\w+\\s*\\?\\?\\s*(\\w+);`));
29
30
  assert(m, `mount must default ${name} from config with a fallback constant`);
30
- const constM = src.match(new RegExp(`const ${m![1]}\\s*=\\s*"([^"]*)"`));
31
- assert(constM, `mount must declare the ${m![1]} fallback as a string literal`);
31
+ const constM = src.match(
32
+ new RegExp(`const ${m![1]}\\s*=\\s*new URL\\(\\s*"([^"]*)"\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\.href`),
33
+ );
34
+ assert(
35
+ constM,
36
+ `mount must default ${m![1]} to new URL("<spec>", import.meta.url) so the door is anchored to the ` +
37
+ `module's own served location, not the document base (#467/#536)`,
38
+ );
32
39
  return constM![1];
33
40
  }
34
-
35
41
  test("#523: the Library App-View mounts the same module standalone and embedded", () => {
36
42
  assert(/mountDeliveryGraphLibrary/.test(LIBRARY_JS), "library.mount.js must export mountDeliveryGraphLibrary");
37
43
  for (const [file, html] of [["library-embed.html", EMBED_HTML], ["library-standalone.html", STANDALONE_HTML]] as const) {
@@ -52,10 +58,11 @@ test("#523: the page binds the Library node to the library App-View sidecars", (
52
58
  assert(library?.props?.standalone === "./delivery-graphs/library-standalone.html", "it has the library standalone sidecar");
53
59
  });
54
60
 
55
- test("#523/#279: the Library list door default is base-relative and hits the listLibrary door", () => {
56
- const url = defaultUrl(LIBRARY_JS, "libraryUrl");
57
- assert(url.endsWith("delivery-graph/library"), `libraryUrl default "${url}" must hit the listLibrary door`);
58
- assert(!url.startsWith("/"), `default libraryUrl "${url}" must be base-relative (App-View #279 resolution class)`);
61
+ test("#523/#467/#536: the Library list door default is module-anchored and hits the listLibrary door", () => {
62
+ const spec = defaultSpec(LIBRARY_JS, "libraryUrl");
63
+ assert(spec.endsWith("delivery-graph/library"), `libraryUrl default "${spec}" must hit the listLibrary door`);
64
+ assert(!spec.startsWith("/"), `default libraryUrl "${spec}" must not be absolute (App-View #279 resolution class)`);
65
+ assert(spec.startsWith("../"), `default libraryUrl "${spec}" must step up out of /delivery-graphs/ (module-anchored, #467/#536)`);
59
66
  // The list read consumes the door's `entries` array (one row per saved entry).
60
67
  assert(/body\.entries/.test(LIBRARY_JS), "library.mount.js must render one row per `entries[]` item the listLibrary door returns");
61
68
  });
@@ -98,15 +105,38 @@ test("#523: the compose mount exposes an INBOUND reuse-fill seam (message → #d
98
105
  });
99
106
 
100
107
  test("#523: Save-to-library on the staged App-View posts save-from-digest", () => {
101
- const url = defaultUrl(STAGED_JS, "saveLibraryUrl");
102
- assert(url.endsWith("actions/delivery-graph/library/save"), `saveLibraryUrl default "${url}" must hit the saveToLibrary door`);
103
- assert(!url.startsWith("/"), `default saveLibraryUrl "${url}" must be base-relative (App-View #279 resolution class)`);
108
+ const spec = defaultSpec(STAGED_JS, "saveLibraryUrl");
109
+ assert(spec.endsWith("actions/delivery-graph/library/save"), `saveLibraryUrl default "${spec}" must hit the saveToLibrary door`);
110
+ assert(!spec.startsWith("/"), `default saveLibraryUrl "${spec}" must not be absolute (App-View #279 resolution class)`);
111
+ assert(spec.startsWith("../"), `default saveLibraryUrl "${spec}" must step up out of /delivery-graphs/ (module-anchored, #467/#536)`);
104
112
  assert(/data-save-library=/.test(STAGED_JS), "staged.mount.js must render a per-row Save-to-library affordance carrying the digest");
105
113
  // Save-from-digest: it posts { name, digest } — it must NOT compile or stage a raw graph (the #460
106
114
  // operator boundary the staged view enforces stays intact).
107
115
  assert(/post\(saveLibraryUrl,\s*\{\s*name:[^}]*digest:/.test(STAGED_JS), "Save-to-library must POST { name, digest } (save-from-digest) to the save door");
108
116
  });
109
117
 
118
+ test("#538: the in-flight grid's column widths never over-allocate (no 0-width column, no header char-wrap)", () => {
119
+ // Regression guard: the grid is `table-layout:fixed` with `overflow-wrap:anywhere` headers, so a
120
+ // column squeezed to 0 width wraps its title one character per line (a ~250px-tall header row). That
121
+ // happens when the declared widths over-allocate. Pin the contract: every data column carries a
122
+ // percentage width, and their sum leaves headroom (>=5%) for the implicit row-action column — so no
123
+ // column can collapse to 0.
124
+ const page = JSON.parse(PAGE_JSON) as { nodes: Array<Record<string, any>> };
125
+ const grid = page.nodes.find((n) => n.id === "delivery-graphs-inflight");
126
+ assert(grid, "the page must carry the delivery-graphs-inflight grid");
127
+ const columns = (grid?.props?.columns ?? []) as Array<Record<string, any>>;
128
+ assert(columns.length > 0, "the grid must declare columns");
129
+ let sum = 0;
130
+ for (const col of columns) {
131
+ const w = col.width;
132
+ assert(typeof w === "string" && /^\d+(\.\d+)?%$/.test(w), `column "${col.header}" must declare a percentage width so it can't collapse to 0 (got ${JSON.stringify(w)})`);
133
+ const parsed = Number.parseFloat(w);
134
+ assert(parsed > 0, `column "${col.header}" must declare a positive width so it can't collapse to 0 (got ${JSON.stringify(w)})`);
135
+ sum += parsed;
136
+ }
137
+ assert(sum <= 95, `declared column widths sum to ${sum}% — they must leave >=5% for the row-action column so nothing squeezes to 0 (#538)`);
138
+ });
139
+
110
140
  test("#523: Save-to-library is offered on a dispatched/history grid row (save-from-dispatched)", () => {
111
141
  const page = JSON.parse(PAGE_JSON) as { nodes: Array<Record<string, any>> };
112
142
  const grid = page.nodes.find((n) => n.id === "delivery-graphs-inflight");
@@ -6,10 +6,21 @@
6
6
  // Dispatch button and NO way to see the graph. The staged App-View (pages/delivery-graphs/staged.*)
7
7
  // closes that: per row it offers Preview-DI (over the nano-navigate bridge) AND Dispatch. This test
8
8
  // pins the wiring so it cannot silently regress: the sidecars exist and mount the same module, the door
9
- // defaults are base-relative (the #279 App-View resolution class — a leading-slash path 404s through the
10
- // console iframe), it drives the DI-preview bridge, and it posts the dispatch by digest.
9
+ // defaults are MODULE-ANCHORED (`new URL("../app/api/…", import.meta.url)`, the #467 App-View
10
+ // resolution class — see below), it drives the DI-preview bridge, and it posts the dispatch by digest.
11
+ //
12
+ // #279/#467 resolution class: the staged shell (staged-embed.html / staged-standalone.html) — and thus
13
+ // staged.mount.js — is served ONE DIRECTORY DEEP at `<appMount>/delivery-graphs/`, while the API lives
14
+ // at the app root, a sibling of `/delivery-graphs/`: `<appMount>/app/api/…`. A leading-slash absolute
15
+ // default resolves against the console iframe ORIGIN, not the app-view base (#279). A document-base-
16
+ // relative default (`"app/api/…"`) resolves against the `…/delivery-graphs/` shell base to
17
+ // `…/delivery-graphs/app/api/…` → 404 on EVERY surface (the Studio console never injects
18
+ // window.__NANO_APP_VIEW__, so the default runs) — the "Could not load staged proposals." bug (#536).
19
+ // The only correct default is anchored to staged.mount.js's OWN url (import.meta.url): `../app/api/…`
20
+ // steps up out of `/delivery-graphs/` onto `<appMount>/app/api/…` on all surfaces. This test pins that
21
+ // resolution against the REAL, `/delivery-graphs/`-deep module url so neither regression can return.
11
22
  import { test } from "node:test";
12
- import { assert } from "#test-assert";
23
+ import { assert, assertEquals } from "#test-assert";
13
24
  import { readFileSync } from "node:fs";
14
25
 
15
26
  const ROOT = decodeURIComponent(new URL("../", import.meta.url).pathname);
@@ -19,15 +30,55 @@ const EMBED_HTML = readFileSync(`${DIR}/staged-embed.html`, "utf8");
19
30
  const STANDALONE_HTML = readFileSync(`${DIR}/staged-standalone.html`, "utf8");
20
31
  const PAGE_JSON = readFileSync(`${ROOT}pages/delivery-graphs.page.json`, "utf8");
21
32
 
22
- // Pull the string default out of `const <name> = config.<field> ?? <CONST>;` (or a module const).
23
- function defaultUrl(name: string): string {
33
+ // The REAL served location of staged.mount.js on each surface: one directory deep under
34
+ // `/delivery-graphs/`.
35
+ const STANDALONE_MOUNT = "http://127.0.0.1:3000/delivery-graphs/staged.mount.js";
36
+ const STUDIO_MOUNT = "http://studio-host:8080/console/app-view/Workforce/delivery-graphs/staged.mount.js";
37
+
38
+ // Pull the module-anchored default spec out of `const <name> = config.<field> ?? <CONST>;` where the
39
+ // CONST is declared `const <CONST> = new URL("<spec>", import.meta.url).href;`.
40
+ function defaultSpec(name: string): string {
24
41
  const m = MOUNT_JS.match(new RegExp(`${name}\\s*=\\s*config\\.\\w+\\s*\\?\\?\\s*(\\w+);`));
25
42
  assert(m, `staged.mount.js must default ${name} from config with a fallback constant`);
26
- const constM = MOUNT_JS.match(new RegExp(`const ${m![1]}\\s*=\\s*"([^"]*)"`));
27
- assert(constM, `staged.mount.js must declare the ${m![1]} fallback as a string literal`);
43
+ const constM = MOUNT_JS.match(
44
+ new RegExp(`const ${m![1]}\\s*=\\s*new URL\\(\\s*"([^"]*)"\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\.href`),
45
+ );
46
+ assert(
47
+ constM,
48
+ `staged.mount.js must default ${m![1]} to new URL("<spec>", import.meta.url) so the door is anchored ` +
49
+ `to the module's own served location, not the document base (#467/#536)`,
50
+ );
28
51
  return constM![1];
29
52
  }
30
53
 
54
+ // A module-anchored door default must (a) not be absolute (#279), (b) step up out of /delivery-graphs/
55
+ // (#467), and (c) resolve onto <appMount>/app/api/… both standalone AND inside the Studio iframe.
56
+ function assertModuleAnchored(name: string, endpoint: string): void {
57
+ const spec = defaultSpec(name);
58
+ assert(
59
+ !spec.startsWith("/"),
60
+ `default ${name} spec "${spec}" must not be absolute: a leading-slash path resolves against the ` +
61
+ `iframe ORIGIN (console :8080), not the app-view base, so every fetch 404s (#279)`,
62
+ );
63
+ assert(
64
+ spec.startsWith("../"),
65
+ `default ${name} spec "${spec}" must step up out of /delivery-graphs/ (import.meta.url points at ` +
66
+ `<appMount>/delivery-graphs/staged.mount.js; the API is a sibling at <appMount>/app/api/…) (#467)`,
67
+ );
68
+ assert(spec.endsWith(endpoint), `default ${name} spec "${spec}" must hit the ${endpoint} door`);
69
+ assertEquals(
70
+ new URL(spec, STANDALONE_MOUNT).href,
71
+ `http://127.0.0.1:3000/${endpoint}`,
72
+ `default ${name} must resolve to the app root, not the /delivery-graphs/ shell base (#467)`,
73
+ );
74
+ assertEquals(
75
+ new URL(spec, STUDIO_MOUNT).href,
76
+ `http://studio-host:8080/console/app-view/Workforce/${endpoint}`,
77
+ `default ${name} must resolve under the app-view base the console proxies, not the console origin ` +
78
+ `root (#279) nor the /delivery-graphs/ shell base (#467/#536)`,
79
+ );
80
+ }
81
+
31
82
  test("#511: the staged App-View mounts the same module standalone and embedded", () => {
32
83
  assert(/mountStagedProposals/.test(MOUNT_JS), "staged.mount.js must export mountStagedProposals");
33
84
  for (const [file, html] of [["staged-embed.html", EMBED_HTML], ["staged-standalone.html", STANDALONE_HTML]] as const) {
@@ -48,25 +99,19 @@ test("#511: the page binds the Staged proposals node to the staged App-View side
48
99
  assert(staged?.props?.standalone === "./delivery-graphs/staged-standalone.html", "it has the staged standalone sidecar");
49
100
  });
50
101
 
51
- test("#511/#279: the staged list door default is base-relative", () => {
52
- const url = defaultUrl("stagedUrl");
53
- assert(url.endsWith("delivery-graph/staged"), `stagedUrl default "${url}" must hit the listStagedProposals door`);
54
- assert(!url.startsWith("/"), `default stagedUrl "${url}" must be base-relative (App-View #279 resolution class)`);
102
+ test("#511/#279/#467: the staged list door default is module-anchored to the app root", () => {
103
+ assertModuleAnchored("stagedUrl", "app/api/delivery-graph/staged");
55
104
  });
56
105
 
57
106
  test("#511: DI preview — the staged view wires the proposal-bpmn door and bridges the XML to the explorer", () => {
58
- const url = defaultUrl("proposalBpmnUrl");
59
- assert(url.endsWith("actions/delivery-graph/proposal-bpmn"), `proposalBpmnUrl default "${url}" must hit the previewProposalBpmn door`);
60
- assert(!url.startsWith("/"), `default proposalBpmnUrl "${url}" must be base-relative`);
107
+ assertModuleAnchored("proposalBpmnUrl", "app/api/actions/delivery-graph/proposal-bpmn");
61
108
  assert(/data-preview-di=/.test(MOUNT_JS), "staged.mount.js must render a per-row Preview-DI affordance carrying the digest");
62
109
  assert(/target:\s*"definitionPreview"/.test(MOUNT_JS), "staged.mount.js must post nano-navigate to the definitionPreview target");
63
110
  assert(/params:\s*\{\s*xml:/.test(MOUNT_JS), "staged.mount.js must carry the compiled BPMN xml in the bridge message");
64
111
  });
65
112
 
66
113
  test("#460/#511: Dispatch is the operator's launch — posts the digest to the dispatch door, and never compiles/stages", () => {
67
- const url = defaultUrl("dispatchUrl");
68
- assert(url.endsWith("actions/delivery-graph/dispatch"), `dispatchUrl default "${url}" must hit the dispatchDeliveryGraph door`);
69
- assert(!url.startsWith("/"), `default dispatchUrl "${url}" must be base-relative`);
114
+ assertModuleAnchored("dispatchUrl", "app/api/actions/delivery-graph/dispatch");
70
115
  assert(/data-dispatch=/.test(MOUNT_JS), "staged.mount.js must render a per-row Dispatch affordance carrying the digest");
71
116
  assert(/window\.confirm\(/.test(MOUNT_JS), "Dispatch must confirm before launching (dispatch authorises side effects)");
72
117
  // Operator-only: this surface dispatches a digest that is ALREADY staged — it must not compile or
@@ -77,9 +122,7 @@ test("#460/#511: Dispatch is the operator's launch — posts the digest to the d
77
122
  });
78
123
 
79
124
  test("#520: Dismiss is the operator's discard — posts the digest to the dismiss door, behind a confirm, and launches nothing", () => {
80
- const url = defaultUrl("dismissUrl");
81
- assert(url.endsWith("actions/delivery-graph/dismiss"), `dismissUrl default "${url}" must hit the dismissProposal door`);
82
- assert(!url.startsWith("/"), `default dismissUrl "${url}" must be base-relative (App-View #279 resolution class)`);
125
+ assertModuleAnchored("dismissUrl", "app/api/actions/delivery-graph/dismiss");
83
126
  assert(/data-dismiss=/.test(MOUNT_JS), "staged.mount.js must render a per-row Dismiss affordance carrying the digest");
84
127
  // Dismiss is a one-way discard off the staged list — confirm before it drops the proposal.
85
128
  assert(/window\.confirm\(DISMISS_CONFIRM\)/.test(MOUNT_JS), "staged.mount.js must confirm before dismissing (a one-way discard off the staged list)");