@nanobpm/urban 0.79.0 → 0.81.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 (52) hide show
  1. package/dist/runtime/core/manifest.d.ts +44 -11
  2. package/dist/runtime/core/modules/cancel.d.ts +13 -8
  3. package/dist/runtime/core/modules/cancel.js +40 -21
  4. package/dist/runtime/core/modules/datasource.js +7 -4
  5. package/dist/runtime/core/modules/formjs.gen.d.ts +1 -0
  6. package/dist/runtime/core/modules/formjs.gen.js +5 -0
  7. package/dist/runtime/core/modules/forms.d.ts +22 -0
  8. package/dist/runtime/core/modules/forms.js +59 -0
  9. package/dist/runtime/core/modules/gateway.js +5 -4
  10. package/dist/runtime/core/modules/instance-status-read-model.d.ts +56 -0
  11. package/dist/runtime/core/modules/instance-status-read-model.js +127 -0
  12. package/dist/runtime/core/modules/instance-tracking.d.ts +47 -19
  13. package/dist/runtime/core/modules/instance-tracking.js +451 -123
  14. package/dist/runtime/core/modules/open-user-tasks-store.d.ts +5 -0
  15. package/dist/runtime/core/modules/open-user-tasks-store.js +8 -0
  16. package/dist/runtime/core/modules/pages.js +42 -4
  17. package/dist/runtime/core/modules/runtime.gen.js +1 -1
  18. package/dist/runtime/core/modules/surfaces.js +18 -124
  19. package/dist/runtime/core/read-model.d.ts +28 -0
  20. package/dist/runtime/core/read-model.js +44 -4
  21. package/dist/runtime/core/validate.js +192 -0
  22. package/dist/runtime/index.d.ts +2 -1
  23. package/dist/runtime/index.js +5 -0
  24. package/package.json +1 -1
  25. package/src/runtime/browser/fake-dom.test-utils.ts +114 -0
  26. package/src/runtime/browser/formjs.browser.js +192 -0
  27. package/src/runtime/browser/formjs.browser.test.ts +136 -0
  28. package/src/runtime/browser/runtime.browser.js +101 -1
  29. package/src/runtime/browser/runtime.engineform.test.ts +158 -0
  30. package/src/runtime/core/manifest.ts +44 -11
  31. package/src/runtime/core/modules/cancel.test.ts +112 -38
  32. package/src/runtime/core/modules/cancel.ts +42 -22
  33. package/src/runtime/core/modules/datasource.ts +7 -4
  34. package/src/runtime/core/modules/formjs.gen.ts +6 -0
  35. package/src/runtime/core/modules/forms.test.ts +122 -0
  36. package/src/runtime/core/modules/forms.ts +67 -0
  37. package/src/runtime/core/modules/gateway.ts +7 -4
  38. package/src/runtime/core/modules/gen-artifacts.test.ts +41 -0
  39. package/src/runtime/core/modules/instance-status-read-model.ts +156 -0
  40. package/src/runtime/core/modules/instance-tracking.test.ts +589 -60
  41. package/src/runtime/core/modules/instance-tracking.ts +469 -126
  42. package/src/runtime/core/modules/open-user-tasks-store.test.ts +11 -0
  43. package/src/runtime/core/modules/open-user-tasks-store.ts +11 -0
  44. package/src/runtime/core/modules/pages.test.ts +99 -0
  45. package/src/runtime/core/modules/pages.ts +44 -5
  46. package/src/runtime/core/modules/runtime.gen.ts +1 -1
  47. package/src/runtime/core/modules/surfaces.ts +19 -121
  48. package/src/runtime/core/read-model.test.ts +69 -0
  49. package/src/runtime/core/read-model.ts +43 -4
  50. package/src/runtime/core/validate.test.ts +341 -0
  51. package/src/runtime/core/validate.ts +188 -0
  52. package/src/runtime/index.ts +15 -0
@@ -56,6 +56,11 @@ export declare class OpenUserTasksStore {
56
56
  syncInstance(processInstanceKey: string, openTasks: readonly UserTaskSummary[]): void;
57
57
  /** Retire every projected open task for an instance (e.g. it terminated). Idempotent. */
58
58
  clearInstance(processInstanceKey: string): void;
59
+ /** Every distinct instance key currently projected as having ≥1 open user task. Used by the
60
+ * reconciler to re-probe (and, when answered, RETIRE) keys that have dropped out of a binding's
61
+ * `activeStatuses` allow-list — a projected row must be retired independently of the base-row
62
+ * selector, or an answered escalation's row would linger and the derived edge tear (ADR 0065). */
63
+ instanceKeys(): string[];
59
64
  /** True iff the instance currently has any open user task — the "parked on a human" predicate. */
60
65
  hasOpenTask(processInstanceKey: string): boolean;
61
66
  /** The projected open tasks for an instance, in insertion order. */
@@ -113,6 +113,14 @@ export class OpenUserTasksStore {
113
113
  return;
114
114
  this.#db.run(`DELETE FROM ${OPEN_USER_TASKS_TABLE} WHERE process_instance_key = ?`, [processInstanceKey]);
115
115
  }
116
+ /** Every distinct instance key currently projected as having ≥1 open user task. Used by the
117
+ * reconciler to re-probe (and, when answered, RETIRE) keys that have dropped out of a binding's
118
+ * `activeStatuses` allow-list — a projected row must be retired independently of the base-row
119
+ * selector, or an answered escalation's row would linger and the derived edge tear (ADR 0065). */
120
+ instanceKeys() {
121
+ const rows = this.#db.all(`SELECT DISTINCT process_instance_key FROM ${OPEN_USER_TASKS_TABLE}`);
122
+ return rows.map((r) => r.process_instance_key);
123
+ }
116
124
  /** True iff the instance currently has any open user task — the "parked on a human" predicate. */
117
125
  hasOpenTask(processInstanceKey) {
118
126
  if (!processInstanceKey)
@@ -28,8 +28,11 @@ import { errorMessage, isRecord } from "../guards.js";
28
28
  import { html, json } from "../router.js";
29
29
  import { cancelInstanceReconciling } from "./cancel.js";
30
30
  import { apiDocsPath } from "./api.js";
31
+ import { FORMJS_JS } from "./formjs.gen.js";
32
+ import { completeUserTaskResponse, resolveFormResponse } from "./forms.js";
31
33
  import { quoteIdent } from "./gateway.js";
32
34
  import { RENDERER_JS } from "./runtime.gen.js";
35
+ import { isSqlIdentifier } from "../read-model.js";
33
36
  /** A JavaScript-body response (the renderer module served at the fingerprinted
34
37
  * `/app/runtime.<hash>.js`, and its `/app/runtime.js` back-compat route). */
35
38
  function javascript(body, status = 200, extraHeaders) {
@@ -116,9 +119,10 @@ async function serveAsset(read, filePath) {
116
119
  return { status: 404, headers: { "content-type": "text/plain; charset=utf-8" }, body: "not found" };
117
120
  }
118
121
  }
119
- /** A SQL identifier guard — a table name must be a bare identifier *and* a known table
120
- * (checked against `schema()`), so `/app/data/:table` can never inject SQL. */
121
- const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
122
+ /** A table name must be a bare SQL identifier ({@link isSqlIdentifier}, the runtime-wide single source
123
+ * of truth) *and* a known table (checked against `schema()`), so `/app/data/:table` can never inject
124
+ * SQL. Reusing the shared guard (rather than a private regex) keeps this surface from drifting from
125
+ * manifest / read-model / data-source identifier validation. */
122
126
  // A query flag (e.g. ?count=1) is "on" when present and not one of the falsy
123
127
  // spellings. A bare `?count` (empty string) still counts as on, so a caller can
124
128
  // opt in without inventing a value.
@@ -297,7 +301,7 @@ export function createPagesRoutes(opts, deps) {
297
301
  // source is rejected rather than silently served off the default.
298
302
  if (source !== sourceName)
299
303
  return json({ error: `unknown datasource "${source}"` }, 404);
300
- if (!IDENT.test(table))
304
+ if (!isSqlIdentifier(table))
301
305
  return json({ error: "invalid table name" }, 400);
302
306
  let tables;
303
307
  try {
@@ -489,6 +493,26 @@ export function createPagesRoutes(opts, deps) {
489
493
  }
490
494
  },
491
495
  });
496
+ // ── GET /app/actions/form & POST /app/actions/complete (ADR 0026, #457) ───
497
+ // Engine-declared forms in a `dataGrid` detail drawer. A grid whose `detail`
498
+ // opts into `engineForm` resolves the selected row's `form_key` here and
499
+ // completes the row's user task here — the SAME two seams the `taskInbox`
500
+ // surface uses, reused (not forked) via `forms.ts` so the resolution gate and
501
+ // the completion contract can't drift between the two surfaces. This closes the
502
+ // form-duplication drift class: the grid renders the authoritative deployed
503
+ // `.form` rather than a hand-authored page-local `detail.form` copy.
504
+ routes.push({
505
+ method: "GET",
506
+ path: "/app/actions/form",
507
+ source: "surface:pages",
508
+ handler: (req) => resolveFormResponse(engine, req.query.get("formKey") ?? undefined, req.query.get("formId") ?? undefined),
509
+ });
510
+ routes.push({
511
+ method: "POST",
512
+ path: "/app/actions/complete",
513
+ source: "surface:pages",
514
+ handler: async (req) => completeUserTaskResponse(engine, await req.text()),
515
+ });
492
516
  // ── nested (multi-appView) sidecars (#442) ──────────────────────────────
493
517
  // An app with more than one `appView` cannot share the four FLAT sidecar names above:
494
518
  // each view ships its own SUBDIR sidecar — e.g. `pages/cockpit.page.json` →
@@ -603,6 +627,7 @@ function rendererShell(homePage, apiDocsPath) {
603
627
  </head>
604
628
  <body>${apiDocsBadge}
605
629
  <main id="page" data-home="${escapeAttr(homePage)}"><p class="pc-empty">Loading…</p></main>
630
+ <script>${FORMJS_JS}</script>
606
631
  <script type="module" src=".${RUNTIME_JS_PATH}"></script>
607
632
  </body>
608
633
  </html>`;
@@ -669,6 +694,19 @@ body { margin:0; font:15px/1.5 system-ui,sans-serif; padding:2rem; max-width:64r
669
694
  .pc-field-check input { width:auto; padding:0; accent-color:var(--nano-accent); cursor:pointer; }
670
695
  .pc-btn { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--nano-accent); color:var(--nano-on-accent); font:inherit; cursor:pointer; }
671
696
  .pc-btn:disabled { opacity:.5; cursor:default; }
697
+ /* The shared engine-form renderer (NanoFormJs, ADR 0026/#457) styled to match the
698
+ page runtime's native detail controls. njf-* are its stable class names; both
699
+ this surface and the taskInbox page style the same renderer output. */
700
+ .njf-form { display:flex; flex-direction:column; gap:.4rem; margin-top:.5rem; }
701
+ .njf-heading { font-size:1rem; margin:0 0 .25rem; }
702
+ .njf-text { color:var(--nano-text-muted); margin:.25rem 0; }
703
+ .njf-field { display:flex; flex-direction:column; gap:.25rem; }
704
+ .njf-field label { font-size:.8rem; color:var(--nano-text-muted); }
705
+ .njf-field input:not([type=checkbox]):not([type=radio]), .njf-field textarea, .njf-field select { padding:.5rem .6rem; border:1px solid var(--nano-edge); border-radius:.4rem; font:inherit; background:var(--nano-inset); color:var(--nano-text); max-width:24rem; }
706
+ .njf-actions { display:flex; gap:.5rem; margin-top:.4rem; }
707
+ .njf-actions button { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--nano-accent); color:var(--nano-on-accent); font:inherit; cursor:pointer; }
708
+ .njf-msg { font-size:.85rem; margin-top:.35rem; }
709
+ .njf-msg.err { color:var(--nano-danger); }
672
710
  .pc-msg { font-size:.85rem; margin-top:.5rem; min-height:1.2em; }
673
711
  .pc-msg.err { color:var(--nano-danger); }
674
712
  .pc-msg.ok { color:var(--nano-ok); }