Skip to content

Commit 35af87d

Browse files
alicodingclaude
andauthored
feat: user-facing vocabulary says step, not node (goal 0053) (#110)
* feat: user-facing vocabulary says step, not node (goal 0053) -- seed copy drops internal doc refs Sweeps every user-visible surface (locale JSON, NodeType/ConfigField Label/Description/Output, validation and run-failure messages, seeded workflow descriptions) from "node" to "step" per goal 0053, leaving code identifiers (NodeType, composition.Node, RefKind wire values) untouched. Rides the goal 0044 dry-run gap #5 rider: seeded descriptions in builtinworkflows*.go (plus two more found in list/builtin.go and httprequest/builtin.go) no longer cite internal docs/adr/goal references, and check-ui-copy.sh now gates builtinworkflows*.go the same way it already gates locale JSON. Bumps SeedRevision (1 -> 2) on every seeded entity whose Description changed and updates seed_fingerprints.json to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FJ8wStsHyu7XPLTspNjMnQ * docs: goal 0053 acceptance recorded; ui-copy gate covers list/http seed descriptions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FJ8wStsHyu7XPLTspNjMnQ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent ce5b5b8 commit 35af87d

24 files changed

Lines changed: 166 additions & 149 deletions

frontend/src/composition/validationCopy.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,31 @@ function t(key: string, vars: Record<string, unknown> = {}): string {
1818
}
1919

2020
describe('formatIssuesForCopy', () => {
21-
it('names the workflow, its id, counts, and each issue with its node/edge id', () => {
21+
it('names the workflow, its id, counts, and each issue with its step/edge id', () => {
2222
const out = formatIssuesForCopy(t, 'Load sample HTML', 'load-sample-html-workflow', [
2323
issue({ Severity: Severity.SeverityError, NodeID: 'n1', Message: 'a workflow must start with a Trigger step' }),
2424
issue({ Severity: Severity.SeverityWarning, EdgeID: 'e7', Message: 'dangling edge' }),
2525
])
2626
expect(out).toBe(
2727
'Mill workflow "Load sample HTML" (id: load-sample-html-workflow) — validation issues (1 error · 1 warning):\n' +
28-
'- [error] node n1: a workflow must start with a Trigger step\n' +
28+
'- [error] step n1: a workflow must start with a Trigger step\n' +
2929
'- [warning] edge e7: dangling edge',
3030
)
3131
})
3232

33-
it('never double-prefixes when the message already carries its node location', () => {
34-
// ValidateGraph's real messages lead with "node <id>: " -- caught
35-
// from an actual paste that read "node X: node X: ..." (goal 0021).
33+
it('never double-prefixes when the message already carries its step location', () => {
34+
// ValidateGraph's real messages lead with "step <id>: " -- guards
35+
// against reading "step X: step X: ...".
3636
const out = formatIssuesForCopy(t, 'W', 'w-id', [
37-
issue({ NodeID: 'n1', Message: 'node n1: a workflow must start with a Trigger step' }),
37+
issue({ NodeID: 'n1', Message: 'step n1: a workflow must start with a Trigger step' }),
3838
])
39-
expect(out).toContain('- [error]: node n1: a workflow must start with a Trigger step')
40-
expect(out).not.toContain('node n1: node n1:')
39+
expect(out).toContain('- [error]: step n1: a workflow must start with a Trigger step')
40+
expect(out).not.toContain('step n1: step n1:')
4141
})
4242

43-
it('omits the location fragment when an issue has no node or edge id', () => {
44-
const out = formatIssuesForCopy(t, 'W', 'w-id', [issue({ Message: 'graph has no nodes' })])
45-
expect(out).toContain('- [error]: graph has no nodes')
43+
it('omits the location fragment when an issue has no step or edge id', () => {
44+
const out = formatIssuesForCopy(t, 'W', 'w-id', [issue({ Message: 'graph has no steps' })])
45+
expect(out).toContain('- [error]: graph has no steps')
4646
})
4747

4848
it('pluralizes counts', () => {

frontend/src/composition/validationCopy.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ export function formatIssuesForCopy(t: (key: string, opts?: Record<string, unkno
1818

1919
const lines = issues.map((i) => {
2020
// ValidateGraph's own Message usually already leads with
21-
// "node <id>: "/"edge <id>: " -- adding our own location fragment
22-
// then double-prefixed every line (caught from a real paste, goal
23-
// 0021). Only add a location when the message doesn't carry one.
24-
const alreadyLocated = i.Message.startsWith('node ') || i.Message.startsWith('edge ')
21+
// "step <id>: "/"edge <id>: " -- adding our own location fragment
22+
// then double-prefixed every line. Only add a location when the
23+
// message doesn't carry one.
24+
const alreadyLocated = i.Message.startsWith('step ') || i.Message.startsWith('edge ')
2525
const where = alreadyLocated ? '' : i.NodeID ? t('validationCopy.nodeLocation', { id: i.NodeID }) : i.EdgeID ? t('validationCopy.edgeLocation', { id: i.EdgeID }) : ''
2626
return `- [${i.Severity}]${where}: ${i.Message}`
2727
})

frontend/src/locales/en/app.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@
5555
"unrecognizedTitle": "Couldn't read that as a Mill export",
5656
"back": "Back",
5757
"summaryUpdate": "This will UPDATE \"{{label}}\" — replacing the current draft (the published version stays untouched until you publish).",
58-
"summaryCreate": "This will CREATE \"{{label}}\" ({{nodeCount}} node{{plural}}).",
58+
"summaryCreate": "This will CREATE \"{{label}}\" ({{nodeCount}} step{{plural}}).",
5959
"unresolvedTitle": "{{count}} reference{{plural}} won't resolve here",
60-
"unresolvedItemPrefix": "node",
60+
"unresolvedItemPrefix": "step",
6161
"unresolvedItemSuffix": "references \"{{value}}\", which doesn't exist here — point it at one before running",
6262
"applyFailedTitle": "Apply failed",
6363
"cancel": "Cancel",

frontend/src/locales/en/composition.json

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"runButtonTooltip": "Runs the saved draft (test run).",
2828
"run": "Run",
2929
"runInStepModeAriaLabel": "Run in step mode",
30-
"runInStepModeTooltip": "Pauses before every node so you can inspect and edit its data."
30+
"runInStepModeTooltip": "Pauses before every step so you can inspect and edit its data."
3131
},
3232
"canvasNodeView": {
3333
"runStatus": {
@@ -159,18 +159,18 @@
159159
"plainSuffix": ".",
160160
"breakpointSet": "Breakpoint set",
161161
"noBreakpoint": "No breakpoint",
162-
"breakpointHint": "{{state}} — click the dot on the node card to {{action}}.",
162+
"breakpointHint": "{{state}} — click the dot on the step card to {{action}}.",
163163
"removeIt": "remove it",
164164
"addOne": "add one",
165165
"breakpointBadge": "Breakpoint",
166166
"breakpointDescription": "A run pauses here to let you inspect and edit its data before it continues -- a debugging aid, not policy."
167167
},
168168
"nodeInspector": {
169-
"nodeType": "Node type",
169+
"nodeType": "Step type",
170170
"saveBeforeHotkey": "Save this workflow before assigning a hotkey.",
171171
"setShortcut": "Set shortcut",
172172
"openAccessibilitySettings": "Open Accessibility Settings",
173-
"noConfiguration": "This node type takes no configuration.",
173+
"noConfiguration": "This step type takes no configuration.",
174174
"generateTestPayload": "Generate test payload",
175175
"hoverToPreviewChild": "Hover to preview the child — click Open to edit it",
176176
"defaultOption": "(default)"
@@ -181,11 +181,11 @@
181181
},
182182
"draftWorkflowSchema": {
183183
"needsLabel": "A workflow needs a label",
184-
"needsAtLeastOneNode": "A workflow needs at least one node",
185-
"danglingConnection": "A connection references a node that no longer exists.",
186-
"terminalCannotHaveOutgoing": "A Decision node (a terminal outcome) cannot have an outgoing connection.",
187-
"onlyBranchCanFanOut": "Only a Branch node can have more than one outgoing connection.",
188-
"needsExactlyOneStart": "A workflow must have exactly one starting node.",
184+
"needsAtLeastOneNode": "A workflow needs at least one step",
185+
"danglingConnection": "A connection references a step that no longer exists.",
186+
"terminalCannotHaveOutgoing": "A Decision step (a terminal outcome) cannot have an outgoing connection.",
187+
"onlyBranchCanFanOut": "Only a Branch step can have more than one outgoing connection.",
188+
"needsExactlyOneStart": "A workflow must have exactly one starting step.",
189189
"notValidYetFallback": "This workflow is not valid yet."
190190
},
191191
"validationPanel": {
@@ -197,7 +197,7 @@
197197
},
198198
"validationCopy": {
199199
"header": "Mill workflow \"{{label}}\" (id: {{id}}) — validation issues ({{counts}}):",
200-
"nodeLocation": " node {{id}}",
200+
"nodeLocation": " step {{id}}",
201201
"edgeLocation": " edge {{id}}"
202202
},
203203
"workflowEditorTab": {
@@ -214,7 +214,7 @@
214214
},
215215
"nodePalette": {
216216
"addSteps": "Add steps",
217-
"onlyOneTriggerTitle": "A workflow can only have one trigger. Select the existing trigger node on the canvas to change its type instead.",
217+
"onlyOneTriggerTitle": "A workflow can only have one trigger. Select the existing trigger step on the canvas to change its type instead.",
218218
"searchPlaceholder": "Search steps…",
219219
"searchAriaLabel": "Search steps",
220220
"noMatches": "No steps match \"{{query}}\""
@@ -245,11 +245,11 @@
245245
"redoAriaLabel": "Redo",
246246
"autoLayoutAriaLabel": "Auto-layout",
247247
"deleteSelectedAriaLabel": "Delete selected",
248-
"addStepsHint": "Add steps to drag a node type onto the canvas, connect them, click a node to configure it."
248+
"addStepsHint": "Add steps to drag a step type onto the canvas, connect them, click a step to configure it."
249249
},
250250
"compositionCanvas": {
251-
"onlyDecisionEdgesConfigurable": "Only a Decision node’s outgoing edges are configurable.",
252-
"selectNodeToConfigure": "Select a node to configure it."
251+
"onlyDecisionEdgesConfigurable": "Only a Decision step’s outgoing edges are configurable.",
252+
"selectNodeToConfigure": "Select a step to configure it."
253253
},
254254
"compositionView": {
255255
"heading": "Workflows",

frontend/src/locales/en/configure.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
"deleteAriaLabel": "Delete {{label}}",
106106
"searchPlaceholder": "Search lists…",
107107
"emptyHeading": "No lists yet",
108-
"emptyDescription": "A reusable typed dataset a workflow's List Search (or List Lookup) node can resolve against.",
108+
"emptyDescription": "A reusable typed dataset a workflow's List Search (or List Lookup) step can resolve against.",
109109
"columnsRowsSummary": "{{columns}} columns, {{rows}} rows",
110110
"deleteConfirmTitle": "Delete list?",
111111
"deleteConfirmBody": "This permanently deletes \"{{label}}\". This cannot be undone.",
@@ -158,21 +158,21 @@
158158
"deleteAriaLabel": "Delete {{label}}",
159159
"searchPlaceholder": "Search execution environments…",
160160
"emptyHeading": "No execution environments yet",
161-
"emptyDescription": "A reusable, pinned shell/directory/env a code-execution workflow node can run inside.",
161+
"emptyDescription": "A reusable, pinned shell/directory/env a code-execution workflow step can run inside.",
162162
"freshTempDirPerRun": "fresh temp dir per run",
163163
"deleteConfirmTitle": "Delete execution environment?",
164164
"deleteConfirmBody": "This permanently deletes \"{{label}}\". This cannot be undone."
165165
},
166166
"configureDecisions": {
167167
"heading": "Decisions",
168168
"newDecision": "New decision",
169-
"pageDescription": "A Decision is a reusable, typed terminal outcome that a workflow's Decision node reaches to end the run with a real category and typed result, instead of just running out of steps.",
169+
"pageDescription": "A Decision is a reusable, typed terminal outcome that a workflow's Decision step reaches to end the run with a real category and typed result, instead of just running out of steps.",
170170
"label": "Label",
171171
"category": "Category",
172172
"categoryCaptionEditing": "Cannot be changed after creation -- duplicate this Decision to create one with a different category.",
173173
"categoryCaptionCreating": "Cannot be changed after creation -- duplicate this Decision later to create one with a different category.",
174174
"outputs": "Outputs",
175-
"outputsDescription": "This Decision's typed result fields, bound by a workflow's decision-outcome node when it reaches this outcome.",
175+
"outputsDescription": "This Decision's typed result fields, bound by a workflow's decision-outcome step when it reaches this outcome.",
176176
"keyPlaceholder": "key",
177177
"labelPlaceholder": "label",
178178
"enumValuesPlaceholder": "enum values, comma separated",
@@ -194,7 +194,7 @@
194194
"deleteAriaLabel": "Delete {{label}}",
195195
"searchPlaceholder": "Search decisions…",
196196
"emptyHeading": "No decisions yet",
197-
"emptyDescription": "A reusable, typed TERMINAL outcome a workflow's Decision node reaches to end the run.",
197+
"emptyDescription": "A reusable, typed TERMINAL outcome a workflow's Decision step reaches to end the run.",
198198
"outputsSummary": "Outputs: {{keys}}",
199199
"none": "none",
200200
"deleteConfirmTitle": "Delete decision?",
@@ -223,7 +223,7 @@
223223
"deleteAriaLabel": "Delete {{label}}",
224224
"searchPlaceholder": "Search MCP servers…",
225225
"emptyHeading": "No MCP servers yet",
226-
"emptyDescription": "A reusable stdio connection an mcp-tool-call workflow node can resolve by ID.",
226+
"emptyDescription": "A reusable stdio connection an mcp-tool-call workflow step can resolve by ID.",
227227
"serverTools": "{{label}} — tools",
228228
"noToolsExposed": "This server exposes no tools.",
229229
"deleteConfirmTitle": "Delete MCP server?",
@@ -318,7 +318,7 @@
318318
"requestBody": "Request body",
319319
"addBodyField": "Add body field",
320320
"outputHeading": "Output — response schema",
321-
"outputDescription": "The attributes this request's response provides, for a workflow node to read and bind into its own Attributes.",
321+
"outputDescription": "The attributes this request's response provides, for a workflow step to read and bind into its own Attributes.",
322322
"addOutputField": "Add output field",
323323
"fieldNameAriaLabel": "Field name",
324324
"namePlaceholder": "name",
@@ -434,7 +434,7 @@
434434
"removeHeaderAriaLabel": "Remove header",
435435
"addHeader": "Add header",
436436
"schema": "Schema",
437-
"schemaDescription": "The payload's structure only: typed input/output fields a workflow node can bind Attributes to. Method and URL live above and are never part of the schema. Leave empty to keep using a literal request body.",
437+
"schemaDescription": "The payload's structure only: typed input/output fields a workflow step can bind Attributes to. Method and URL live above and are never part of the schema. Leave empty to keep using a literal request body.",
438438
"multiOperationNote": "This request's stored schema declares {{count}} operations, but a request is one call. Remove the extras below, or duplicate the request once per operation.",
439439
"hideRawOpenapi": "Hide raw OpenAPI",
440440
"viewRawOpenapi": "View raw OpenAPI",

internal/domain/composition/aiclassify.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func init() {
5353
Effect: guardrail.ClassExternal, // dynamic downgrade for a loopback provider -- aiprovider.go's aiNodeEffectOverride
5454
Output: "unchanged payload -- the chosen category is written into the named Attribute below",
5555
Label: "AI: Classify",
56-
Description: "Sends the running payload (plus an optional instruction) to a Configure-authored AI provider and asks it to pick exactly one of this step's own declared categories, writing the choice into a named Attribute -- a dedicated classification node (Dify's own precedent researched in docs/goals/0031-ai-node-family.md), not composed from extract-structured, since \"pick one of these labels\" is common enough across real workflows to deserve its own shape. Pairs with Branch: route on the written Attribute to act on the classification. The category list is this workflow's own business decision (node-local, not Configure-authored) -- two workflows classifying into different category sets is normal, not drift.",
56+
Description: "Sends the running payload (plus an optional instruction) to a Configure-authored AI provider and asks it to pick exactly one of this step's own declared categories, writing the choice into a named Attribute -- a dedicated classification step (Dify's own precedent researched in docs/goals/0031-ai-node-family.md), not composed from extract-structured, since \"pick one of these labels\" is common enough across real workflows to deserve its own shape. Pairs with Branch: route on the written Attribute to act on the classification. The category list is this workflow's own business decision (step-local, not Configure-authored) -- two workflows classifying into different category sets is normal, not drift.",
5757
ConfigFields: []ConfigField{
5858
{
5959
Key: aiProviderIDConfigKey, Label: "AI provider",

internal/domain/composition/aicompletion.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func init() {
3333
Effect: guardrail.ClassExternal, // dynamic downgrade for a loopback provider -- aiprovider.go's aiNodeEffectOverride, dispatched via EffectForNode
3434
Output: "the AI completion text, replacing the payload",
3535
Label: "AI: Completion",
36-
Description: "Sends a prompt (plus the running payload) to a Configure-authored AI provider -- local Ollama, or a BYO OpenAI-compatible/Anthropic endpoint -- and replaces the payload with its text completion. Composition (docs/goals/0031-ai-node-family.md): the system prompt is this node's own System prompt field; the user message is Prompt followed by the current payload (blank-line separated) when the payload is non-empty, else Prompt alone. One deterministic call per run -- never a loop or autonomous agent behavior (docs/SPEC.md §1.1's locked invariant). A local (localhost/127.0.0.1/::1) provider runs without an approval ask; any other endpoint asks by default, the same posture integration-http already has for outbound calls.",
36+
Description: "Sends a prompt (plus the running payload) to a Configure-authored AI provider -- local Ollama, or a BYO OpenAI-compatible/Anthropic endpoint -- and replaces the payload with its text completion. Composition (docs/goals/0031-ai-node-family.md): the system prompt is this step's own System prompt field; the user message is Prompt followed by the current payload (blank-line separated) when the payload is non-empty, else Prompt alone. One deterministic call per run -- never a loop or autonomous agent behavior (docs/SPEC.md §1.1's locked invariant). A local (localhost/127.0.0.1/::1) provider runs without an approval ask; any other endpoint asks by default, the same posture integration-http already has for outbound calls.",
3737
ConfigFields: []ConfigField{
3838
{
3939
Key: aiProviderIDConfigKey, Label: "AI provider",

internal/domain/composition/aiextractstructured.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func init() {
121121
Effect: guardrail.ClassExternal, // dynamic downgrade for a loopback provider -- aiprovider.go's aiNodeEffectOverride
122122
Output: "unchanged payload -- the extracted typed result is written into the named Attributes below",
123123
Label: "AI: Extract structured data",
124-
Description: "Sends a prompt (plus the running payload) to a Configure-authored AI provider, requests a JSON-schema-constrained structured response, and writes each declared output field into this workflow's Attributes by the same key -- the node that composes with Branch for real decisioning (docs/goals/0031-ai-node-family.md). Composition matches process-ai-completion: user content = Prompt + payload. Every declared field is required in the requested schema; a field the provider's response omits still appears in Attributes, zero-valued for its type.",
124+
Description: "Sends a prompt (plus the running payload) to a Configure-authored AI provider, requests a JSON-schema-constrained structured response, and writes each declared output field into this workflow's Attributes by the same key -- the step that composes with Branch for real decisioning (docs/goals/0031-ai-node-family.md). Composition matches process-ai-completion: user content = Prompt + payload. Every declared field is required in the requested schema; a field the provider's response omits still appears in Attributes, zero-valued for its type.",
125125
ConfigFields: []ConfigField{
126126
{
127127
Key: aiProviderIDConfigKey, Label: "AI provider",

0 commit comments

Comments
 (0)