@ixo/oracle-runtime-workers 0.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.
- package/README.md +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowsPlugin — the flow-builder plugin. The agent designs reusable flow
|
|
3
|
+
* *templates* by conversation (steps, data wiring, conditions, schedules,
|
|
4
|
+
* assignees, forms), inspects live flow *runs*, and fixes the template when a
|
|
5
|
+
* run reveals a build mistake. It builds on the editor's Qi Flow engine; the
|
|
6
|
+
* user instantiates and runs the flow in the portal. The agent never executes,
|
|
7
|
+
* signs, or holds a key — it only writes flow documents and reads their state.
|
|
8
|
+
*
|
|
9
|
+
* It reuses the editor plugin's Matrix provider + readers but contributes its
|
|
10
|
+
* own flow tools; it does not require the editor plugin to be loaded.
|
|
11
|
+
*
|
|
12
|
+
* This module must stay statically LIGHT: `new FlowsPlugin()` happens at
|
|
13
|
+
* Worker module scope, and the tool builders' module graph (`tools/*` →
|
|
14
|
+
* actions/translator → `@ixo/editor/core` → `@ixo/matrix-crdt` → vscode-lib)
|
|
15
|
+
* schedules a timer during module evaluation (vscode-lib's
|
|
16
|
+
* `IdleValue`/`runWhenIdle`), which workerd forbids in global scope. The tool
|
|
17
|
+
* modules are therefore reached only via `await import()` inside
|
|
18
|
+
* `getRequestTools`, which runs per turn inside a Durable Object request
|
|
19
|
+
* context where timers are allowed.
|
|
20
|
+
*/
|
|
21
|
+
import type { MatrixClient } from 'matrix-js-sdk';
|
|
22
|
+
import { OraclePlugin } from '../../plugin-api/oracle-plugin';
|
|
23
|
+
import type {
|
|
24
|
+
PluginManifest,
|
|
25
|
+
PluginTool,
|
|
26
|
+
RuntimeContext,
|
|
27
|
+
} from '../../plugin-api/types';
|
|
28
|
+
import { FLOWS_PLUGIN_NAME } from './prompts';
|
|
29
|
+
|
|
30
|
+
export interface FlowsPluginOptions {
|
|
31
|
+
/** A long-lived Matrix client owned by the host (or a test), shared across rooms. */
|
|
32
|
+
matrixClient?: MatrixClient;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Discovery surface (returned by `list_capabilities` / `load_capability`). The
|
|
37
|
+
* `summary` is the one-line teaser; `whenToUse`/`whenNotToUse`/`examples` teach
|
|
38
|
+
* the agent when and how to load it. The full operating contract lives in
|
|
39
|
+
* `FLOWS_OPERATING_GUIDE` (prompts.ts), injected into the system prompt once
|
|
40
|
+
* the capability is loaded — kept out of the manifest so it costs no tokens on
|
|
41
|
+
* turns where flows is never used.
|
|
42
|
+
*/
|
|
43
|
+
const manifest: PluginManifest = {
|
|
44
|
+
title: 'Flow Builder',
|
|
45
|
+
summary:
|
|
46
|
+
'Build runnable automation flows by conversation. Assemble a reusable flow *template* from action blocks — wiring each step into the next, with conditions, schedules, assignees, and forms — then the user reviews and runs it in the portal. You design and write the flow in planning mode (propose the blocks, confirm, then build); you never execute, sign, or run a step.',
|
|
47
|
+
whenToUse: [
|
|
48
|
+
'User wants to build an automation/workflow/flow from steps or actions.',
|
|
49
|
+
"User wants to change a step's inputs, condition, trigger, schedule, or assignee.",
|
|
50
|
+
'User wants to know what an action needs (its inputs/prerequisites) before adding it.',
|
|
51
|
+
'User wants to fill in a form/survey on a flow.',
|
|
52
|
+
'User wants to inspect a flow run, find out why a step failed, and fix the template.',
|
|
53
|
+
],
|
|
54
|
+
whenNotToUse: [
|
|
55
|
+
'Editing prose/pages/documents (use the editor).',
|
|
56
|
+
'Actually executing/running/signing a step — that happens in the portal, by the user.',
|
|
57
|
+
],
|
|
58
|
+
examples: [
|
|
59
|
+
{
|
|
60
|
+
user: 'Build a flow that emails the applicant when their claim is approved',
|
|
61
|
+
thought:
|
|
62
|
+
'Discover the blocks and their requirements first, then propose a plan before writing anything.',
|
|
63
|
+
tool: 'list_actions',
|
|
64
|
+
args: { tag: 'claims' },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
user: 'What does the submit-claim step need before I can add it?',
|
|
68
|
+
tool: 'describe_action',
|
|
69
|
+
args: { action: 'qi/claim.submit' },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
user: 'Why did the second step of my flow fail?',
|
|
73
|
+
tool: 'flow_status',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
tags: ['flows', 'templates', 'automation', 'workflow', 'forms'],
|
|
77
|
+
category: 'automation',
|
|
78
|
+
visibility: 'on-demand',
|
|
79
|
+
stability: 'beta',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export class FlowsPlugin extends OraclePlugin {
|
|
83
|
+
readonly name = FLOWS_PLUGIN_NAME;
|
|
84
|
+
readonly version = '0.1.0';
|
|
85
|
+
readonly manifest = manifest;
|
|
86
|
+
|
|
87
|
+
private readonly matrixClient?: MatrixClient;
|
|
88
|
+
|
|
89
|
+
constructor(options: FlowsPluginOptions = {}) {
|
|
90
|
+
super();
|
|
91
|
+
this.matrixClient = options.matrixClient;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override async getRequestTools(
|
|
95
|
+
_rtCtx: RuntimeContext,
|
|
96
|
+
): Promise<PluginTool[]> {
|
|
97
|
+
// Lazy on purpose — see the module header. The module registry caches the
|
|
98
|
+
// imports, so only the first turn in an isolate pays for them.
|
|
99
|
+
const [authoring, discovery, forms, inspect, linkage, settings] =
|
|
100
|
+
await Promise.all([
|
|
101
|
+
import('./tools/authoring'),
|
|
102
|
+
import('./tools/discovery'),
|
|
103
|
+
import('./tools/forms'),
|
|
104
|
+
import('./tools/inspect'),
|
|
105
|
+
import('./tools/linkage'),
|
|
106
|
+
import('./tools/settings'),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
return [
|
|
110
|
+
// Discovery
|
|
111
|
+
discovery.buildListActionsTool(),
|
|
112
|
+
discovery.buildDescribeActionTool(),
|
|
113
|
+
discovery.buildListReferenceableFieldsTool(this.matrixClient),
|
|
114
|
+
// Linkage
|
|
115
|
+
...linkage.buildLinkageTools(this.matrixClient),
|
|
116
|
+
// Inspect
|
|
117
|
+
inspect.buildReadFlowTool(this.matrixClient),
|
|
118
|
+
inspect.buildGetStepTool(this.matrixClient),
|
|
119
|
+
inspect.buildFlowStatusTool(this.matrixClient),
|
|
120
|
+
inspect.buildExplainStepTool(this.matrixClient),
|
|
121
|
+
// Authoring
|
|
122
|
+
...authoring.buildAuthoringTools(this.matrixClient),
|
|
123
|
+
// Settings mutators
|
|
124
|
+
...settings.buildSettingsTools(this.matrixClient),
|
|
125
|
+
// Forms
|
|
126
|
+
...forms.buildFormTools(this.matrixClient),
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form read/fill (spec §2.5, §7.2). The agent can read a human form step's
|
|
3
|
+
* questions and PRE-FILL answers; it never submits — the user reviews and
|
|
4
|
+
* submits in the portal.
|
|
5
|
+
*
|
|
6
|
+
* Verified shape: the survey schema lives in `block.props.surveySchema` (a
|
|
7
|
+
* JSON-stringified SurveyJS schema); the answers persist in the RUNTIME map at
|
|
8
|
+
* `runtime.output.form.answers` (a JSON string). Filling writes that runtime
|
|
9
|
+
* field and deliberately does NOT set `state:'completed'` (submission is the
|
|
10
|
+
* user's action). We reuse the editor plugin's native survey + runtime helpers.
|
|
11
|
+
*/
|
|
12
|
+
import type { Doc as YDoc } from 'yjs';
|
|
13
|
+
import {
|
|
14
|
+
extractBlockProperties,
|
|
15
|
+
getBlockDetail,
|
|
16
|
+
readRuntimeState,
|
|
17
|
+
updateRuntimeState,
|
|
18
|
+
type SurveyElement,
|
|
19
|
+
} from './ydoc-helpers';
|
|
20
|
+
import { setStepInputs } from './edit';
|
|
21
|
+
import { FlowError } from './errors';
|
|
22
|
+
import { inferPortType } from './port-types';
|
|
23
|
+
import { readStep } from './read';
|
|
24
|
+
import { stepIdToBlockId } from './translator';
|
|
25
|
+
|
|
26
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return typeof value === 'object' && value !== null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function safeParseObject(value: string): unknown {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(value);
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FormQuestion {
|
|
39
|
+
name: string;
|
|
40
|
+
type: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
isRequired: boolean;
|
|
43
|
+
/** Allowed values for choice questions — the agent must fill the value, not the label. */
|
|
44
|
+
choices?: Array<{ value: string; text: string }>;
|
|
45
|
+
visibleIf?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FillFormResult {
|
|
49
|
+
applied: string[];
|
|
50
|
+
rejected: Array<{ name: string; reason: string }>;
|
|
51
|
+
validation: { ok: boolean; warnings: string[] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readSurveySchema(
|
|
55
|
+
doc: YDoc,
|
|
56
|
+
stepId: string,
|
|
57
|
+
): { blockId: string; questions: FormQuestion[] } {
|
|
58
|
+
const blockId = stepIdToBlockId(stepId);
|
|
59
|
+
const detail = getBlockDetail(doc, blockId);
|
|
60
|
+
if (!detail)
|
|
61
|
+
throw new FlowError('step_not_found', `No step "${stepId}" in this flow.`);
|
|
62
|
+
const props = extractBlockProperties(detail);
|
|
63
|
+
|
|
64
|
+
// The schema location differs by form mechanism: the `form` block stores it at
|
|
65
|
+
// top-level `surveySchema`; the `qi/human.form.submit` action stores it inside
|
|
66
|
+
// `inputs.surveySchema`. Check both. (`extractBlockProperties` may auto-parse a
|
|
67
|
+
// JSON prop to an object, so a value can arrive as an object or a string.)
|
|
68
|
+
let raw: unknown = props.surveySchema;
|
|
69
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
70
|
+
const inputs = props.inputs;
|
|
71
|
+
const parsedInputs =
|
|
72
|
+
typeof inputs === 'string' ? safeParseObject(inputs) : inputs;
|
|
73
|
+
if (isRecord(parsedInputs)) raw = parsedInputs.surveySchema;
|
|
74
|
+
}
|
|
75
|
+
if (typeof raw === 'string' && raw.length > 0) raw = safeParseObject(raw);
|
|
76
|
+
|
|
77
|
+
if (!isRecord(raw))
|
|
78
|
+
throw new FlowError(
|
|
79
|
+
'validation_failed',
|
|
80
|
+
`Step "${stepId}" has no form to fill.`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Accept either the pages form (`{ pages: [{ elements }] }`) or the flat
|
|
84
|
+
// elements form (`{ elements }`) — SurveyJS renders both.
|
|
85
|
+
const pages = Array.isArray(raw.pages)
|
|
86
|
+
? raw.pages
|
|
87
|
+
: Array.isArray(raw.elements)
|
|
88
|
+
? [{ elements: raw.elements }]
|
|
89
|
+
: [];
|
|
90
|
+
return { blockId, questions: flattenQuestions(pages) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Flatten SurveyJS pages -> questions, descending into panels (which carry nested elements). */
|
|
94
|
+
function flattenQuestions(
|
|
95
|
+
pages: Array<{ elements?: SurveyElement[] }>,
|
|
96
|
+
): FormQuestion[] {
|
|
97
|
+
const out: FormQuestion[] = [];
|
|
98
|
+
const visit = (elements: SurveyElement[] | undefined): void => {
|
|
99
|
+
for (const el of elements ?? []) {
|
|
100
|
+
if (el.elements || el.templateElements) {
|
|
101
|
+
visit(el.elements);
|
|
102
|
+
visit(el.templateElements);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
out.push({
|
|
106
|
+
name: el.name,
|
|
107
|
+
type: el.type,
|
|
108
|
+
title: el.title,
|
|
109
|
+
isRequired: el.isRequired ?? false,
|
|
110
|
+
choices: el.choices,
|
|
111
|
+
visibleIf: el.visibleIf,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
for (const page of pages) visit(page.elements);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Read a form step's questions with their exact fillable values. */
|
|
120
|
+
export function describeForm(
|
|
121
|
+
doc: YDoc,
|
|
122
|
+
stepId: string,
|
|
123
|
+
): { questions: FormQuestion[] } {
|
|
124
|
+
const { questions } = readSurveySchema(doc, stepId);
|
|
125
|
+
return { questions };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** One referenceable output a form step exposes downstream. */
|
|
129
|
+
export interface FormOutputField {
|
|
130
|
+
field: string;
|
|
131
|
+
type: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The output fields a form step exposes for downstream references. A form emits
|
|
136
|
+
* a single bundled output, NOT a field per question:
|
|
137
|
+
* - `answers` — the whole answers object,
|
|
138
|
+
* - `form.answers` — that object as a JSON string (matches the form block's
|
|
139
|
+
* runtime output shape).
|
|
140
|
+
* Plus, for each question, the load-bearing individual runtime path
|
|
141
|
+
* `answers.<questionName>`. A scalar input (e.g. a DID for `matrix.dm.targetDid`)
|
|
142
|
+
* MUST reference the single field — wiring the whole `answers` object stringifies
|
|
143
|
+
* to "[object Object]" at runtime. The per-question paths resolve because the
|
|
144
|
+
* form action's `run()` returns `answers` as a real object.
|
|
145
|
+
*
|
|
146
|
+
* Returns just the two bundle fields when the step has no readable survey schema
|
|
147
|
+
* yet (so callers still get a usable answer for an unconfigured form step).
|
|
148
|
+
*/
|
|
149
|
+
export function formOutputFields(doc: YDoc, stepId: string): FormOutputField[] {
|
|
150
|
+
const fields: FormOutputField[] = [
|
|
151
|
+
{ field: 'answers', type: 'object' },
|
|
152
|
+
{ field: 'form.answers', type: 'string' },
|
|
153
|
+
];
|
|
154
|
+
let questions: FormQuestion[];
|
|
155
|
+
try {
|
|
156
|
+
questions = readSurveySchema(doc, stepId).questions;
|
|
157
|
+
} catch {
|
|
158
|
+
return fields;
|
|
159
|
+
}
|
|
160
|
+
for (const q of questions) {
|
|
161
|
+
fields.push({ field: `answers.${q.name}`, type: inferPortType(q.name) });
|
|
162
|
+
}
|
|
163
|
+
return fields;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Existing form answers from the runtime map (if any). */
|
|
167
|
+
function existingAnswers(doc: YDoc, blockId: string): Record<string, unknown> {
|
|
168
|
+
const runtime = readRuntimeState(doc, blockId)[blockId];
|
|
169
|
+
const output =
|
|
170
|
+
runtime && typeof runtime.output === 'object'
|
|
171
|
+
? (runtime.output as Record<string, unknown>)
|
|
172
|
+
: undefined;
|
|
173
|
+
const form =
|
|
174
|
+
output && typeof output.form === 'object'
|
|
175
|
+
? (output.form as Record<string, unknown>)
|
|
176
|
+
: undefined;
|
|
177
|
+
if (typeof form?.answers !== 'string') return {};
|
|
178
|
+
try {
|
|
179
|
+
const parsed: unknown = JSON.parse(form.answers);
|
|
180
|
+
return parsed && typeof parsed === 'object'
|
|
181
|
+
? (parsed as Record<string, unknown>)
|
|
182
|
+
: {};
|
|
183
|
+
} catch {
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Pre-fill a form step's answers. Validates against the schema (choice questions
|
|
190
|
+
* accept only declared values), writes `runtime.output.form.answers`, and never
|
|
191
|
+
* marks the step complete. `merge` (default true) keeps existing answers.
|
|
192
|
+
*/
|
|
193
|
+
export function fillForm(
|
|
194
|
+
doc: YDoc,
|
|
195
|
+
stepId: string,
|
|
196
|
+
answers: Record<string, unknown>,
|
|
197
|
+
merge = true,
|
|
198
|
+
): FillFormResult {
|
|
199
|
+
const { blockId, questions } = readSurveySchema(doc, stepId);
|
|
200
|
+
const byName = new Map(questions.map((q) => [q.name, q]));
|
|
201
|
+
|
|
202
|
+
const applied: string[] = [];
|
|
203
|
+
const rejected: Array<{ name: string; reason: string }> = [];
|
|
204
|
+
const accepted: Record<string, unknown> = {};
|
|
205
|
+
|
|
206
|
+
for (const [name, value] of Object.entries(answers)) {
|
|
207
|
+
const question = byName.get(name);
|
|
208
|
+
if (!question) {
|
|
209
|
+
rejected.push({ name, reason: 'no such question' });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (question.choices && question.choices.length > 0) {
|
|
213
|
+
const allowed = question.choices.map((c) => c.value);
|
|
214
|
+
const values = Array.isArray(value) ? value : [value];
|
|
215
|
+
const bad = values.find((v) => !allowed.includes(String(v)));
|
|
216
|
+
if (bad !== undefined) {
|
|
217
|
+
rejected.push({
|
|
218
|
+
name,
|
|
219
|
+
reason: `"${String(bad)}" is not an allowed value`,
|
|
220
|
+
});
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
accepted[name] = value;
|
|
225
|
+
applied.push(name);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const base = merge ? existingAnswers(doc, blockId) : {};
|
|
229
|
+
const finalAnswers = { ...base, ...accepted };
|
|
230
|
+
|
|
231
|
+
doc.transact(() => {
|
|
232
|
+
const runtime = readRuntimeState(doc, blockId)[blockId] ?? {};
|
|
233
|
+
const output =
|
|
234
|
+
runtime.output && typeof runtime.output === 'object'
|
|
235
|
+
? (runtime.output as Record<string, unknown>)
|
|
236
|
+
: {};
|
|
237
|
+
const form =
|
|
238
|
+
output.form && typeof output.form === 'object'
|
|
239
|
+
? (output.form as Record<string, unknown>)
|
|
240
|
+
: {};
|
|
241
|
+
// Write answers only — never set state:'completed' (that is the user's submit).
|
|
242
|
+
updateRuntimeState(doc, blockId, {
|
|
243
|
+
output: {
|
|
244
|
+
...output,
|
|
245
|
+
form: { ...form, answers: JSON.stringify(finalAnswers) },
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const requiredMissing = questions
|
|
251
|
+
.filter((q) => q.isRequired && !(q.name in finalAnswers))
|
|
252
|
+
.map((q) => q.name);
|
|
253
|
+
const warnings =
|
|
254
|
+
requiredMissing.length > 0
|
|
255
|
+
? [`Required questions still unanswered: ${requiredMissing.join(', ')}`]
|
|
256
|
+
: [];
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
applied,
|
|
260
|
+
rejected,
|
|
261
|
+
validation: { ok: rejected.length === 0, warnings },
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── Authoring a form's questions (build time) ───────────────────────────────
|
|
266
|
+
|
|
267
|
+
/** One question the agent wants on a form (friendly shape — no SurveyJS internals). */
|
|
268
|
+
export interface FormQuestionInput {
|
|
269
|
+
/** The answer key this question produces (referenced downstream as output.<name>). */
|
|
270
|
+
name: string;
|
|
271
|
+
/** The question text shown to the user (defaults to the name). */
|
|
272
|
+
label?: string;
|
|
273
|
+
/**
|
|
274
|
+
* Question type. Common SurveyJS types: text (default), comment (multi-line),
|
|
275
|
+
* dropdown, radiogroup, checkbox (multi-select), boolean, rating.
|
|
276
|
+
*/
|
|
277
|
+
type?: string;
|
|
278
|
+
required?: boolean;
|
|
279
|
+
/** Allowed options for choice questions (dropdown/radiogroup/checkbox). */
|
|
280
|
+
choices?: string[];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Build a SurveyJS schema (pages form) from friendly questions. */
|
|
284
|
+
function buildSurveyJs(questions: FormQuestionInput[]): {
|
|
285
|
+
pages: Array<{ name: string; elements: Array<Record<string, unknown>> }>;
|
|
286
|
+
} {
|
|
287
|
+
return {
|
|
288
|
+
pages: [
|
|
289
|
+
{
|
|
290
|
+
name: 'page1',
|
|
291
|
+
elements: questions.map((q) => {
|
|
292
|
+
const element: Record<string, unknown> = {
|
|
293
|
+
type: q.type && q.type.length > 0 ? q.type : 'text',
|
|
294
|
+
name: q.name,
|
|
295
|
+
title: q.label ?? q.name,
|
|
296
|
+
};
|
|
297
|
+
if (q.required) element.isRequired = true;
|
|
298
|
+
if (q.choices && q.choices.length > 0)
|
|
299
|
+
element.choices = q.choices.map((c) => ({ value: c, text: c }));
|
|
300
|
+
return element;
|
|
301
|
+
}),
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Author a form step's questions. Builds a SurveyJS schema from the friendly
|
|
309
|
+
* questions and writes it to the step's `surveySchema` input (where the
|
|
310
|
+
* `qi/human.form.submit` action reads it). Without this a form step shows
|
|
311
|
+
* "Configure Survey Schema JSON…" and cannot run — this is how the agent
|
|
312
|
+
* actually defines the form, not just adds an empty form step.
|
|
313
|
+
*/
|
|
314
|
+
export function setFormSchema(
|
|
315
|
+
doc: YDoc,
|
|
316
|
+
roomId: string,
|
|
317
|
+
stepId: string,
|
|
318
|
+
questions: FormQuestionInput[],
|
|
319
|
+
): { questionCount: number } {
|
|
320
|
+
const step = readStep(doc, roomId, stepId);
|
|
321
|
+
if (!step)
|
|
322
|
+
throw new FlowError('step_not_found', `No step "${stepId}" in this flow.`);
|
|
323
|
+
const survey = buildSurveyJs(questions);
|
|
324
|
+
// Preserve any existing inputs; surveySchema is stored as a JSON string.
|
|
325
|
+
setStepInputs(doc, stepId, {
|
|
326
|
+
...(step.inputs ?? {}),
|
|
327
|
+
surveySchema: JSON.stringify(survey),
|
|
328
|
+
});
|
|
329
|
+
return { questionCount: questions.length };
|
|
330
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed wiring checks (spec §3.2). Powered by the action output schemas + the
|
|
3
|
+
* port-type inference (port-types.ts) and the metadata overlay (§7.5).
|
|
4
|
+
*
|
|
5
|
+
* Input ports aren't shipped by the registry, so `check_link`'s primary job is
|
|
6
|
+
* catching a reference to an output field a step doesn't actually produce; it
|
|
7
|
+
* additionally rejects a type mismatch when both sides resolve to a known core
|
|
8
|
+
* port type (the input side via an overlay entry).
|
|
9
|
+
*/
|
|
10
|
+
import type { Doc as YDoc } from 'yjs';
|
|
11
|
+
import type { ActionDefinition } from '@ixo/editor/core';
|
|
12
|
+
import { getAllActions } from '@ixo/editor/core';
|
|
13
|
+
import { ACTION_METADATA, type ActionRequirement } from './action-metadata';
|
|
14
|
+
import { getActionDef, isFormAction } from './actions';
|
|
15
|
+
import { formOutputFields } from './forms';
|
|
16
|
+
import { inferPortType, isCorePortType } from './port-types';
|
|
17
|
+
import { readFlowSpec } from './read';
|
|
18
|
+
|
|
19
|
+
export interface LinkCheck {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
warning?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CompatibleProducer {
|
|
26
|
+
action: string;
|
|
27
|
+
field: string;
|
|
28
|
+
type: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Output port type for a field of an action: overlay override, else inference. */
|
|
32
|
+
function outputPortType(
|
|
33
|
+
def: ActionDefinition,
|
|
34
|
+
field: string,
|
|
35
|
+
): string | undefined {
|
|
36
|
+
const overlay = ACTION_METADATA[def.type]?.outputPorts?.find(
|
|
37
|
+
(p) => p.path === field,
|
|
38
|
+
);
|
|
39
|
+
if (overlay) return overlay.portType;
|
|
40
|
+
const out = def.outputSchema?.find(
|
|
41
|
+
(f: { path: string; type: string }) => f.path === field,
|
|
42
|
+
);
|
|
43
|
+
return out ? inferPortType(field, out.type) : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Declared input port type for an action's input — only available from the overlay. */
|
|
47
|
+
function overlayInputPortType(
|
|
48
|
+
actionType: string,
|
|
49
|
+
input: string,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
return ACTION_METADATA[actionType]?.inputPorts?.find((p) => p.path === input)
|
|
52
|
+
?.portType;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The port type an input expects: overlay if declared, else inferred from the input name. */
|
|
56
|
+
function expectedInputType(actionType: string, input: string): string {
|
|
57
|
+
return overlayInputPortType(actionType, input) ?? inferPortType(input);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Can `fromStep.field` feed `toStep.input`? */
|
|
61
|
+
export function checkLink(
|
|
62
|
+
doc: YDoc,
|
|
63
|
+
ref: string,
|
|
64
|
+
fromStep: string,
|
|
65
|
+
field: string,
|
|
66
|
+
toStep: string,
|
|
67
|
+
input: string,
|
|
68
|
+
): LinkCheck {
|
|
69
|
+
const flow = readFlowSpec(doc, ref);
|
|
70
|
+
if (!flow) return { ok: false, reason: 'That flow does not exist.' };
|
|
71
|
+
|
|
72
|
+
const from = flow.steps.find((s) => s.id === fromStep);
|
|
73
|
+
const to = flow.steps.find((s) => s.id === toStep);
|
|
74
|
+
if (!from)
|
|
75
|
+
return { ok: false, reason: `No step "${fromStep}" in this flow.` };
|
|
76
|
+
if (!to) return { ok: false, reason: `No step "${toStep}" in this flow.` };
|
|
77
|
+
|
|
78
|
+
const fromDef = getActionDef(from.action);
|
|
79
|
+
if (!fromDef)
|
|
80
|
+
return { ok: false, reason: `Step "${fromStep}" has an unknown action.` };
|
|
81
|
+
|
|
82
|
+
let outType = outputPortType(fromDef, field);
|
|
83
|
+
// A form step's outputSchema only declares the bundled `answers`/`form.answers`
|
|
84
|
+
// ports, so the individual `answers.<questionName>` paths fall through here.
|
|
85
|
+
// Validate them against the step's actual survey schema and infer the type.
|
|
86
|
+
if (outType === undefined && isFormAction(fromDef)) {
|
|
87
|
+
outType = formOutputFields(doc, fromStep).find(
|
|
88
|
+
(f) => f.field === field,
|
|
89
|
+
)?.type;
|
|
90
|
+
}
|
|
91
|
+
if (outType === undefined) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
reason: `Step "${fromStep}" does not produce an output field "${field}".`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Input side is only typed when the overlay declares it; otherwise accept and note.
|
|
99
|
+
const declaredInput = overlayInputPortType(to.action, input);
|
|
100
|
+
if (declaredInput === undefined) {
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
warning: `"${input}" has no declared type, so the value type was not verified.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (outType === declaredInput) return { ok: true };
|
|
107
|
+
if (isCorePortType(outType) && isCorePortType(declaredInput)) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
reason: `Type mismatch: "${field}" is ${outType} but "${input}" expects ${declaredInput}.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
warning: `Could not confirm ${outType} is compatible with ${declaredInput}.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Actions (and the field) that produce a value compatible with a step's input. */
|
|
120
|
+
export function compatibleActions(
|
|
121
|
+
doc: YDoc,
|
|
122
|
+
ref: string,
|
|
123
|
+
stepId: string,
|
|
124
|
+
forInput: string,
|
|
125
|
+
): CompatibleProducer[] {
|
|
126
|
+
const flow = readFlowSpec(doc, ref);
|
|
127
|
+
const step = flow?.steps.find((s) => s.id === stepId);
|
|
128
|
+
const wanted = step
|
|
129
|
+
? expectedInputType(step.action, forInput)
|
|
130
|
+
: inferPortType(forInput);
|
|
131
|
+
|
|
132
|
+
const out: CompatibleProducer[] = [];
|
|
133
|
+
for (const def of getAllActions()) {
|
|
134
|
+
for (const f of def.outputSchema ?? []) {
|
|
135
|
+
const type = outputPortType(def, f.path);
|
|
136
|
+
if (type === wanted) out.push({ action: def.type, field: f.path, type });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Declarative prerequisites of an action (overlay-backed). */
|
|
143
|
+
export function requirements(action: string): ActionRequirement[] {
|
|
144
|
+
const def = getActionDef(action);
|
|
145
|
+
if (!def) return [];
|
|
146
|
+
return ACTION_METADATA[def.type]?.requires ?? [];
|
|
147
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic port types (spec §5/§7.5). Actions don't ship typed input/output
|
|
3
|
+
* ports, so we infer a semantic type from the field name (a documented core
|
|
4
|
+
* vocabulary), with the metadata overlay as the authoritative override.
|
|
5
|
+
*
|
|
6
|
+
* Open vocabulary: an unrecognised field falls back to its primitive type.
|
|
7
|
+
*/
|
|
8
|
+
export const CORE_PORT_TYPES = [
|
|
9
|
+
'did',
|
|
10
|
+
'entityDid',
|
|
11
|
+
'chainAddress',
|
|
12
|
+
'transactionHash',
|
|
13
|
+
'claimId',
|
|
14
|
+
'claimCollectionId',
|
|
15
|
+
'roomId',
|
|
16
|
+
'messageId',
|
|
17
|
+
'eventId',
|
|
18
|
+
'proposalId',
|
|
19
|
+
'bidId',
|
|
20
|
+
'invoiceId',
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
const CORE = new Set<string>(CORE_PORT_TYPES);
|
|
24
|
+
|
|
25
|
+
/** Field-name (leaf) patterns -> semantic port type. Order matters (most specific first). */
|
|
26
|
+
const PATTERNS: Array<[RegExp, string]> = [
|
|
27
|
+
[/transactionhash$/i, 'transactionHash'],
|
|
28
|
+
[/entitydid$/i, 'entityDid'],
|
|
29
|
+
[/collectionid$/i, 'claimCollectionId'],
|
|
30
|
+
[/claimid$/i, 'claimId'],
|
|
31
|
+
[/proposalid$/i, 'proposalId'],
|
|
32
|
+
[/invoiceid$/i, 'invoiceId'],
|
|
33
|
+
[/bidid$/i, 'bidId'],
|
|
34
|
+
[/roomid$/i, 'roomId'],
|
|
35
|
+
[/messageid$/i, 'messageId'],
|
|
36
|
+
[/eventid$/i, 'eventId'],
|
|
37
|
+
[/(^|[a-z])did$/i, 'did'],
|
|
38
|
+
[/address$/i, 'chainAddress'],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** Whether a port type is part of the documented core (i.e. safe to enforce on a mismatch). */
|
|
42
|
+
export function isCorePortType(type: string): boolean {
|
|
43
|
+
return CORE.has(type);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Infer a semantic port type from a field path, falling back to the primitive type. */
|
|
47
|
+
export function inferPortType(path: string, primitive?: string): string {
|
|
48
|
+
const leaf = path.split('.').pop() ?? path;
|
|
49
|
+
for (const [pattern, type] of PATTERNS) {
|
|
50
|
+
if (pattern.test(leaf)) return type;
|
|
51
|
+
}
|
|
52
|
+
return primitive ?? 'string';
|
|
53
|
+
}
|