@object-ui/app-shell 7.0.0 → 7.2.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 (141) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/dist/console/AppContent.js +23 -17
  3. package/dist/console/ConsoleShell.d.ts +16 -0
  4. package/dist/console/ConsoleShell.js +43 -2
  5. package/dist/console/ai/AiChatPage.js +47 -16
  6. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  7. package/dist/console/ai/LiveCanvas.js +6 -4
  8. package/dist/console/home/HomeLayout.js +5 -7
  9. package/dist/console/home/HomePage.js +1 -9
  10. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  11. package/dist/console/organizations/OrganizationsPage.js +22 -3
  12. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  13. package/dist/console/organizations/provisionEnvironment.js +64 -0
  14. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  15. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  16. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  17. package/dist/environment/EnvironmentListToolbar.js +59 -0
  18. package/dist/environment/entitlements.d.ts +90 -0
  19. package/dist/environment/entitlements.js +91 -0
  20. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  21. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  22. package/dist/hooks/useActionModal.js +15 -1
  23. package/dist/hooks/useAiSurface.d.ts +59 -0
  24. package/dist/hooks/useAiSurface.js +78 -0
  25. package/dist/hooks/useChatConversation.d.ts +30 -0
  26. package/dist/hooks/useChatConversation.js +63 -0
  27. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  28. package/dist/hooks/useConsoleActionRuntime.js +42 -10
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +10 -2
  31. package/dist/layout/AppHeader.js +28 -4
  32. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  33. package/dist/layout/ConsoleFloatingChatbot.js +41 -10
  34. package/dist/layout/ConsoleLayout.js +5 -6
  35. package/dist/layout/ContextSelectors.js +59 -35
  36. package/dist/layout/agentPicker.d.ts +56 -0
  37. package/dist/layout/agentPicker.js +40 -0
  38. package/dist/preview/CommitTimeline.d.ts +15 -0
  39. package/dist/preview/CommitTimeline.js +82 -0
  40. package/dist/preview/DraftPreviewBar.js +20 -7
  41. package/dist/preview/UnpublishedAppBar.js +11 -7
  42. package/dist/preview/commitHistory.d.ts +28 -0
  43. package/dist/preview/commitHistory.js +48 -0
  44. package/dist/providers/ExpressionProvider.js +9 -3
  45. package/dist/providers/MetadataProvider.js +9 -0
  46. package/dist/utils/index.d.ts +2 -2
  47. package/dist/utils/index.js +1 -1
  48. package/dist/utils/recordFormNavigation.d.ts +60 -0
  49. package/dist/utils/recordFormNavigation.js +35 -0
  50. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  51. package/dist/utils/resolvePageVarTokens.js +72 -0
  52. package/dist/views/CreateViewDialog.js +14 -1
  53. package/dist/views/FlowRunner.d.ts +2 -30
  54. package/dist/views/FlowRunner.js +18 -50
  55. package/dist/views/ObjectView.js +26 -12
  56. package/dist/views/ScreenView.d.ts +70 -0
  57. package/dist/views/ScreenView.js +73 -0
  58. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  59. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  60. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  61. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  62. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  63. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  64. package/dist/views/metadata-admin/PackagesPage.js +58 -5
  65. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  66. package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
  67. package/dist/views/metadata-admin/ResourceListPage.js +28 -19
  68. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  69. package/dist/views/metadata-admin/anchors.js +20 -2
  70. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  71. package/dist/views/metadata-admin/createBody.js +30 -0
  72. package/dist/views/metadata-admin/i18n.js +108 -2
  73. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
  74. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
  75. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
  76. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  77. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  78. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  79. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  80. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  81. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  82. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
  83. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  84. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  85. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  86. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  87. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  88. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  89. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  90. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  91. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  92. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  93. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  94. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
  95. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  96. package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
  97. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  98. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  99. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  100. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  101. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  102. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  103. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  104. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  105. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  106. package/dist/views/metadata-admin/issuePath.js +65 -0
  107. package/dist/views/metadata-admin/package-scope.d.ts +41 -0
  108. package/dist/views/metadata-admin/package-scope.js +59 -0
  109. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  110. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  111. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
  112. package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
  113. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  114. package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
  115. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  116. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  117. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  118. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  119. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  120. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  121. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  122. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  123. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  124. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  125. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  126. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
  127. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
  128. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  129. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  130. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  131. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  132. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  133. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  134. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  135. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  136. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
  137. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  138. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
  139. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  140. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  141. package/package.json +38 -38
@@ -0,0 +1,97 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /**
3
+ * flow-expr-problems — pure, client-side detection of EXPRESSION issues across a
4
+ * whole flow draft, for surfacing in the Problems panel + canvas badges (#1934).
5
+ *
6
+ * Two kinds, mirroring the inline inspector checks but aggregated per node/edge:
7
+ * • ADR-0032 brace / shape ERRORS on every CEL field (decision conditions +
8
+ * branch expressions, screen `visibleWhen`, loop collection, edge guards…).
9
+ * Deterministic, scope-free → zero false positives.
10
+ * • Scope-aware "unknown reference" WARNINGS — a bare root not in scope at the
11
+ * node. The START node is skipped: its entry condition legitimately uses the
12
+ * trigger record's fields *bare* (`status`), which can't be told apart from a
13
+ * typo without the object schema (an async fetch this pure pass avoids); the
14
+ * inline inspector check, which does fetch, still covers it.
15
+ *
16
+ * Only CEL (`expression`) surfaces are scanned — template (`{var}`) values use
17
+ * single braces legally and are left to the inline check.
18
+ */
19
+ import { fieldsForNodeType, getFieldValue } from '../inspectors/flow-node-config';
20
+ import { resolveFlowScope } from '../inspectors/flow-scope';
21
+ import { scopeRoots, findUnknownRefs, describeUnknownRefs } from '../inspectors/flow-ref-check';
22
+ import { validateExpressionClient } from '../inspectors/expression-validate';
23
+ function asArray(v) {
24
+ return Array.isArray(v) ? v : [];
25
+ }
26
+ function asRecord(v) {
27
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
28
+ }
29
+ function str(v) {
30
+ return typeof v === 'string' && v ? v : undefined;
31
+ }
32
+ /** Brace error (error) else unknown-ref (warning, when `roots` given) for one CEL value. */
33
+ function checkCel(value, roots) {
34
+ const issue = validateExpressionClient('predicate', value);
35
+ if (issue)
36
+ return { level: 'error', message: issue.message };
37
+ if (roots && roots.size > 0) {
38
+ const unknown = findUnknownRefs(value, 'predicate', roots);
39
+ if (unknown.length > 0)
40
+ return { level: 'warning', message: describeUnknownRefs(unknown) };
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Scan a flow draft for expression problems, resolved onto node / edge targets.
46
+ * Pure: no network — the trigger object's fields are not expanded (root-only
47
+ * scope), which is why the start node is excluded from the ref check.
48
+ */
49
+ export function flowExpressionProblems(draft) {
50
+ const nodes = asArray(draft.nodes).map(asRecord);
51
+ const edges = asArray(draft.edges).map(asRecord);
52
+ const startId = str(nodes.find((n) => str(n.type) === 'start')?.id);
53
+ const out = [];
54
+ for (const node of nodes) {
55
+ const nodeId = str(node.id);
56
+ const type = str(node.type);
57
+ if (!nodeId || !type)
58
+ continue;
59
+ // Root-only scope at this node; skip the ref check on the start node (its
60
+ // bare trigger-record fields are indistinguishable from typos here).
61
+ const roots = nodeId === startId ? null : scopeRoots(resolveFlowScope(draft, nodeId).refs);
62
+ for (const field of fieldsForNodeType(type)) {
63
+ if (field.kind === 'expression') {
64
+ const hit = checkCel(getFieldValue(node, field), roots);
65
+ if (hit)
66
+ out.push({ target: { kind: 'node', nodeId }, level: hit.level, message: hit.message });
67
+ }
68
+ else if (field.kind === 'objectList' && field.columns) {
69
+ const exprCols = field.columns.filter((c) => c.kind === 'expression');
70
+ if (exprCols.length === 0)
71
+ continue;
72
+ for (const row of asArray(getFieldValue(node, field))) {
73
+ const r = asRecord(row);
74
+ const rowLabel = str(r.label);
75
+ for (const col of exprCols) {
76
+ const hit = checkCel(r[col.key], roots);
77
+ if (hit) {
78
+ const prefix = rowLabel || col.label;
79
+ out.push({ target: { kind: 'node', nodeId }, level: hit.level, message: prefix ? `${prefix}: ${hit.message}` : hit.message });
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ for (const edge of edges) {
87
+ const source = str(edge.source);
88
+ const target = str(edge.target);
89
+ if (!source || !target || edge.isDefault === true)
90
+ continue;
91
+ const roots = source === startId ? null : scopeRoots(resolveFlowScope(draft, source).refs);
92
+ const hit = checkCel(edge.condition, roots);
93
+ if (hit)
94
+ out.push({ target: { kind: 'edge', source, target }, level: hit.level, message: hit.message });
95
+ }
96
+ return out;
97
+ }
@@ -0,0 +1,84 @@
1
+ import type { DiagnosticLevel } from './simulator/flow-sim-types';
2
+ import { type FlowEdge, type FlowNode } from './flow-canvas-layout';
3
+ /** What a problem points at on the canvas — drives badge placement + reveal. */
4
+ export type FlowProblemTarget = {
5
+ kind: 'node';
6
+ nodeId: string;
7
+ } | {
8
+ kind: 'edge';
9
+ edgeKey: string;
10
+ source: string;
11
+ target: string;
12
+ } | {
13
+ kind: 'flow';
14
+ };
15
+ /** Origin of a problem — labels the panel row and lets the UI group counts. */
16
+ export type FlowProblemSource = 'structural' | 'server' | 'expression';
17
+ /** One actionable issue, resolved onto a concrete canvas element. */
18
+ export interface FlowProblem {
19
+ /** Stable-enough key for React lists. */
20
+ id: string;
21
+ level: DiagnosticLevel;
22
+ message: string;
23
+ target: FlowProblemTarget;
24
+ source: FlowProblemSource;
25
+ /**
26
+ * Extra elements to flag with the red error ring/stroke beyond `target` —
27
+ * e.g. every hop of a cycle. The badge + click-reveal still use `target`.
28
+ */
29
+ highlight?: {
30
+ nodeIds: string[];
31
+ edges: Array<{
32
+ source: string;
33
+ target: string;
34
+ }>;
35
+ };
36
+ }
37
+ /** A server diagnostic entry (subset of the layered record's `_diagnostics`). */
38
+ export interface ServerDiagnostic {
39
+ /** Dotted (or array) JSON path, e.g. `nodes.2.config.objectName`. */
40
+ path?: string | Array<string | number>;
41
+ message: string;
42
+ /** Defaults to `'error'`. */
43
+ severity?: DiagnosticLevel;
44
+ }
45
+ /** Stable `source->target` key matching an edge problem to a rendered edge. */
46
+ export declare function edgeProblemKey(source: string, target: string): string;
47
+ export interface BuildFlowProblemsArgs {
48
+ nodes: FlowNode[];
49
+ edges: FlowEdge[];
50
+ /** Server `_diagnostics`, flattened to a severity-tagged, path-keyed list. */
51
+ serverDiagnostics?: ServerDiagnostic[];
52
+ /** Declared flow variables — needed to resolve scope for the expression check. */
53
+ variables?: unknown[];
54
+ }
55
+ /**
56
+ * Build the unified problem list from structural validation + server
57
+ * diagnostics. Errors are listed before warnings; within a level each source
58
+ * keeps its own emit order (structural before server).
59
+ */
60
+ export declare function buildFlowProblems({ nodes, edges, serverDiagnostics, variables }: BuildFlowProblemsArgs): FlowProblem[];
61
+ /** A folded badge for one canvas element (errors dominate warnings). */
62
+ export interface ProblemBadge {
63
+ level: DiagnosticLevel;
64
+ /** Tooltip text — each problem message on its own line. */
65
+ title: string;
66
+ count: number;
67
+ }
68
+ export interface ProblemIndex {
69
+ byNode: Map<string, ProblemBadge>;
70
+ byEdge: Map<string, ProblemBadge>;
71
+ }
72
+ /** Group problems into per-node / per-edge badges for the canvas overlay. */
73
+ export declare function indexProblemBadges(problems: FlowProblem[]): ProblemIndex;
74
+ /**
75
+ * Error elements to paint with the red ring/stroke, derived from the unified
76
+ * problem list. ERRORS ONLY — warnings get an amber badge but no ring. Includes
77
+ * each error's `highlight` set, so a cycle paints its whole loop (every hop node
78
+ * + edge) red while its badge still sits on the closing edge. Lets the preview
79
+ * derive the red sets from `problems` instead of a second validateFlowDraft pass.
80
+ */
81
+ export declare function deriveInvalidElements(problems: FlowProblem[]): {
82
+ invalidNodeIds: string[];
83
+ invalidEdges: Set<string>;
84
+ };
@@ -0,0 +1,209 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /**
3
+ * flow-problems — unify the two flow-validation sources into one flat,
4
+ * per-element issue list that the canvas badges and the Problems panel both
5
+ * render.
6
+ *
7
+ * 1. `validateFlowDraft` (client, structural): no resolvable entry,
8
+ * unreachable nodes, a decision with no default branch, duplicate node
9
+ * ids, dangling edges, un-declared cycles.
10
+ * 2. The server `_diagnostics` already attached to the layered record
11
+ * (schema validation), each keyed by a dotted JSON path.
12
+ *
13
+ * "Surfacing, not detection": detection already exists. This module only maps
14
+ * each detected issue onto a concrete canvas element — a node id or a stable
15
+ * edge key — so a badge can sit on the offending element and a Problems-panel
16
+ * row can select + reveal it. Flow-level issues (no specific element) are kept
17
+ * too: listed in the panel, but without a badge.
18
+ */
19
+ import { validateFlowDraft } from './simulator/flow-sim-validate';
20
+ import { edgeKey } from './flow-canvas-layout';
21
+ import { flowExpressionProblems } from './flow-expr-problems';
22
+ /** Stable `source->target` key matching an edge problem to a rendered edge. */
23
+ export function edgeProblemKey(source, target) {
24
+ return `${source}->${target}`;
25
+ }
26
+ /** Resolve an edge's selection key (`edgeKey`) from its endpoints. */
27
+ function resolveEdgeKey(edges, source, target) {
28
+ const idx = edges.findIndex((e) => e.source === source && e.target === target);
29
+ return idx >= 0 ? edgeKey(edges[idx], idx) : `${source}->${target}#-1`;
30
+ }
31
+ /** Normalize a dotted/array JSON path to segments (numbers stay numeric). */
32
+ function pathSegments(path) {
33
+ if (Array.isArray(path))
34
+ return path;
35
+ if (typeof path !== 'string' || !path)
36
+ return [];
37
+ return path.split('.').map((seg) => {
38
+ const n = Number(seg);
39
+ return Number.isInteger(n) && String(n) === seg ? n : seg;
40
+ });
41
+ }
42
+ /**
43
+ * Map a structural diagnostic's optional anchors (`edge`, `cycle`, `nodeId`)
44
+ * onto a badge target. A cycle points its badge at the *closing* hop — the edge
45
+ * the author marks as a back-edge to resolve it — but flags EVERY hop (nodes +
46
+ * edges) for the red error highlight so the whole loop reads as the problem.
47
+ */
48
+ function structuralMapping(diag, edges) {
49
+ if (diag.edge) {
50
+ const { source, target } = diag.edge;
51
+ return { target: { kind: 'edge', source, target, edgeKey: resolveEdgeKey(edges, source, target) } };
52
+ }
53
+ if (diag.cycle && diag.cycle.length >= 2) {
54
+ const c = diag.cycle;
55
+ const source = c[c.length - 2];
56
+ const target = c[c.length - 1];
57
+ const nodeIds = [];
58
+ const hops = [];
59
+ for (let i = 0; i < c.length - 1; i++) {
60
+ nodeIds.push(c[i]);
61
+ hops.push({ source: c[i], target: c[i + 1] });
62
+ }
63
+ return {
64
+ target: { kind: 'edge', source, target, edgeKey: resolveEdgeKey(edges, source, target) },
65
+ highlight: { nodeIds, edges: hops },
66
+ };
67
+ }
68
+ if (diag.nodeId)
69
+ return { target: { kind: 'node', nodeId: diag.nodeId } };
70
+ return { target: { kind: 'flow' } };
71
+ }
72
+ /** Map a server diagnostic's JSON path onto a node/edge/flow target. */
73
+ function serverTarget(path, nodes, edges) {
74
+ const segs = pathSegments(path);
75
+ if (segs.length >= 2 && typeof segs[1] === 'number') {
76
+ const idx = segs[1];
77
+ if (segs[0] === 'nodes' && nodes[idx]?.id)
78
+ return { kind: 'node', nodeId: nodes[idx].id };
79
+ if (segs[0] === 'edges' && edges[idx]) {
80
+ const e = edges[idx];
81
+ return { kind: 'edge', source: e.source, target: e.target, edgeKey: edgeKey(e, idx) };
82
+ }
83
+ }
84
+ return { kind: 'flow' };
85
+ }
86
+ /** Short stable token for a target, used in a problem's React key. */
87
+ function targetKey(t) {
88
+ if (t.kind === 'node')
89
+ return `n:${t.nodeId}`;
90
+ if (t.kind === 'edge')
91
+ return `e:${t.source}->${t.target}`;
92
+ return 'flow';
93
+ }
94
+ /**
95
+ * Build the unified problem list from structural validation + server
96
+ * diagnostics. Errors are listed before warnings; within a level each source
97
+ * keeps its own emit order (structural before server).
98
+ */
99
+ export function buildFlowProblems({ nodes, edges, serverDiagnostics, variables }) {
100
+ const problems = [];
101
+ const v = validateFlowDraft(nodes, edges);
102
+ const pushStructural = (level, list) => {
103
+ list.forEach((diag, i) => {
104
+ const { target, highlight } = structuralMapping(diag, edges);
105
+ problems.push({
106
+ id: `structural:${level}:${i}:${targetKey(target)}`,
107
+ level,
108
+ message: diag.message,
109
+ target,
110
+ source: 'structural',
111
+ ...(highlight ? { highlight } : {}),
112
+ });
113
+ });
114
+ };
115
+ pushStructural('error', v.errors);
116
+ pushStructural('warning', v.warnings);
117
+ (serverDiagnostics ?? []).forEach((diag, i) => {
118
+ const level = diag.severity === 'warning' ? 'warning' : 'error';
119
+ const target = serverTarget(diag.path, nodes, edges);
120
+ problems.push({
121
+ id: `server:${i}:${targetKey(target)}`,
122
+ level,
123
+ message: diag.message,
124
+ target,
125
+ source: 'server',
126
+ });
127
+ });
128
+ // Client-side EXPRESSION issues (ADR-0032 braces + scope-aware unknown refs),
129
+ // resolved onto node / edge targets — see flow-expr-problems.
130
+ flowExpressionProblems({ nodes, edges, variables: variables ?? [] }).forEach((ep, i) => {
131
+ const target = ep.target.kind === 'edge'
132
+ ? {
133
+ kind: 'edge',
134
+ source: ep.target.source,
135
+ target: ep.target.target,
136
+ edgeKey: resolveEdgeKey(edges, ep.target.source, ep.target.target),
137
+ }
138
+ : { kind: 'node', nodeId: ep.target.nodeId };
139
+ problems.push({
140
+ id: `expression:${ep.level}:${i}:${targetKey(target)}`,
141
+ level: ep.level,
142
+ message: ep.message,
143
+ target,
144
+ source: 'expression',
145
+ });
146
+ });
147
+ // Errors first so the panel + counts lead with blockers (stable within level).
148
+ return problems
149
+ .map((p, i) => [p, i])
150
+ .sort((a, b) => {
151
+ if (a[0].level !== b[0].level)
152
+ return a[0].level === 'error' ? -1 : 1;
153
+ return a[1] - b[1];
154
+ })
155
+ .map(([p]) => p);
156
+ }
157
+ function foldBadge(list) {
158
+ const level = list.some((p) => p.level === 'error') ? 'error' : 'warning';
159
+ return { level, title: list.map((p) => p.message).join('\n'), count: list.length };
160
+ }
161
+ /** Group problems into per-node / per-edge badges for the canvas overlay. */
162
+ export function indexProblemBadges(problems) {
163
+ const nodeLists = new Map();
164
+ const edgeLists = new Map();
165
+ for (const p of problems) {
166
+ if (p.target.kind === 'node') {
167
+ const l = nodeLists.get(p.target.nodeId) ?? [];
168
+ l.push(p);
169
+ nodeLists.set(p.target.nodeId, l);
170
+ }
171
+ else if (p.target.kind === 'edge') {
172
+ const k = edgeProblemKey(p.target.source, p.target.target);
173
+ const l = edgeLists.get(k) ?? [];
174
+ l.push(p);
175
+ edgeLists.set(k, l);
176
+ }
177
+ }
178
+ const byNode = new Map();
179
+ for (const [id, list] of nodeLists)
180
+ byNode.set(id, foldBadge(list));
181
+ const byEdge = new Map();
182
+ for (const [k, list] of edgeLists)
183
+ byEdge.set(k, foldBadge(list));
184
+ return { byNode, byEdge };
185
+ }
186
+ /**
187
+ * Error elements to paint with the red ring/stroke, derived from the unified
188
+ * problem list. ERRORS ONLY — warnings get an amber badge but no ring. Includes
189
+ * each error's `highlight` set, so a cycle paints its whole loop (every hop node
190
+ * + edge) red while its badge still sits on the closing edge. Lets the preview
191
+ * derive the red sets from `problems` instead of a second validateFlowDraft pass.
192
+ */
193
+ export function deriveInvalidElements(problems) {
194
+ const nodeSet = new Set();
195
+ const edgeSet = new Set();
196
+ for (const p of problems) {
197
+ if (p.level !== 'error')
198
+ continue;
199
+ if (p.target.kind === 'node')
200
+ nodeSet.add(p.target.nodeId);
201
+ else if (p.target.kind === 'edge')
202
+ edgeSet.add(edgeProblemKey(p.target.source, p.target.target));
203
+ for (const id of p.highlight?.nodeIds ?? [])
204
+ nodeSet.add(id);
205
+ for (const e of p.highlight?.edges ?? [])
206
+ edgeSet.add(edgeProblemKey(e.source, e.target));
207
+ }
208
+ return { invalidNodeIds: [...nodeSet], invalidEdges: edgeSet };
209
+ }
@@ -35,6 +35,27 @@ export declare function newField(name: string, type: FieldTypeId, label?: string
35
35
  export declare function toLabel(name: string): string;
36
36
  /** Normalize an arbitrary string into a valid snake_case field name. */
37
37
  export declare function toFieldName(raw: string): string;
38
+ /**
39
+ * Prefix-stable variant of {@link toFieldName} for *live keystroke* input.
40
+ *
41
+ * The strict `toFieldName` trims a trailing `_`, which makes it impossible
42
+ * to TYPE a multi-word identifier into a controlled input: the field's
43
+ * `onChange` re-normalizes on every keystroke, so the instant the user
44
+ * presses `_` the value is `"repair_"` -> trimmed to `"repair"` -> the
45
+ * underscore vanishes before the next letter arrives, yielding
46
+ * `"repairticket"` instead of `"repair_ticket"`. (Authors of non-Latin
47
+ * locales hit this hardest: their label cannot derive a Latin slug, so
48
+ * they MUST type the identifier by hand.)
49
+ *
50
+ * This variant keeps a single trailing `_` so typing can continue, and
51
+ * returns `''` (not the `'field'` placeholder) on empty input so clearing
52
+ * the box actually clears it. A trailing `_` is itself a valid identifier
53
+ * per the spec ("starts with a letter, may contain letters/digits/`_`"),
54
+ * so no separate commit-time trim is required for correctness; callers
55
+ * that need a canonical form for a *complete* string (label->name
56
+ * derivation, group keys) should keep using strict `toFieldName`.
57
+ */
58
+ export declare function toFieldNameLoose(raw: string): string;
38
59
  /**
39
60
  * A declared field group (a.k.a. "section"). Lives at the object's
40
61
  * top level as `draft.fieldGroups`; individual fields opt into a group
@@ -38,9 +38,11 @@ export function indexOfField(view, name) {
38
38
  /** Build a fresh field definition for the given type. */
39
39
  export function newField(name, type, label) {
40
40
  const def = { type, label: label ?? toLabel(name) };
41
- // Seed a single empty option so picklist editor renders a row to fill in.
41
+ // Picklist-style fields start with no options; the OptionsEditor shows a
42
+ // blank input row locally and only persists rows once they have a value, so
43
+ // an unfilled row never trips the spec's identifier validation.
42
44
  if (type === 'select' || type === 'multiselect' || type === 'radio' || type === 'checkboxes') {
43
- def.options = [{ value: '', label: '' }];
45
+ def.options = [];
44
46
  }
45
47
  return { name, def };
46
48
  }
@@ -67,6 +69,39 @@ export function toFieldName(raw) {
67
69
  return `f_${sanitized}`;
68
70
  return sanitized;
69
71
  }
72
+ /**
73
+ * Prefix-stable variant of {@link toFieldName} for *live keystroke* input.
74
+ *
75
+ * The strict `toFieldName` trims a trailing `_`, which makes it impossible
76
+ * to TYPE a multi-word identifier into a controlled input: the field's
77
+ * `onChange` re-normalizes on every keystroke, so the instant the user
78
+ * presses `_` the value is `"repair_"` -> trimmed to `"repair"` -> the
79
+ * underscore vanishes before the next letter arrives, yielding
80
+ * `"repairticket"` instead of `"repair_ticket"`. (Authors of non-Latin
81
+ * locales hit this hardest: their label cannot derive a Latin slug, so
82
+ * they MUST type the identifier by hand.)
83
+ *
84
+ * This variant keeps a single trailing `_` so typing can continue, and
85
+ * returns `''` (not the `'field'` placeholder) on empty input so clearing
86
+ * the box actually clears it. A trailing `_` is itself a valid identifier
87
+ * per the spec ("starts with a letter, may contain letters/digits/`_`"),
88
+ * so no separate commit-time trim is required for correctness; callers
89
+ * that need a canonical form for a *complete* string (label->name
90
+ * derivation, group keys) should keep using strict `toFieldName`.
91
+ */
92
+ export function toFieldNameLoose(raw) {
93
+ const sanitized = raw
94
+ .trim()
95
+ .toLowerCase()
96
+ .replace(/[^a-z0-9]+/g, '_')
97
+ .replace(/^_+/g, '') // trim leading only -- a trailing `_` must survive
98
+ .replace(/_{2,}/g, '_');
99
+ if (!sanitized)
100
+ return '';
101
+ if (!/^[a-z_]/.test(sanitized))
102
+ return `f_${sanitized}`;
103
+ return sanitized;
104
+ }
70
105
  /** Read `draft.fieldGroups` into a normalized, well-typed list. */
71
106
  export function readGroups(fieldGroupsInput) {
72
107
  if (!Array.isArray(fieldGroupsInput))
@@ -0,0 +1,43 @@
1
+ /**
2
+ * screen-spec — pure helpers that map a flow `screen` node's authored `config`
3
+ * onto the runtime `ScreenSpec` (the contract {@link ScreenView} renders), plus
4
+ * `{var}` interpolation for the title/description and `visibleWhen` field
5
+ * gating. Kept framework-free so {@link ScreenPreview} stays a thin component
6
+ * and these stay unit-testable.
7
+ */
8
+ import type { ScreenSpec } from '../../ScreenView';
9
+ /** Minimal node shape the preview needs (id + authored config). */
10
+ export interface ScreenPreviewNode {
11
+ id: string;
12
+ label?: string;
13
+ config?: Record<string, unknown>;
14
+ }
15
+ /**
16
+ * Interpolate `{var}` references, mirroring the simulator's `{var}` syntax
17
+ * (flow-simulator.ts). Known vars are substituted; unknown refs stay literal so
18
+ * the author still sees the dependency in the design preview.
19
+ */
20
+ export declare function interpolate(text: string | undefined, vars: Record<string, unknown> | undefined): string;
21
+ /**
22
+ * A field's `visibleWhen` gate, evaluated against the current variables using
23
+ * the SAME evaluator the simulator uses for edge conditions (so the preview
24
+ * agrees with the simulator). Real metadata mixes `{var}` and bare-var styles
25
+ * (e.g. `{createOpportunity} == true`, `stage == "review"`), so brace
26
+ * placeholders are normalised to bare identifiers first.
27
+ *
28
+ * Fail-OPEN: a missing condition, an unparseable one, or one that references a
29
+ * not-yet-set variable (the inspector has no run state) keeps the field
30
+ * visible — the design preview never hides a configured field just because it
31
+ * lacks the data to decide. With live run state (the simulator) it gates
32
+ * faithfully: `createOpportunity == false` hides the field.
33
+ */
34
+ export declare function isFieldVisibleWhen(visibleWhen: unknown, variables: Record<string, unknown> | undefined): boolean;
35
+ /** Count of authored field rows hidden by their `visibleWhen` against `variables`. */
36
+ export declare function hiddenFieldCount(node: ScreenPreviewNode, variables: Record<string, unknown> | undefined): number;
37
+ /**
38
+ * Map a screen node's authored `config` onto the runtime `ScreenSpec` — the
39
+ * same keys the engine's `screen` executor reads (title / description / fields
40
+ * / objectName / mode / defaults / idVariable). `fields` are gated by their
41
+ * `visibleWhen` against `variables` (omit `variables` to keep every field).
42
+ */
43
+ export declare function buildScreenSpec(node: ScreenPreviewNode, variables?: Record<string, unknown>): ScreenSpec;
@@ -0,0 +1,108 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ import { evalCondition } from './simulator/flow-sim-validate';
3
+ /**
4
+ * Interpolate `{var}` references, mirroring the simulator's `{var}` syntax
5
+ * (flow-simulator.ts). Known vars are substituted; unknown refs stay literal so
6
+ * the author still sees the dependency in the design preview.
7
+ */
8
+ export function interpolate(text, vars) {
9
+ if (!text)
10
+ return '';
11
+ if (!vars)
12
+ return text;
13
+ return text.replace(/\{([^{}]+)\}/g, (m, k) => {
14
+ const v = vars[String(k).trim()];
15
+ return v === undefined || v === null ? m : String(v);
16
+ });
17
+ }
18
+ /**
19
+ * A field's `visibleWhen` gate, evaluated against the current variables using
20
+ * the SAME evaluator the simulator uses for edge conditions (so the preview
21
+ * agrees with the simulator). Real metadata mixes `{var}` and bare-var styles
22
+ * (e.g. `{createOpportunity} == true`, `stage == "review"`), so brace
23
+ * placeholders are normalised to bare identifiers first.
24
+ *
25
+ * Fail-OPEN: a missing condition, an unparseable one, or one that references a
26
+ * not-yet-set variable (the inspector has no run state) keeps the field
27
+ * visible — the design preview never hides a configured field just because it
28
+ * lacks the data to decide. With live run state (the simulator) it gates
29
+ * faithfully: `createOpportunity == false` hides the field.
30
+ */
31
+ export function isFieldVisibleWhen(visibleWhen, variables) {
32
+ if (typeof visibleWhen !== 'string' || !visibleWhen.trim())
33
+ return true;
34
+ if (!variables)
35
+ return true;
36
+ const normalized = visibleWhen.replace(/\{([\w.]+)\}/g, '$1');
37
+ const { result, error } = evalCondition(normalized, variables);
38
+ return error ? true : result;
39
+ }
40
+ /**
41
+ * Coerce the authored `config.fields` rows into runtime `ScreenFieldSpec`s,
42
+ * dropping any whose `visibleWhen` evaluates false against `variables` — exactly
43
+ * what the runtime `screen` executor emits (it filters server-side before
44
+ * sending the ScreenSpec).
45
+ */
46
+ function toScreenFields(raw, variables) {
47
+ if (!Array.isArray(raw))
48
+ return [];
49
+ const out = [];
50
+ for (const f of raw) {
51
+ if (!f || typeof f !== 'object')
52
+ continue;
53
+ const row = f;
54
+ if (typeof row.name !== 'string' || !row.name)
55
+ continue;
56
+ if (!isFieldVisibleWhen(row.visibleWhen, variables))
57
+ continue;
58
+ out.push({
59
+ name: row.name,
60
+ label: typeof row.label === 'string' ? row.label : undefined,
61
+ type: typeof row.type === 'string' ? row.type : undefined,
62
+ required: row.required === true,
63
+ });
64
+ }
65
+ return out;
66
+ }
67
+ /** Count of authored field rows hidden by their `visibleWhen` against `variables`. */
68
+ export function hiddenFieldCount(node, variables) {
69
+ const raw = node.config?.fields;
70
+ if (!Array.isArray(raw))
71
+ return 0;
72
+ let hidden = 0;
73
+ for (const f of raw) {
74
+ if (!f || typeof f !== 'object')
75
+ continue;
76
+ const row = f;
77
+ if (typeof row.name !== 'string' || !row.name)
78
+ continue;
79
+ if (!isFieldVisibleWhen(row.visibleWhen, variables))
80
+ hidden++;
81
+ }
82
+ return hidden;
83
+ }
84
+ /**
85
+ * Map a screen node's authored `config` onto the runtime `ScreenSpec` — the
86
+ * same keys the engine's `screen` executor reads (title / description / fields
87
+ * / objectName / mode / defaults / idVariable). `fields` are gated by their
88
+ * `visibleWhen` against `variables` (omit `variables` to keep every field).
89
+ */
90
+ export function buildScreenSpec(node, variables) {
91
+ const c = (node.config && typeof node.config === 'object' ? node.config : {});
92
+ const objectName = typeof c.objectName === 'string' && c.objectName ? c.objectName : undefined;
93
+ const mode = c.mode === 'edit' ? 'edit' : c.mode === 'create' ? 'create' : undefined;
94
+ const defaults = c.defaults && typeof c.defaults === 'object' && !Array.isArray(c.defaults)
95
+ ? c.defaults
96
+ : undefined;
97
+ return {
98
+ nodeId: node.id,
99
+ title: typeof c.title === 'string' ? c.title : undefined,
100
+ description: typeof c.description === 'string' ? c.description : undefined,
101
+ fields: toScreenFields(c.fields, variables),
102
+ kind: objectName ? 'object-form' : 'fields',
103
+ objectName,
104
+ mode,
105
+ defaults,
106
+ idVariable: typeof c.idVariable === 'string' ? c.idVariable : undefined,
107
+ };
108
+ }
@@ -28,6 +28,12 @@ export interface SimEdge {
28
28
  };
29
29
  isDefault?: boolean;
30
30
  label?: string;
31
+ /**
32
+ * Connection type (FlowEdgeSchema). A `'back'` edge is an ADR-0044 declared
33
+ * back-edge: traversed normally at run time, but excluded from DAG cycle
34
+ * validation so a revise/rework loop can re-enter an earlier node.
35
+ */
36
+ type?: string;
31
37
  }
32
38
  export type SimStatus = 'idle' | 'running' | 'paused' | 'done' | 'error';
33
39
  /** One evaluated outgoing edge of a decision (kept for the debug timeline). */
@@ -74,8 +80,22 @@ export interface SimState {
74
80
  export type DiagnosticLevel = 'error' | 'warning';
75
81
  export interface Diagnostic {
76
82
  level: DiagnosticLevel;
83
+ /** The node this diagnostic points at (for an inline badge + click-to-reveal). */
77
84
  nodeId?: string;
85
+ /**
86
+ * The edge this diagnostic points at (e.g. a dangling endpoint). Carries the
87
+ * endpoints so the designer can key the inline badge by `source->target`.
88
+ */
89
+ edge?: {
90
+ source: string;
91
+ target: string;
92
+ };
78
93
  message: string;
94
+ /**
95
+ * For a cycle error: the node path that closes the loop (e.g. `['a','b','a']`),
96
+ * so the designer can paint the offending edges/nodes inline on the canvas.
97
+ */
98
+ cycle?: string[];
79
99
  }
80
100
  export interface FlowValidation {
81
101
  errors: Diagnostic[];
@@ -4,5 +4,12 @@ export declare function evalCondition(expr: string, variables: Record<string, un
4
4
  result: boolean;
5
5
  error?: string;
6
6
  };
7
+ /**
8
+ * Find a directed cycle in `edges` over `nodeIds`, returned as the node path
9
+ * that closes the loop (e.g. `['a','b','a']`), or `null` when the graph is a
10
+ * DAG. Iterative DFS with a recursion-stack colour map; the first cycle found
11
+ * wins (enough to report — the author fixes one at a time).
12
+ */
13
+ export declare function findCycle(nodeIds: string[], edges: SimEdge[]): string[] | null;
7
14
  /** Static structural checks; `errors` block Run, `warnings` are advisory. */
8
15
  export declare function validateFlowDraft(nodes: SimNode[], edges: SimEdge[]): FlowValidation;