Skip to content

Commit 02d2d8a

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 430dc36 commit 02d2d8a

26 files changed

Lines changed: 199 additions & 164 deletions

docs/SPEC.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ an implicit `FINAL`.
8484
near-term product is the local/offline/open-source substrate that
8585
makes M365 Copilot and/or local Ollama usable there — reliable
8686
structure-preserving DOM capture → markdown (§5, ADR-0030's decision
87-
matrix), the §2.1 bridge, and a local AI node (pending decision) —
87+
matrix), the §2.1 bridge, and a local AI step (pending decision) —
8888
while the MCP/connector/canvas platform stays the long game, built
8989
and shipped even where blocked. Never rip a capability out because
9090
one environment blocks it (exactly how MCP already works: built,
@@ -183,10 +183,10 @@ an implicit `FINAL`.
183183
explicitly initiate on the user's own behalf via a user-configured
184184
connector. `LOCKED`
185185
**Invariant, owner-confirmed 2026-08-11, sharpening (not weakening)
186-
the above: Mill exposes AI as a user-configured node (local Ollama /
186+
the above: Mill exposes AI as a user-configured step (local Ollama /
187187
BYO key), but never runs an autonomous decide-and-act agent loop
188188
itself — the guardrail always sits between any AI output and a real
189-
action.** An AI node is exactly the user-configured-connector case
189+
action.** An AI step is exactly the user-configured-connector case
190190
this bullet already permits: the user brings the endpoint/key, a
191191
local-Ollama call is zero-egress, and each step is one deterministic
192192
configured call — never Mill deciding what to call next. What stays
@@ -974,8 +974,8 @@ competing for attention across the whole app.
974974
surface, adopted ahead of ADR-0005 B2's own stated deferral trigger ("2+
975975
real multi-step workflows exist to design against") by explicit decision
976976
— see ADR-0005's own Update section, not a silent resolution of §3's
977-
`OPEN` status.** Drag a node type from the palette onto the canvas,
978-
connect nodes by dragging between handles, click a node or edge to
977+
`OPEN` status.** Drag a step type from the palette onto the canvas,
978+
connect steps by dragging between handles, click a step or edge to
979979
configure it in a right-side Inspector — composing and configuring
980980
happen in one motion. `Workflow.Nodes []Node` + `Workflow.Edges []Edge`
981981
(§3.3's schema) replaced the old `Workflow.Steps []Step`; the
@@ -1078,7 +1078,7 @@ competing for attention across the whole app.
10781078
rules and any already-drawn edges stay valid) — modeled on Zapier's
10791079
own in-place trigger-event swap rather than n8n, which has no
10801080
"replace node" feature (a standing, unresolved community request).
1081-
Surfaced as a "Node type" `Select` in the Inspector, shown only when
1081+
Surfaced as a "Step type" `Select` in the Inspector, shown only when
10821082
the selected node's Kind has more than one NodeType to choose from
10831083
(most Kinds today have exactly one — Capture, Decision — where a
10841084
single-option dropdown would be noise, not a control, same reasoning

docs/goals/0053-step-vocabulary.md

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,33 @@ and this goal picks the one users already see in their run results.
3636

3737
## Acceptance (checkable)
3838

39-
- [ ] No user-visible surface (locale strings, palette, editor,
39+
- [x] No user-visible surface (locale strings, palette, editor,
4040
Settings, seeded descriptions) contains "node" as the term for
4141
a workflow step; grep-verified across locales +
42-
manually-eyeballed canvas/palette pass.
43-
- [ ] SPEC.md's surface-describing sections use step; internal/
44-
architecture sections may keep Node where naming code types.
42+
manually-eyeballed canvas/palette pass — delivered 2026-08-14:
43+
grep proof zero rendered "node" (locale values, Go
44+
Label/Description/Output, validation/run error text), the one
45+
legitimate remainder being a Node.js reference; the full
46+
Playwright suite exercises the real palette/canvas copy. Rider
47+
delivered in the same change (goal 0044 dry-run gap 5): all
48+
seeded descriptions dropped internal doc references, and
49+
check-ui-copy.sh now gates seeded descriptions (workflow, list,
50+
HTTP-request seeds) alongside locale JSON.
51+
- [x] SPEC.md's surface-describing sections use step; internal/
52+
architecture sections may keep Node where naming code types —
53+
surface walkthrough sentences and a stale quoted Inspector
54+
label converted; §3.2–§9 architecture narrative retains Node
55+
where interleaved with code identifiers, per this line's own
56+
allowance.
4557
- [ ] Goal 0052's contract items reference this decision and ship
46-
step-vocabulary wire names (checked there, recorded here).
47-
- [ ] Internal identifiers demonstrably unchanged (no mass rename in
48-
the diff).
49-
- [ ] E2e/unit suites green; any test asserting on visible copy
50-
updated in the same change.
58+
step-vocabulary wire names (checked there, recorded here) —
59+
open until 0052 lands; `RefKind: "node-type"` in the
60+
clipboard-apply preview is 0052 wire vocabulary, deliberately
61+
untouched here.
62+
- [x] Internal identifiers demonstrably unchanged (no mass rename in
63+
the diff) — NodeType/composition.Node/test ids/CSS/wire names
64+
all intact; the diff touches strings and docs only.
65+
- [x] E2e/unit suites green; any test asserting on visible copy
66+
updated in the same change — vitest 270/270, full Playwright
67+
207 passed (3 pre-existing timing flakes passed on retry),
68+
validationCopy prefix guard updated to the step prefix.

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",

0 commit comments

Comments
 (0)