fsanyoto commited on
Commit
fc71889
·
verified ·
1 Parent(s): 7c1820c

Prune files not in the deploy set (stale build context)

Browse files
web/src/ai-agent/AiAgentPage.tsx DELETED
@@ -1,237 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // ai-agent/AiAgentPage.tsx — WAVE 33 · W33-T54/T55 (owner item 7, ruling R3).
3
- //
4
- // Owner: *"collapse the web browser into an AI agent module that builds the flow
5
- // for you"*, and *"lay out the foundation with custom tools we provide"*. The
6
- // tools are the action catalog. This page is the door to them: one box, one
7
- // draft, one Accept.
8
- //
9
- // ⛔ THE DRAFT IS SHOWN BEFORE ANYTHING IS SAVED, and that is T54's `done-when`
10
- // rather than a nicety. The one failure class no validator can catch is a
11
- // well-formed flow that does something OTHER than what was asked for, and the
12
- // only thing that catches it is a person reading the steps. So `draftFlow`
13
- // writes nothing, the steps are painted in full, and `Accept` is a separate
14
- // click through the ORDINARY create door.
15
- //
16
- // ⛔ AND WHAT `clean_actions` CHANGED IS SHOWN TOO. It drops a config key it does
17
- // not recognise and answers 200 (D-75), so a draft accepted without that
18
- // disclosure would open as a flow the person was never shown. `dropped` is
19
- // painted as plainly as the steps are.
20
- //
21
- // ⚠ C4: no `index.css` rule is added or changed here. This module ships its own
22
- // co-located sheet, which is the established practice (`query.css`,
23
- // `SwipeView.css`, `navExtras.css`) and the contract for every lane but B.
24
- // ---------------------------------------------------------------------------
25
-
26
- import { useState } from "react";
27
-
28
- import { acceptDraft, draftFlow, type Draft, type DraftResult } from "./aiAgentApi";
29
- import "./aiAgent.css";
30
-
31
- /** The examples under the box. Deliberately three, deliberately short, deliberately concrete. */
32
- const EXAMPLES = [
33
- "Go to each supplier's website and read the price into the Price column",
34
- "Every Monday, find the new rows in Leads and enrich the Instagram profiles",
35
- "Open each product page, click Accept cookies, then read the stock count",
36
- ];
37
-
38
- export default function AiAgentPage() {
39
- const [prompt, setPrompt] = useState("");
40
- const [busy, setBusy] = useState(false);
41
- const [problem, setProblem] = useState("");
42
- const [result, setResult] = useState<DraftResult | null>(null);
43
- const [saved, setSaved] = useState("");
44
-
45
- const ask = async (text?: string) => {
46
- const q = (text ?? prompt).trim();
47
- if (!q || busy) return;
48
- setBusy(true);
49
- setProblem("");
50
- setResult(null);
51
- setSaved("");
52
- const r = await draftFlow(q);
53
- setBusy(false);
54
- if (!r.ok) {
55
- // ⚠ THE SERVER'S OWN SENTENCE, PRINTED. A refusal here is usually the model saying "this
56
- // needs a step this tool does not have", which is the most useful thing on the screen —
57
- // rewriting it into "something went wrong" would throw away the only answer.
58
- setProblem(r.message);
59
- return;
60
- }
61
- setResult(r.value);
62
- };
63
-
64
- const accept = async (draft: Draft) => {
65
- if (busy) return;
66
- setBusy(true);
67
- setProblem("");
68
- const r = await acceptDraft(draft);
69
- setBusy(false);
70
- if (!r.ok) {
71
- setProblem(r.message);
72
- return;
73
- }
74
- setSaved(r.value.id);
75
- };
76
-
77
- return (
78
- <div className="aiagent">
79
- <h2 className="aiagent-h">Describe what you want, and I will build the steps</h2>
80
- <p className="aiagent-sub">
81
- I can only use the steps this tool already has. If what you ask for needs something that
82
- is not built, I will say so rather than guess.
83
- </p>
84
-
85
- <textarea
86
- id="aiagent-prompt"
87
- className="aiagent-box"
88
- rows={3}
89
- placeholder="Go to this website, do this…"
90
- value={prompt}
91
- disabled={busy}
92
- onChange={(e) => setPrompt(e.target.value)}
93
- />
94
- <div className="aiagent-actions">
95
- <button
96
- type="button"
97
- className="aiagent-go"
98
- disabled={busy || !prompt.trim()}
99
- onClick={() => void ask()}
100
- >
101
- {busy ? "Thinking…" : "Draft the steps"}
102
- </button>
103
- {/* ⚠ SAYS WHAT IT WILL NOT DO. "Draft" is a promise about writing, and a person about to
104
- hand a description to an AI is entitled to know the answer is not going live. */}
105
- <span className="aiagent-note">Nothing is saved until you accept it.</span>
106
- </div>
107
-
108
- {!result && !problem ? (
109
- <div className="aiagent-eg">
110
- <p className="aiagent-eg-h">For example</p>
111
- {EXAMPLES.map((e) => (
112
- <button
113
- key={e}
114
- type="button"
115
- className="aiagent-eg-row"
116
- disabled={busy}
117
- onClick={() => {
118
- setPrompt(e);
119
- void ask(e);
120
- }}
121
- >
122
- {e}
123
- </button>
124
- ))}
125
- </div>
126
- ) : null}
127
-
128
- {problem ? <p className="aiagent-problem">{problem}</p> : null}
129
-
130
- {result ? (
131
- <div className="aiagent-draft">
132
- <h3 className="aiagent-draft-h">{result.draft.name}</h3>
133
- <p className="aiagent-draft-sub">
134
- Starts on: {result.draft.trigger || "manual"}
135
- {result.draft.table ? ` · walks ${result.draft.table}` : ""}
136
- {result.provider ? ` · drafted by ${result.provider}` : ""}
137
- </p>
138
-
139
- <ol className="aiagent-steps">
140
- {result.draft.actions.map((a, i) => (
141
- <li key={a.id} className="aiagent-step">
142
- <span className="aiagent-step-n">{i + 1}</span>
143
- <div className="aiagent-step-body">
144
- <p className="aiagent-step-kind">{a.kind}</p>
145
- {a.why ? <p className="aiagent-step-why">{a.why}</p> : null}
146
- {/* ⛔ THE CONFIG IS PAINTED, NOT SUMMARISED. "Reads a page" tells a person
147
- nothing they can check; the URL and the selector are the whole of what they
148
- are being asked to approve. */}
149
- {Object.entries(a.config)
150
- .filter(([, v]) => v !== "" && v !== false && v !== null && v !== undefined)
151
- .map(([k, v]) => (
152
- <p key={k} className="aiagent-step-cfg">
153
- <span className="aiagent-step-key">{k}</span> {String(v)}
154
- </p>
155
- ))}
156
- {/*
157
- ⛔⛔ THE BLANKS, SHOWN. Skipping empty values is right on its own, and the
158
- system prompt telling the model to leave a setting blank rather than invent it
159
- is right on its own — together they made an UNFINISHED step paint as a finished
160
- one: a `web_read` with no selector and no target column rendered as url + attr
161
- + timeout and nothing else, and the create door then accepted it. A person
162
- approved a step that reads nothing into nowhere, having been shown no blank.
163
- Found by the verifier on this ticket, not by a gate.
164
- ⚠ The words are the SERVER's (`action_needs`), so this line, the builder's
165
- Configured label and the run refusal all say the same thing.
166
- */}
167
- {a.needs.length ? (
168
- <p className="aiagent-step-needs">
169
- Still needs {a.needs.join(", ")} — you can fill it in after you accept.
170
- </p>
171
- ) : null}
172
- {a.unknownField ? (
173
- <p className="aiagent-step-needs">
174
- There is no column called {a.unknownField} on that database, so this step
175
- would write nowhere. Pick the column after you accept.
176
- </p>
177
- ) : null}
178
- </div>
179
- </li>
180
- ))}
181
- </ol>
182
-
183
- {/* ⛔ WHOLE-DRAFT DISCLOSURES — a step list the door truncated, a trigger this
184
- deployment does not have. Standing rule (W30/R6): a limit that cannot be removed
185
- must be REPORTED with its cause and a recommended fix, and a silent truncation IS
186
- the violation. The draft door caps at 12 steps while the engine stores 20, so a
187
- longer answer lost steps that would have been storable. */}
188
- {result.notes.length ? (
189
- <div className="aiagent-dropped">
190
- {result.notes.map((n) => (
191
- <p key={n} className="aiagent-dropped-h">{n}</p>
192
- ))}
193
- </div>
194
- ) : null}
195
-
196
- {result.dropped.length ? (
197
- <div className="aiagent-dropped">
198
- <p className="aiagent-dropped-h">
199
- Some settings were changed to fit what this tool accepts:
200
- </p>
201
- {result.dropped.map((d) => (
202
- <p key={d.kind + d.keys.join()} className="aiagent-dropped-row">
203
- {d.kind}: {d.keys.join(", ")}
204
- </p>
205
- ))}
206
- </div>
207
- ) : null}
208
-
209
- {saved ? (
210
- <p className="aiagent-saved">
211
- Saved. It is an ordinary automation now — open it from Automation to edit any step.
212
- </p>
213
- ) : (
214
- <div className="aiagent-actions">
215
- <button
216
- type="button"
217
- className="aiagent-go"
218
- disabled={busy}
219
- onClick={() => void accept(result.draft)}
220
- >
221
- Accept and create it
222
- </button>
223
- <button
224
- type="button"
225
- className="aiagent-again"
226
- disabled={busy}
227
- onClick={() => void ask()}
228
- >
229
- Try again
230
- </button>
231
- </div>
232
- )}
233
- </div>
234
- ) : null}
235
- </div>
236
- );
237
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/ai-agent/aiAgent.css DELETED
@@ -1,279 +0,0 @@
1
- /* ---------------------------------------------------------------------------
2
- ai-agent/aiAgent.css — the AI agent module's own stylesheet
3
- (WAVE 33, owner item 7, ruling R3).
4
-
5
- Shipped BESIDE its component, following `query/query.css` and the
6
- `FormInterface.css` / `SwipeView.css` precedent, rather than added to
7
- `index.css` — which is B's file this wave (contract C4). Vite carries the
8
- import; the node shim in `verify_ui` already stubs a CSS require for exactly
9
- this shape.
10
-
11
- Deliberately shaped like the Query page it sits beside: same page padding,
12
- same card surfaces, same title scale. The two are the same kind of thing —
13
- describe something in words, read what came back before it is committed — and
14
- a person should not have to learn two layouts to do it.
15
-
16
- ⛔ EVERY FONT SIZE IS A `--lp-fs-*` TOKEN AND EVERY COLOUR A `--lp-*` TOKEN.
17
- Wave-21 R5 is gated over every stylesheet under src, not just index.css, and
18
- one literal here reds `web_ui` for the whole tree.
19
-
20
- ⛔ NO EMOJIS AND NO ALL-CAPS CHROME (DESIGN.md R6).
21
-
22
- ⛔⛔ AND NOTHING IN THIS COMMENT MAY CONTAIN THE TWO CHARACTERS THAT CLOSE IT.
23
- A path glob written out in a sibling stylesheet's header spelled the
24
- terminator, the comment ended mid-sentence, and `lightningcss` then died on
25
- the first backtick after it. `tsc` passed and vite transformed every module;
26
- the failure surfaced in the MINIFIER, nowhere near its cause.
27
- --------------------------------------------------------------------------- */
28
-
29
- .aiagent {
30
- height: 100%;
31
- overflow-y: auto;
32
- padding: 34px 44px 56px;
33
- box-sizing: border-box;
34
- background: var(--lp-wash);
35
- }
36
-
37
- .aiagent-h {
38
- margin: 0 0 6px;
39
- font-size: var(--lp-fs-lg);
40
- font-weight: 650;
41
- color: var(--lp-ink);
42
- }
43
-
44
- .aiagent-sub {
45
- margin: 0 0 18px;
46
- max-width: 62ch;
47
- font-size: var(--lp-fs-sm);
48
- line-height: var(--lp-lh);
49
- color: var(--lp-muted);
50
- }
51
-
52
- .aiagent-box {
53
- display: block;
54
- width: 100%;
55
- max-width: 74ch;
56
- padding: 12px 14px;
57
- box-sizing: border-box;
58
- font: inherit;
59
- font-size: var(--lp-fs-sm);
60
- line-height: var(--lp-lh);
61
- color: var(--lp-ink);
62
- background: var(--lp-surface);
63
- border: 1px solid var(--lp-line);
64
- border-radius: var(--lp-r-md);
65
- resize: vertical;
66
- }
67
-
68
- .aiagent-box:focus {
69
- outline: none;
70
- border-color: var(--lp-blue-solid);
71
- }
72
-
73
- .aiagent-actions {
74
- display: flex;
75
- align-items: center;
76
- gap: 12px;
77
- margin: 12px 0 0;
78
- flex-wrap: wrap;
79
- }
80
-
81
- .aiagent-go {
82
- padding: 8px 16px;
83
- font: inherit;
84
- font-size: var(--lp-fs-sm);
85
- font-weight: 600;
86
- color: var(--lp-surface);
87
- background: var(--lp-blue-solid);
88
- border: 1px solid var(--lp-blue-solid);
89
- border-radius: var(--lp-r-md);
90
- cursor: pointer;
91
- }
92
-
93
- .aiagent-go:disabled {
94
- opacity: 0.5;
95
- cursor: default;
96
- }
97
-
98
- .aiagent-again {
99
- padding: 8px 16px;
100
- font: inherit;
101
- font-size: var(--lp-fs-sm);
102
- color: var(--lp-ink);
103
- background: var(--lp-surface);
104
- border: 1px solid var(--lp-line);
105
- border-radius: var(--lp-r-md);
106
- cursor: pointer;
107
- }
108
-
109
- .aiagent-again:disabled {
110
- opacity: 0.5;
111
- cursor: default;
112
- }
113
-
114
- .aiagent-note {
115
- font-size: var(--lp-fs-2xs);
116
- color: var(--lp-muted);
117
- }
118
-
119
- .aiagent-eg {
120
- margin: 26px 0 0;
121
- max-width: 74ch;
122
- }
123
-
124
- .aiagent-eg-h {
125
- margin: 0 0 8px;
126
- font-size: var(--lp-fs-2xs);
127
- color: var(--lp-muted);
128
- }
129
-
130
- .aiagent-eg-row {
131
- display: block;
132
- width: 100%;
133
- margin: 0 0 8px;
134
- padding: 10px 14px;
135
- text-align: left;
136
- font: inherit;
137
- font-size: var(--lp-fs-sm);
138
- color: var(--lp-ink);
139
- background: var(--lp-surface);
140
- border: 1px solid var(--lp-line);
141
- border-radius: var(--lp-r-md);
142
- cursor: pointer;
143
- }
144
-
145
- .aiagent-eg-row:disabled {
146
- opacity: 0.5;
147
- cursor: default;
148
- }
149
-
150
- .aiagent-problem {
151
- margin: 18px 0 0;
152
- max-width: 74ch;
153
- padding: 10px 14px;
154
- font-size: var(--lp-fs-sm);
155
- line-height: var(--lp-lh);
156
- color: var(--lp-red-deep);
157
- background: var(--lp-surface);
158
- border: 1px solid var(--lp-red-deep);
159
- border-radius: var(--lp-r-md);
160
- }
161
-
162
- .aiagent-draft {
163
- margin: 26px 0 0;
164
- max-width: 74ch;
165
- padding: 18px 20px;
166
- background: var(--lp-surface);
167
- border: 1px solid var(--lp-line);
168
- border-radius: var(--lp-r-md);
169
- }
170
-
171
- .aiagent-draft-h {
172
- margin: 0 0 4px;
173
- font-size: var(--lp-fs-md);
174
- font-weight: 650;
175
- color: var(--lp-ink);
176
- }
177
-
178
- .aiagent-draft-sub {
179
- margin: 0 0 16px;
180
- font-size: var(--lp-fs-2xs);
181
- color: var(--lp-muted);
182
- }
183
-
184
- .aiagent-steps {
185
- margin: 0;
186
- padding: 0;
187
- list-style: none;
188
- }
189
-
190
- .aiagent-step {
191
- display: flex;
192
- gap: 12px;
193
- padding: 12px 0;
194
- border-top: 1px solid var(--lp-line);
195
- }
196
-
197
- .aiagent-step-n {
198
- flex: none;
199
- width: 22px;
200
- height: 22px;
201
- display: inline-flex;
202
- align-items: center;
203
- justify-content: center;
204
- font-size: var(--lp-fs-2xs);
205
- font-weight: 600;
206
- color: var(--lp-muted);
207
- background: var(--lp-surface-2);
208
- border-radius: 999px;
209
- }
210
-
211
- .aiagent-step-body {
212
- min-width: 0;
213
- }
214
-
215
- .aiagent-step-kind {
216
- margin: 0;
217
- font-size: var(--lp-fs-sm);
218
- font-weight: 600;
219
- color: var(--lp-ink);
220
- }
221
-
222
- .aiagent-step-why {
223
- margin: 2px 0 6px;
224
- font-size: var(--lp-fs-2xs);
225
- line-height: var(--lp-lh);
226
- color: var(--lp-muted);
227
- }
228
-
229
- .aiagent-step-cfg {
230
- margin: 0;
231
- font-size: var(--lp-fs-2xs);
232
- line-height: var(--lp-lh);
233
- color: var(--lp-ink);
234
- overflow-wrap: anywhere;
235
- }
236
-
237
- .aiagent-step-key {
238
- display: inline-block;
239
- min-width: 84px;
240
- color: var(--lp-muted);
241
- }
242
-
243
- .aiagent-dropped {
244
- margin: 14px 0 0;
245
- padding: 10px 14px;
246
- background: var(--lp-surface-2);
247
- border-left: 3px solid var(--lp-yellow-deep);
248
- border-radius: var(--lp-r-md);
249
- }
250
-
251
- .aiagent-dropped-h {
252
- margin: 0 0 4px;
253
- font-size: var(--lp-fs-2xs);
254
- font-weight: 600;
255
- color: var(--lp-ink);
256
- }
257
-
258
- .aiagent-dropped-row {
259
- margin: 0;
260
- font-size: var(--lp-fs-2xs);
261
- color: var(--lp-muted);
262
- }
263
-
264
- .aiagent-saved {
265
- margin: 16px 0 0;
266
- font-size: var(--lp-fs-sm);
267
- line-height: var(--lp-lh);
268
- color: var(--lp-ink);
269
- }
270
-
271
- /* The per-step disclosures: a required setting the assistant left blank (it was told to leave one
272
- blank rather than invent it), and a column the target database does not have. Toned like a
273
- warning rather than an error, because neither stops the draft being accepted and fixed. */
274
- .aiagent-step-needs {
275
- margin: 4px 0 0;
276
- font-size: var(--lp-fs-2xs);
277
- line-height: var(--lp-lh);
278
- color: var(--lp-yellow-deep);
279
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/ai-agent/aiAgentApi.ts DELETED
@@ -1,197 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // ai-agent/aiAgentApi.ts — WAVE 33 · W33-T54/T55 (owner item 7, ruling R3):
3
- // two calls, and nothing else.
4
- //
5
- // Shaped like `query/queryApi.ts` — the same `Result<T>`, the same 4xx/5xx
6
- // message split — because the failure that matters is identical: a wrong
7
- // `credentials` word produces no client-side symptom at all, just a 401 from a
8
- // server that never saw a session.
9
- //
10
- // ⛔ DRAFTING AND SAVING ARE TWO DIFFERENT DOORS, AND THAT IS RULING R3, NOT A
11
- // convenience. `draftFlow` writes NOTHING; accepting a draft calls the ORDINARY
12
- // `POST /automations` that the New-automation button has always called. R3 asks
13
- // for a flow "indistinguishable from a hand-built one", and the way to get that
14
- // is for there to be no second write path — not for a second one to be written
15
- // carefully enough to look the same.
16
- //
17
- // ⚠ NO CLIENT UNION OVER `kind`. The action kinds are the server's vocabulary
18
- // and arrive as strings (the wave-9 law): a client
19
- // `type Kind = "web_read" | …` turns "the server added a kind" into "the client
20
- // drops the step".
21
- // ---------------------------------------------------------------------------
22
-
23
- import { API_V1, CREDENTIALS } from "../apiContract";
24
-
25
- export type Result<T> =
26
- | { ok: true; value: T }
27
- | { ok: false; status: number; message: string };
28
-
29
- /** One step of a drafted flow — the CLEANED config, plus the model's own reason for it. */
30
- export interface DraftAction {
31
- id: string;
32
- /** A STRING, never a union — see the header. */
33
- kind: string;
34
- config: Record<string, unknown>;
35
- /** One sentence: why the assistant put this step here. May be "". */
36
- why: string;
37
- /**
38
- * ⛔ WHAT THIS STEP IS STILL MISSING, in the words the runner uses — straight off the server's
39
- * `action_needs`, the same predicate the builder's Configured label and the run refusal read.
40
- * The system prompt tells the model to leave a setting BLANK rather than invent it, and this
41
- * page skips empty values; together those two right decisions made an unfinished step paint as
42
- * a finished one, and the create door then accepted it. Empty is the ordinary case.
43
- */
44
- needs: string[];
45
- /**
46
- * ⛔ A COLUMN THE TARGET DATABASE DOES NOT HAVE, when the assistant named one. Usually the
47
- * column's LABEL where its key was wanted — which is what a person would say, so it is what an
48
- * obedient model echoes, and nothing validated it at draft, accept or store.
49
- */
50
- unknownField?: string;
51
- }
52
-
53
- export interface Draft {
54
- name: string;
55
- trigger: string;
56
- table: string;
57
- actions: DraftAction[];
58
- }
59
-
60
- export interface DraftResult {
61
- draft: Draft;
62
- /** Which rung answered — a wrong draft has to be diagnosable after the fact. */
63
- provider: string;
64
- /**
65
- * ⛔ CONTRACT C7's DISCLOSURE CHANNEL. `clean_actions` drops a config key it does not recognise
66
- * and answers 200 (D-75), so without this the person would accept a flow, open it, and find a
67
- * step configured differently from the one they were shown. Empty is the ordinary case.
68
- */
69
- dropped: { kind: string; keys: string[] }[];
70
- /**
71
- * ⛔ WHOLE-DRAFT DISCLOSURES — a truncated step list, a trigger this deployment does not have.
72
- * Standing rule (owner, W30/R6): a limit that cannot be removed must be REPORTED with its cause
73
- * and a recommended fix. The draft door caps a flow at 12 steps while the engine stores 20, so
74
- * a longer answer loses steps that WOULD have been storable — silently, until now.
75
- */
76
- notes: string[];
77
- /** Always false today, and read rather than assumed: the draft door writes nothing. */
78
- saved: boolean;
79
- }
80
-
81
- export function errorMessage(status: number, message?: string): string {
82
- if (status >= 500 || !message) {
83
- return status >= 500
84
- ? "Something went wrong on our side. Try again in a moment."
85
- : `The server answered ${status}.`;
86
- }
87
- return message;
88
- }
89
-
90
- async function call<T>(
91
- path: string,
92
- init: RequestInit,
93
- read: (body: unknown) => T
94
- ): Promise<Result<T>> {
95
- let res: Response;
96
- try {
97
- res = await fetch(`${API_V1}${path}`, { credentials: CREDENTIALS, ...init });
98
- } catch {
99
- return { ok: false, status: 0, message: "Cannot reach the server." };
100
- }
101
- const body = (await res.json().catch(() => null)) as unknown;
102
- if (!res.ok) {
103
- const detail = (body as { error?: { message?: string } } | null)?.error?.message;
104
- return { ok: false, status: res.status, message: errorMessage(res.status, detail) };
105
- }
106
- return { ok: true, value: read(body) };
107
- }
108
-
109
- const json = (data: unknown): RequestInit => ({
110
- method: "POST",
111
- headers: { "Content-Type": "application/json" },
112
- body: JSON.stringify(data),
113
- });
114
-
115
- const str = (v: unknown): string => (typeof v === "string" ? v : "");
116
-
117
- /** Fails CLOSED: an unreadable step is dropped, never half-parsed into a config nobody wrote. */
118
- export function parseDraft(body: unknown): DraftResult {
119
- const r = (body || {}) as Record<string, unknown>;
120
- const d = (r.draft || {}) as Record<string, unknown>;
121
- const rows = Array.isArray(d.actions) ? (d.actions as unknown[]) : [];
122
- const actions: DraftAction[] = [];
123
- rows.forEach((row, i) => {
124
- const a = (row || {}) as Record<string, unknown>;
125
- if (!str(a.kind)) return;
126
- actions.push({
127
- id: str(a.id) || `act_${i + 1}`,
128
- kind: str(a.kind),
129
- config: (a.config && typeof a.config === "object"
130
- ? (a.config as Record<string, unknown>)
131
- : {}),
132
- why: str(a.why),
133
- // Fails CLOSED the SAFE way: an unreadable `needs` is an empty list, so the page shows no
134
- // false alarm — and the create door still refuses at run time either way.
135
- needs: Array.isArray(a.needs) ? (a.needs as unknown[]).map(str).filter(Boolean) : [],
136
- ...(str(a.unknownField) ? { unknownField: str(a.unknownField) } : {}),
137
- });
138
- });
139
- const dropped: { kind: string; keys: string[] }[] = [];
140
- (Array.isArray(r.dropped) ? (r.dropped as unknown[]) : []).forEach((row) => {
141
- const x = (row || {}) as Record<string, unknown>;
142
- const keys = Array.isArray(x.keys) ? (x.keys as unknown[]).map(str).filter(Boolean) : [];
143
- if (keys.length) dropped.push({ kind: str(x.kind), keys });
144
- });
145
- return {
146
- draft: {
147
- name: str(d.name) || "New automation",
148
- trigger: str(d.trigger) || "manual",
149
- table: str(d.table),
150
- actions,
151
- },
152
- provider: str(r.provider),
153
- dropped,
154
- notes: Array.isArray(r.notes) ? (r.notes as unknown[]).map(str).filter(Boolean) : [],
155
- // ⚠ `=== true`, never truthy. An absent key must read as "nothing was saved", which is the
156
- // safe direction: the wrong one would tell a person their flow exists when it does not.
157
- saved: r.saved === true,
158
- };
159
- }
160
-
161
- /**
162
- * Describe an automation; get a draft back. **This writes nothing.**
163
- *
164
- * The draft is for a person to READ before `acceptDraft` stores it — the only check on the one
165
- * failure class no validator can see: a well-formed flow that does something other than what was
166
- * asked for.
167
- */
168
- export function draftFlow(prompt: string): Promise<Result<DraftResult>> {
169
- return call("/automations/draft", json({ prompt }), parseDraft);
170
- }
171
-
172
- /**
173
- * Store an accepted draft — through the ORDINARY create door, with no marker on it.
174
- *
175
- * ⛔ THIS IS THE WHOLE OF R3's *"indistinguishable from a hand-built one"*. There is no
176
- * `source: "ai"` field and no separate table: what lands is a `plain` automation whose flow
177
- * happens to have been typed by a model, and every editor, gate and runner treats it as one
178
- * because it IS one.
179
- */
180
- export function acceptDraft(draft: Draft): Promise<Result<{ id: string }>> {
181
- return call(
182
- "/automations",
183
- json({
184
- name: draft.name,
185
- kind: "plain",
186
- trigger: { key: draft.trigger || "manual" },
187
- ...(draft.table ? { config: { targetTable: draft.table } } : {}),
188
- flow: {
189
- actions: draft.actions.map((a) => ({ kind: a.kind, config: a.config })),
190
- },
191
- }),
192
- (b) => {
193
- const a = ((b || {}) as { automation?: Record<string, unknown> }).automation || {};
194
- return { id: str(a.id) };
195
- }
196
- );
197
- }