@object-ui/app-shell 7.1.0 → 7.3.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 (107) hide show
  1. package/CHANGELOG.md +320 -0
  2. package/dist/components/ManagedByBadge.js +1 -1
  3. package/dist/console/AppContent.js +9 -15
  4. package/dist/console/ConsoleShell.d.ts +16 -0
  5. package/dist/console/ConsoleShell.js +43 -2
  6. package/dist/console/ai/AiChatPage.js +64 -14
  7. package/dist/console/ai/BuildDebugDrawer.d.ts +20 -0
  8. package/dist/console/ai/BuildDebugDrawer.js +75 -0
  9. package/dist/console/ai/buildDebugApi.d.ts +94 -0
  10. package/dist/console/ai/buildDebugApi.js +16 -0
  11. package/dist/console/home/HomeLayout.js +5 -7
  12. package/dist/console/home/HomePage.js +1 -9
  13. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  14. package/dist/console/organizations/OrganizationsPage.js +32 -4
  15. package/dist/console/organizations/manage/OrganizationLayout.js +1 -1
  16. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  17. package/dist/console/organizations/provisionEnvironment.js +64 -0
  18. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  19. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  20. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  21. package/dist/environment/EnvironmentListToolbar.js +59 -0
  22. package/dist/environment/entitlements.d.ts +90 -0
  23. package/dist/environment/entitlements.js +91 -0
  24. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  25. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  26. package/dist/hooks/useActionModal.js +15 -1
  27. package/dist/hooks/useAiSurface.d.ts +59 -0
  28. package/dist/hooks/useAiSurface.js +78 -0
  29. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  30. package/dist/hooks/useConsoleActionRuntime.js +36 -8
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.js +5 -1
  33. package/dist/layout/AppHeader.js +30 -5
  34. package/dist/layout/ConsoleFloatingChatbot.js +22 -4
  35. package/dist/layout/ConsoleLayout.js +5 -6
  36. package/dist/layout/ContextSelectors.js +0 -19
  37. package/dist/layout/WorkspaceSwitcher.d.ts +14 -0
  38. package/dist/layout/WorkspaceSwitcher.js +76 -0
  39. package/dist/preview/DraftPreviewBar.js +20 -7
  40. package/dist/providers/ExpressionProvider.js +9 -3
  41. package/dist/utils/index.d.ts +2 -2
  42. package/dist/utils/index.js +1 -1
  43. package/dist/utils/managedByEmptyState.d.ts +1 -1
  44. package/dist/utils/managedByEmptyState.js +20 -2
  45. package/dist/utils/recordFormNavigation.d.ts +60 -0
  46. package/dist/utils/recordFormNavigation.js +35 -0
  47. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  48. package/dist/utils/resolvePageVarTokens.js +72 -0
  49. package/dist/views/CreateViewDialog.js +14 -1
  50. package/dist/views/ObjectView.js +27 -13
  51. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  52. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  53. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  54. package/dist/views/metadata-admin/PackagesPage.js +49 -4
  55. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  56. package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
  57. package/dist/views/metadata-admin/ResourceListPage.js +25 -10
  58. package/dist/views/metadata-admin/StudioHomePage.js +1 -5
  59. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  60. package/dist/views/metadata-admin/createBody.js +30 -0
  61. package/dist/views/metadata-admin/i18n.js +20 -2
  62. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
  63. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
  64. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
  65. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  66. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  67. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  68. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  69. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  70. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  71. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
  72. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  73. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  74. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  75. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  76. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
  77. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  78. package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
  79. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  80. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  81. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  82. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  83. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  84. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  85. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  86. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  87. package/dist/views/metadata-admin/package-scope.d.ts +9 -19
  88. package/dist/views/metadata-admin/package-scope.js +11 -25
  89. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  90. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
  91. package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
  92. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  93. package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
  94. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  95. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  96. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  97. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  98. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  99. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
  100. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
  101. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  102. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  103. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  104. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  105. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
  106. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
  107. package/package.json +38 -38
@@ -0,0 +1,95 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * VariableTextInput — a single-line input (or textarea) for an expression /
5
+ * template flow-config value, with a "{x}" data-picker (#1934) that inserts an
6
+ * in-scope reference at the cursor.
7
+ *
8
+ * Brace handling is done FOR the author (ADR-0032): the picker inserts the BARE
9
+ * reference (`discount_pct`) in `expression` fields and the braced
10
+ * `{discount_pct}` in `template` (text / textarea) fields — killing the
11
+ * recurring `{record.x}` brace-in-CEL trap. Free-text typing is untouched, and
12
+ * the picker button is hidden entirely when nothing is in scope, so an empty
13
+ * scope degrades to a plain input.
14
+ */
15
+ import * as React from 'react';
16
+ import { Braces } from 'lucide-react';
17
+ import { cn, Input, Popover, PopoverTrigger, PopoverContent, Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, } from '@object-ui/components';
18
+ /** Wrap a bare reference token for insertion into the given field mode. */
19
+ export function formatToken(token, mode) {
20
+ return mode === 'template' ? `{${token}}` : token;
21
+ }
22
+ /**
23
+ * Splice a reference into `value` at the selection `[selStart, selEnd]`, in the
24
+ * brace mode for the field, returning the new string and the caret position
25
+ * just after the inserted token. Pure (the DOM caret restore lives in the
26
+ * component); selection bounds are clamped and order-normalized so a reversed or
27
+ * out-of-range selection can't corrupt the value.
28
+ */
29
+ export function insertToken(value, mode, token, selStart, selEnd) {
30
+ const text = formatToken(token, mode);
31
+ const a = Math.min(Math.max(selStart, 0), value.length);
32
+ const b = Math.min(Math.max(selEnd, 0), value.length);
33
+ const lo = Math.min(a, b);
34
+ const hi = Math.max(a, b);
35
+ return { next: value.slice(0, lo) + text + value.slice(hi), caret: lo + text.length };
36
+ }
37
+ const PICK_LABEL = 'Insert a reference';
38
+ const SEARCH_LABEL = 'Search references…';
39
+ const EMPTY_LABEL = 'No matching references.';
40
+ export function VariableTextInput({ value, onValueChange, onBlur, onKeyDown, mode, groups, placeholder, disabled, multiline, rows = 4, mono, className, }) {
41
+ const inputRef = React.useRef(null);
42
+ // Remember the caret across the button press (which blurs the field) so the
43
+ // token lands where the author was typing — not appended at the end.
44
+ const caret = React.useRef({ start: 0, end: 0 });
45
+ const [open, setOpen] = React.useState(false);
46
+ const setRef = (el) => {
47
+ inputRef.current = el;
48
+ };
49
+ const rememberCaret = () => {
50
+ const el = inputRef.current;
51
+ if (el) {
52
+ caret.current = {
53
+ start: el.selectionStart ?? value.length,
54
+ end: el.selectionEnd ?? value.length,
55
+ };
56
+ }
57
+ };
58
+ const insert = (token) => {
59
+ const { next, caret: pos } = insertToken(value, mode, token, caret.current.start, caret.current.end);
60
+ onValueChange(next);
61
+ setOpen(false);
62
+ // Restore focus + place the caret just after the inserted token.
63
+ requestAnimationFrame(() => {
64
+ const el = inputRef.current;
65
+ if (!el)
66
+ return;
67
+ el.focus();
68
+ try {
69
+ el.setSelectionRange(pos, pos);
70
+ }
71
+ catch {
72
+ /* some input types disallow setSelectionRange */
73
+ }
74
+ caret.current = { start: pos, end: pos };
75
+ });
76
+ };
77
+ const hasScope = groups.some((g) => g.refs.length > 0);
78
+ const shared = {
79
+ value,
80
+ onChange: (e) => onValueChange(e.target.value),
81
+ onBlur,
82
+ onKeyDown,
83
+ onSelect: rememberCaret,
84
+ onKeyUp: rememberCaret,
85
+ onClick: rememberCaret,
86
+ placeholder,
87
+ disabled,
88
+ };
89
+ return (_jsxs("div", { className: cn('relative', className), children: [multiline ? (_jsx("textarea", { ref: setRef, ...shared, rows: rows, className: "w-full rounded border bg-background px-2 py-1.5 pr-8 font-mono text-xs" })) : (_jsx(Input, { ref: setRef, ...shared, className: cn('h-8 pr-8 text-sm', mono && 'font-mono') })), hasScope && !disabled && (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": PICK_LABEL, title: PICK_LABEL,
90
+ // Capture the caret on mousedown (fires before the input blur), so
91
+ // the insertion point is the author's last position.
92
+ onMouseDown: rememberCaret, className: "absolute right-1 top-1 inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: _jsx(Braces, { className: "h-3.5 w-3.5" }) }) }), _jsx(PopoverContent, { align: "end", className: "w-72 p-0",
93
+ // Keep our rAF focus-restore from fighting Radix's focus return.
94
+ onCloseAutoFocus: (e) => e.preventDefault(), children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: SEARCH_LABEL, className: "h-9" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: EMPTY_LABEL }), groups.map((g) => (_jsx(CommandGroup, { heading: g.label, children: g.refs.map((ref) => (_jsxs(CommandItem, { value: `${ref.token} ${ref.label} ${ref.detail ?? ''}`, onSelect: () => insert(ref.token), className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "truncate font-mono text-xs", children: formatToken(ref.token, mode) }), ref.detail && (_jsx("span", { className: "shrink-0 truncate text-[10px] text-muted-foreground", children: ref.detail }))] }, `${g.id}:${ref.token}`))) }, g.id)))] })] }) })] }))] }));
95
+ }
@@ -39,7 +39,12 @@ export function groupToCondition(group) {
39
39
  const mop = OP_TO_MONGO[c.operator];
40
40
  if (!mop)
41
41
  continue; // unmapped (e.g. notContains/between) — drop rather than emit a bad filter
42
- parts.push({ [c.field]: { [mop]: c.value } });
42
+ // Skip incomplete rows (no value typed yet) — emitting `{field:{$op:''}}` would
43
+ // be a silently-wrong filter (matches only empty), not "no filter".
44
+ const v = c.value;
45
+ if (v == null || v === '' || (Array.isArray(v) && v.length === 0))
46
+ continue;
47
+ parts.push({ [c.field]: { [mop]: v } });
43
48
  }
44
49
  if (parts.length === 0)
45
50
  return undefined;
@@ -93,6 +93,14 @@ export interface FlowConfigField {
93
93
  * the spec's top-level `node.waitEventConfig.eventType`.
94
94
  */
95
95
  path: string[];
96
+ /**
97
+ * Optional secondary read location used when `path` holds no value — lets the
98
+ * inspector tolerate a looser on-disk shape the engine also accepts (e.g. a
99
+ * `wait` node authored with `config.eventType` instead of the spec-canonical
100
+ * `waitEventConfig.eventType`). Reads fall back to it; the inspector writes the
101
+ * canonical `path` and prunes the fallback (migrate-on-edit).
102
+ */
103
+ fallbackPath?: string[];
96
104
  /** Human-readable field label (English — repo is English-only). */
97
105
  label: string;
98
106
  kind: FlowConfigFieldKind;
@@ -120,10 +128,17 @@ export interface FlowConfigField {
120
128
  columns?: FlowConfigColumn[];
121
129
  /** Reference target for `reference` fields — drives the combobox data source. */
122
130
  ref?: FlowReferenceSpec;
131
+ /**
132
+ * Data-picker brace mode override (#1934). Defaults by kind (`expression` →
133
+ * bare CEL, `text` / `textarea` → `{var}` template). Set `'expression'` on a
134
+ * code field (e.g. a script body) so the picker inserts bare references, not
135
+ * `{var}` — `{x}` is a syntax error in a JS/TS script.
136
+ */
137
+ refMode?: 'expression' | 'template';
123
138
  }
124
139
  /** Resolve the config fields for a node type (alias-aware). */
125
140
  export declare function fieldsForNodeType(type?: string): FlowConfigField[];
126
- /** Read the current value at a field's node path. */
141
+ /** Read the current value at a field's node path, falling back to `fallbackPath`. */
127
142
  export declare function getFieldValue(node: Record<string, unknown> | null | undefined, field: FlowConfigField): unknown;
128
143
  /**
129
144
  * The `config` key this field owns, or `undefined` for fields stored outside
@@ -163,6 +163,7 @@ const FLOW_NODE_CONFIG = {
163
163
  cfg('script', 'Code', 'textarea', {
164
164
  placeholder: 'return { ok: true };',
165
165
  help: 'Script body (JS/TS).',
166
+ refMode: 'expression',
166
167
  showWhen: { field: 'actionType', equals: ['code'] },
167
168
  }),
168
169
  cfg('outputVariables', 'Output variables', 'stringList', {
@@ -315,23 +316,27 @@ const FLOW_NODE_CONFIG = {
315
316
  { value: 'condition', label: 'Condition' },
316
317
  ],
317
318
  defaultValue: 'timer',
319
+ fallbackPath: ['config', 'eventType'],
318
320
  }),
319
321
  at('waitEventConfig', 'timerDuration', 'Duration', 'text', {
320
322
  placeholder: 'PT1H · P3D',
321
323
  help: 'ISO 8601 duration (e.g. PT1H, P3D).',
322
324
  showWhen: { field: 'waitEventConfig.eventType', equals: ['timer'] },
325
+ fallbackPath: ['config', 'timerDuration'],
323
326
  }),
324
327
  at('waitEventConfig', 'signalName', 'Signal name', 'text', {
325
328
  placeholder: 'contract.renewed',
326
329
  showWhen: { field: 'waitEventConfig.eventType', equals: ['signal', 'webhook'] },
330
+ fallbackPath: ['config', 'signalName'],
327
331
  }),
328
- at('waitEventConfig', 'timeoutMs', 'Timeout (ms)', 'number', { placeholder: '3600000' }),
332
+ at('waitEventConfig', 'timeoutMs', 'Timeout (ms)', 'number', { placeholder: '3600000', fallbackPath: ['config', 'timeoutMs'] }),
329
333
  at('waitEventConfig', 'onTimeout', 'On timeout', 'select', {
330
334
  options: [
331
335
  { value: 'fail', label: 'Fail' },
332
336
  { value: 'continue', label: 'Continue' },
333
337
  ],
334
338
  defaultValue: 'fail',
339
+ fallbackPath: ['config', 'onTimeout'],
335
340
  }),
336
341
  ],
337
342
  subflow: [
@@ -436,16 +441,22 @@ export function fieldsForNodeType(type) {
436
441
  const canonical = TYPE_ALIASES[type] ?? type;
437
442
  return FLOW_NODE_CONFIG[canonical] ?? [];
438
443
  }
439
- /** Read the current value at a field's node path. */
444
+ /** Read the current value at a field's node path, falling back to `fallbackPath`. */
440
445
  export function getFieldValue(node, field) {
441
- let cur = node;
442
- for (const seg of field.path) {
443
- if (cur && typeof cur === 'object' && !Array.isArray(cur))
444
- cur = cur[seg];
445
- else
446
- return undefined;
447
- }
448
- return cur;
446
+ const read = (path) => {
447
+ let cur = node;
448
+ for (const seg of path) {
449
+ if (cur && typeof cur === 'object' && !Array.isArray(cur))
450
+ cur = cur[seg];
451
+ else
452
+ return undefined;
453
+ }
454
+ return cur;
455
+ };
456
+ const primary = read(field.path);
457
+ if (primary !== undefined)
458
+ return primary;
459
+ return field.fallbackPath ? read(field.fallbackPath) : undefined;
449
460
  }
450
461
  /**
451
462
  * The `config` key this field owns, or `undefined` for fields stored outside
@@ -0,0 +1,39 @@
1
+ /**
2
+ * flow-ref-check — pure, scope-aware "unknown reference" detection for the flow
3
+ * inspector's inline validation (#1934 follow-up). Pairs the data-picker with a
4
+ * gentle warning when an authored expression / template references a name that
5
+ * is NOT in scope at the node — catching typos (`recrod.email`) and stale
6
+ * references the picker would have prevented.
7
+ *
8
+ * Deliberately conservative — a warning that cries wolf is worse than none:
9
+ * • Only the ROOT of a reference path is checked (`record.email` → `record`),
10
+ * so a field list is never needed; if `record` is in scope the whole path is
11
+ * accepted.
12
+ * • Function / macro calls (`daysFromNow(90)`, `has(...)`, `size(...)`) are
13
+ * skipped — an identifier immediately followed by `(` is never a reference.
14
+ * • String-literal contents are stripped before scanning.
15
+ * • Runtime globals the engine injects (`env`, `$error`, `data`, …) and CEL
16
+ * keywords/literals are allow-listed.
17
+ * • For templates only the inside of single-brace `{…}` holes is scanned.
18
+ *
19
+ * The caller supplies the in-scope ROOT names (see {@link scopeRoots}); an empty
20
+ * set means "scope unknown" and the check is skipped (returns nothing).
21
+ */
22
+ import type { ExprFieldRole } from './expression-validate';
23
+ import type { ScopeRef } from './flow-scope';
24
+ export interface UnknownRef {
25
+ /** The unresolved root identifier, as authored. */
26
+ token: string;
27
+ /** Nearest in-scope root within edit distance 2, when one exists (typo hint). */
28
+ suggestion?: string;
29
+ }
30
+ /** The set of valid root identifiers from a resolved scope's refs. */
31
+ export declare function scopeRoots(refs: ReadonlyArray<ScopeRef>): Set<string>;
32
+ /**
33
+ * Find referenced roots that are not in scope. Returns [] when clean, when the
34
+ * source is empty, or when `knownRoots` is empty (scope unknown → don't guess).
35
+ */
36
+ export declare function findUnknownRefs(source: unknown, role: ExprFieldRole, knownRoots: Set<string>): UnknownRef[];
37
+ /** Build a one-line inspector warning from unknown refs (shared by the field
38
+ * and edge inspectors). */
39
+ export declare function describeUnknownRefs(unknown: ReadonlyArray<UnknownRef>): string;
@@ -0,0 +1,114 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /** CEL keywords / literals that are never flow references. */
3
+ const CEL_RESERVED = new Set(['true', 'false', 'null', 'in']);
4
+ /**
5
+ * Roots the engine provides at runtime that won't show up in the graph-resolved
6
+ * scope. Conservative allow-list — better to miss a typo than flag a valid ref.
7
+ */
8
+ const RUNTIME_GLOBALS = new Set(['env', 'request', 'context', 'user', 'now', 'today', 'self', 'data']);
9
+ /** The set of valid root identifiers from a resolved scope's refs. */
10
+ export function scopeRoots(refs) {
11
+ const roots = new Set();
12
+ for (const r of refs) {
13
+ const root = r.token.split('.')[0];
14
+ if (root)
15
+ roots.add(root);
16
+ }
17
+ return roots;
18
+ }
19
+ /** Bounded Levenshtein distance, giving up (returns max+1) once it exceeds `max`. */
20
+ function editDistance(a, b, max = 2) {
21
+ if (Math.abs(a.length - b.length) > max)
22
+ return max + 1;
23
+ const prev = new Array(b.length + 1);
24
+ for (let j = 0; j <= b.length; j++)
25
+ prev[j] = j;
26
+ for (let i = 1; i <= a.length; i++) {
27
+ let diag = prev[0];
28
+ prev[0] = i;
29
+ let rowMin = prev[0];
30
+ for (let j = 1; j <= b.length; j++) {
31
+ const tmp = prev[j];
32
+ prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
33
+ diag = tmp;
34
+ if (prev[j] < rowMin)
35
+ rowMin = prev[j];
36
+ }
37
+ if (rowMin > max)
38
+ return max + 1;
39
+ }
40
+ return prev[b.length];
41
+ }
42
+ /** Extract reference-position root identifiers (skipping members and calls). */
43
+ function rootIdentifiers(src) {
44
+ // Strip string literals so their contents aren't scanned as references.
45
+ const noStrings = src
46
+ .replace(/'(?:[^'\\]|\\.)*'/g, ' ')
47
+ .replace(/"(?:[^"\\]|\\.)*"/g, ' ');
48
+ const out = [];
49
+ // Lookbehind keeps this to ROOTS: an identifier not preceded by `.` (member),
50
+ // a word char, or `$`. Zero-width, so adjacent tokens are never swallowed.
51
+ const re = /(?<![.\w$])([A-Za-z_$][\w$]*)/g;
52
+ let m;
53
+ while ((m = re.exec(noStrings))) {
54
+ const name = m[1];
55
+ // A trailing `(` (after optional spaces) marks a function / macro call.
56
+ if (/^\s*\(/.test(noStrings.slice(m.index + name.length)))
57
+ continue;
58
+ if (CEL_RESERVED.has(name))
59
+ continue;
60
+ out.push(name);
61
+ }
62
+ return out;
63
+ }
64
+ /**
65
+ * Find referenced roots that are not in scope. Returns [] when clean, when the
66
+ * source is empty, or when `knownRoots` is empty (scope unknown → don't guess).
67
+ */
68
+ export function findUnknownRefs(source, role, knownRoots) {
69
+ let raw = '';
70
+ if (typeof source === 'string')
71
+ raw = source;
72
+ else if (source && typeof source === 'object')
73
+ raw = String(source.source ?? '');
74
+ if (!raw.trim() || knownRoots.size === 0)
75
+ return [];
76
+ let scan = raw;
77
+ if (role === 'template') {
78
+ const holes = raw.match(/\{([^{}]+)\}/g);
79
+ if (!holes)
80
+ return [];
81
+ scan = holes.map((h) => h.slice(1, -1)).join(' ; ');
82
+ }
83
+ const seen = new Set();
84
+ const unknown = [];
85
+ for (const root of rootIdentifiers(scan)) {
86
+ if (seen.has(root))
87
+ continue;
88
+ seen.add(root);
89
+ if (knownRoots.has(root) || RUNTIME_GLOBALS.has(root) || root.startsWith('$'))
90
+ continue;
91
+ let suggestion;
92
+ let best = 3;
93
+ for (const k of knownRoots) {
94
+ const d = editDistance(root, k, 2);
95
+ if (d < best) {
96
+ best = d;
97
+ suggestion = k;
98
+ }
99
+ }
100
+ unknown.push({ token: root, suggestion: best <= 2 ? suggestion : undefined });
101
+ }
102
+ return unknown;
103
+ }
104
+ /** Build a one-line inspector warning from unknown refs (shared by the field
105
+ * and edge inspectors). */
106
+ export function describeUnknownRefs(unknown) {
107
+ if (unknown.length === 1) {
108
+ const u = unknown[0];
109
+ return u.suggestion
110
+ ? `Unknown reference \`${u.token}\` — did you mean \`${u.suggestion}\`?`
111
+ : `\`${u.token}\` is not a reference in scope at this step.`;
112
+ }
113
+ return `Not in scope: ${unknown.map((u) => `\`${u.token}\``).join(', ')}.`;
114
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * flow-scope — pure, framework-free resolution of the in-scope variable
3
+ * references at a given flow node, for the inspector's variable data-picker
4
+ * (#1934).
5
+ *
6
+ * "In scope" is GRAPH-AWARE: a reference is offered at node N only if it can
7
+ * actually exist when N runs. Concretely:
8
+ *
9
+ * - Flow variables — every entry in `draft.variables[]` (declared up-front, so
10
+ * always in scope).
11
+ * - Upstream outputs — the `outputVariable(s)` / collected screen
12
+ * `fields[].name` / `assignments` keys / `idVariable` of every ANCESTOR node
13
+ * (a node from which N is reachable, found by walking edges backwards). A
14
+ * node's OWN outputs and any DOWNSTREAM node's outputs are deliberately
15
+ * excluded — they don't exist yet when N runs. This is the property the
16
+ * picker's "a downstream output is not offered upstream" guarantee rests on.
17
+ * - Loop / map iterators — the `iteratorVariable` of an enclosing loop/map
18
+ * ancestor, surfaced as its own group.
19
+ * - Trigger record — on a record-triggered flow, the trigger object's fields.
20
+ * Referenced BARE on the start node's own entry condition (`status`,
21
+ * matching the engine's trigger-evaluation context where the changed
22
+ * record's fields are top-level) and as `record.<field>` on every
23
+ * downstream node (the convention the showcase flows use). The object's
24
+ * field list is fetched lazily by the React layer (see useFlowScope); this
25
+ * module only resolves the object NAME and the correct token prefix.
26
+ *
27
+ * The graph-walk here is the unit-tested heart of the picker; async field-list
28
+ * expansion and rendering live in the React layer so this module stays pure.
29
+ */
30
+ /** Which group a reference belongs to (drives the picker's section headers). */
31
+ export type ScopeGroupId = 'variables' | 'outputs' | 'loop' | 'trigger';
32
+ /**
33
+ * One pickable reference. `token` is the BARE form (no braces); the picker
34
+ * inserts it as-is into CEL `expression` fields and wraps it as `{token}` for
35
+ * `text` / `textarea` template fields (ADR-0032 — the brace rule is handled for
36
+ * the author).
37
+ */
38
+ export interface ScopeRef {
39
+ token: string;
40
+ /** Primary display text (the token, or the bare field name). */
41
+ label: string;
42
+ /** Secondary, muted text — a type, an origin node label, etc. */
43
+ detail?: string;
44
+ group: ScopeGroupId;
45
+ }
46
+ /** The trigger object whose fields the UI layer should fetch and expand. */
47
+ export interface TriggerScope {
48
+ objectName: string;
49
+ /** Per-field token prefix: '' on the start node (bare), 'record.' downstream. */
50
+ fieldPrefix: string;
51
+ /** Also emit `previous.<field>` refs (update / change / before-update triggers). */
52
+ includePrevious: boolean;
53
+ }
54
+ export interface FlowScope {
55
+ /**
56
+ * References resolvable WITHOUT a network fetch: flow variables, upstream
57
+ * outputs, loop iterators, and the whole-record `record` / `previous` tokens.
58
+ */
59
+ refs: ScopeRef[];
60
+ /** Present when a record trigger is in scope — the UI expands its fields. */
61
+ trigger?: TriggerScope;
62
+ }
63
+ interface FlowNodeLike {
64
+ id?: unknown;
65
+ type?: unknown;
66
+ label?: unknown;
67
+ config?: unknown;
68
+ }
69
+ interface FlowEdgeLike {
70
+ source?: unknown;
71
+ target?: unknown;
72
+ }
73
+ /**
74
+ * Ancestor node ids of `nodeId` — every node from which `nodeId` is reachable
75
+ * by following edges forward (equivalently, a reverse breadth-first walk from
76
+ * `nodeId`). Cycle-safe (a declared `back`-edge revise loop won't spin) and
77
+ * never includes `nodeId` itself.
78
+ */
79
+ export declare function flowAncestors(nodeId: string, edges: FlowEdgeLike[]): Set<string>;
80
+ /**
81
+ * The variable names a node INTRODUCES into scope for its successors — mirroring
82
+ * what the simulator (flow-simulator.ts) and engine actually write:
83
+ * `outputVariable` (single), `outputVariables` (list), an assignment node's
84
+ * `assignments` keys (map / array / flat shapes), a screen's collected
85
+ * `fields[].name`, a screen object-form's `idVariable`, and a loop/map
86
+ * `iteratorVariable` (flagged as a `loop` ref). The start node is NOT handled
87
+ * here — its trigger record is resolved separately.
88
+ */
89
+ export declare function nodeOutputRefs(node: FlowNodeLike): ScopeRef[];
90
+ /**
91
+ * Resolve the in-scope reference set at `nodeId` (graph-aware). Pure: the
92
+ * trigger object's fields are NOT expanded here (that needs an async fetch) —
93
+ * the returned `trigger` carries the object name and per-field token prefix for
94
+ * the UI layer to expand. Order: flow variables, upstream outputs, loop
95
+ * iterators, then trigger refs, de-duplicated by token.
96
+ */
97
+ export declare function resolveFlowScope(draft: Record<string, unknown>, nodeId: string | undefined): FlowScope;
98
+ /**
99
+ * Expand a trigger object's fields into per-field refs — `record.<field>`
100
+ * downstream, bare `<field>` on the start node — given an already-fetched field
101
+ * list. Split out from {@link resolveFlowScope} so it is unit-testable without a
102
+ * metadata client.
103
+ */
104
+ export declare function triggerFieldRefs(trigger: TriggerScope, fields: ReadonlyArray<{
105
+ name: string;
106
+ label?: string;
107
+ type?: string;
108
+ }>): ScopeRef[];
109
+ export {};