@llm4ts/shell 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/flows/epic-stories.js +9 -3
  2. package/flows/lib/epic-stories.js +24 -4
  3. package/kits/soap-ace/README.md +313 -0
  4. package/kits/soap-ace/fixtures/demo-bank-soap/RUNBOOK.md +102 -0
  5. package/kits/soap-ace/fixtures/demo-bank-soap/design/api-design.md +620 -0
  6. package/kits/soap-ace/fixtures/demo-bank-soap/responses/cercaConti.xml +3 -0
  7. package/kits/soap-ace/fixtures/demo-bank-soap/responses/cercaMovimenti.xml +3 -0
  8. package/kits/soap-ace/fixtures/demo-bank-soap/responses/confermaBonifico.xml +3 -0
  9. package/kits/soap-ace/fixtures/demo-bank-soap/responses/dettaglioConto.xml +3 -0
  10. package/kits/soap-ace/fixtures/demo-bank-soap/responses/inserisciBonifico.xml +3 -0
  11. package/kits/soap-ace/fixtures/demo-bank-soap/responses/revocaBonifico.xml +3 -0
  12. package/kits/soap-ace/fixtures/demo-bank-soap/soapui/DemoBank-soapui-project.xml +39 -0
  13. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/DemoBank.wsdl +186 -0
  14. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/LegacyRpc.wsdl +29 -0
  15. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/banking.xsd +10 -0
  16. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/bonifici.xsd +107 -0
  17. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/common.xsd +93 -0
  18. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/conti.xsd +71 -0
  19. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/movimenti.xsd +55 -0
  20. package/kits/soap-ace/flows/lib/soap/Analysis.js +508 -0
  21. package/kits/soap-ace/flows/lib/soap/Auth.js +179 -0
  22. package/kits/soap-ace/flows/lib/soap/Call.js +186 -0
  23. package/kits/soap-ace/flows/lib/soap/CallPolicy.js +50 -0
  24. package/kits/soap-ace/flows/lib/soap/Catalog.js +157 -0
  25. package/kits/soap-ace/flows/lib/soap/Classification.js +254 -0
  26. package/kits/soap-ace/flows/lib/soap/Design.js +497 -0
  27. package/kits/soap-ace/flows/lib/soap/DesignProposal.js +204 -0
  28. package/kits/soap-ace/flows/lib/soap/Discover.js +139 -0
  29. package/kits/soap-ace/flows/lib/soap/Envelope.js +132 -0
  30. package/kits/soap-ace/flows/lib/soap/Epic.js +239 -0
  31. package/kits/soap-ace/flows/lib/soap/Instance.js +675 -0
  32. package/kits/soap-ace/flows/lib/soap/Mapping.js +130 -0
  33. package/kits/soap-ace/flows/lib/soap/Masking.js +508 -0
  34. package/kits/soap-ace/flows/lib/soap/OpenApi.js +288 -0
  35. package/kits/soap-ace/flows/lib/soap/Samples.js +287 -0
  36. package/kits/soap-ace/flows/lib/soap/Scenarios.js +132 -0
  37. package/kits/soap-ace/flows/lib/soap/SoapUiImport.js +200 -0
  38. package/kits/soap-ace/flows/lib/soap/Stub.js +34 -0
  39. package/kits/soap-ace/flows/lib/soap/Transport.js +167 -0
  40. package/kits/soap-ace/flows/lib/soap/Wsdl.js +533 -0
  41. package/kits/soap-ace/flows/lib/soap/Xml.js +453 -0
  42. package/kits/soap-ace/flows/lib/soap/Xsd.js +471 -0
  43. package/kits/soap-ace/flows/lib/soap/Yaml.js +309 -0
  44. package/kits/soap-ace/flows/soap-design.js +167 -0
  45. package/kits/soap-ace/flows/soap-discover.js +85 -0
  46. package/kits/soap-ace/flows/soap-epic.js +204 -0
  47. package/kits/soap-ace/flows/soap-sample.js +265 -0
  48. package/kits/soap-ace/packs/ace12-rest/api-style.md +63 -0
  49. package/kits/soap-ace/packs/ace12-rest/pack.md +18 -0
  50. package/kits/soap-ace/packs/ace12-rest/prompts/implement.md +8 -0
  51. package/kits/soap-ace/packs/ace12-rest/reviewers/contract.md +5 -0
  52. package/kits/soap-ace/patterns/PAT-ACE-001-esito-to-problem.md +8 -0
  53. package/kits/soap-ace/patterns/PAT-ACE-002-fault-to-problem.md +8 -0
  54. package/kits/soap-ace/patterns/PAT-ACE-003-amounts-and-enums.md +8 -0
  55. package/kits/soap-ace/patterns/PAT-ACE-004-pagination.md +7 -0
  56. package/kits/soap-ace/patterns/PAT-ACE-005-nil-and-absence.md +7 -0
  57. package/kits/soap-ace/scaffolds/ace12-rest-api/CONTRIBUTING.md +56 -0
  58. package/kits/soap-ace/scaffolds/ace12-rest-api/README.md +16 -0
  59. package/kits/soap-ace/scaffolds/ace12-rest-api/scripts/ace-gates-container.sh +8 -0
  60. package/kits/soap-ace/scaffolds/ace12-rest-api/scripts/ace-gates.sh +41 -0
  61. package/package.json +4 -4
@@ -0,0 +1,309 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ export class YamlError extends Schema.TaggedError()("YamlError", {
4
+ line: Schema.Int,
5
+ detail: Schema.String
6
+ }) {
7
+ get message() {
8
+ return `line ${this.line}: ${this.detail}`;
9
+ }
10
+ }
11
+ export const isYamlMap = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
12
+ export const isYamlList = (value) => Array.isArray(value);
13
+ /** Remove a trailing comment that is outside quotes. */
14
+ const stripComment = (text) => {
15
+ let quote;
16
+ for (let index = 0; index < text.length; index++) {
17
+ const char = text[index];
18
+ if (quote !== undefined) {
19
+ if (char === "\\" && quote === '"')
20
+ index++;
21
+ else if (char === quote)
22
+ quote = undefined;
23
+ }
24
+ else if (char === '"' || char === "'") {
25
+ // A quote opens a quoted scalar only where a value starts (after a key,
26
+ // a list dash, or a flow bracket), never inside a plain value (5 'ft).
27
+ const before = text.slice(0, index).trimEnd();
28
+ if (before === "" || /(^|\s)-$|[:[{,]$/.test(before))
29
+ quote = char;
30
+ }
31
+ else if (char === "#" && (index === 0 || /\s/.test(text[index - 1] ?? ""))) {
32
+ return text.slice(0, index).trimEnd();
33
+ }
34
+ }
35
+ return text.trimEnd();
36
+ };
37
+ class Reader {
38
+ index = 0;
39
+ lines;
40
+ raw;
41
+ constructor(source) {
42
+ this.raw = source.replace(/^\uFEFF/, "").split(/\r?\n/);
43
+ const lines = [];
44
+ for (const [position, rawLine] of this.raw.entries()) {
45
+ if (/^\s*\t/.test(rawLine) && rawLine.trim() !== "") {
46
+ throw new YamlError({ line: position + 1, detail: "tabs are not allowed for indentation" });
47
+ }
48
+ if (rawLine.trim() === "---" || rawLine.trim() === "...")
49
+ continue;
50
+ const text = stripComment(rawLine);
51
+ if (text.trim() === "")
52
+ continue;
53
+ lines.push({
54
+ number: position + 1,
55
+ indent: text.length - text.trimStart().length,
56
+ text: text.trim()
57
+ });
58
+ }
59
+ this.lines = lines;
60
+ }
61
+ fail(line, detail) {
62
+ throw new YamlError({ line, detail });
63
+ }
64
+ document() {
65
+ const first = this.lines[0];
66
+ if (first === undefined)
67
+ return null;
68
+ const value = this.block(first.indent);
69
+ const rest = this.lines[this.index];
70
+ if (rest !== undefined)
71
+ this.fail(rest.number, "unexpected content (check indentation)");
72
+ return value;
73
+ }
74
+ block(indent) {
75
+ const line = this.lines[this.index];
76
+ if (line === undefined)
77
+ return null;
78
+ return line.text === "-" || line.text.startsWith("- ")
79
+ ? this.sequence(indent)
80
+ : this.mapping(indent);
81
+ }
82
+ sequence(indent) {
83
+ const items = [];
84
+ for (;;) {
85
+ const line = this.lines[this.index];
86
+ if (line === undefined || line.indent < indent)
87
+ return items;
88
+ if (line.indent > indent)
89
+ this.fail(line.number, "unexpected indentation");
90
+ if (!(line.text === "-" || line.text.startsWith("- ")))
91
+ return items;
92
+ const rest = line.text === "-" ? "" : line.text.slice(2).trim();
93
+ if (rest === "-" || rest.startsWith("- ")) {
94
+ // `- - a`: a list item that is itself a list, starting on this line.
95
+ const nestedIndent = indent + (line.text.length - rest.length);
96
+ this.lines[this.index] = { number: line.number, indent: nestedIndent, text: rest };
97
+ items.push(this.sequence(nestedIndent));
98
+ continue;
99
+ }
100
+ this.index++;
101
+ if (rest === "") {
102
+ const next = this.lines[this.index];
103
+ items.push(next !== undefined && next.indent > indent ? this.block(next.indent) : null);
104
+ }
105
+ else if (this.isMappingStart(rest)) {
106
+ // `- key: value` opens a mapping whose further keys sit two columns in.
107
+ const itemIndent = indent + (line.text.length - rest.length);
108
+ items.push(this.mapping(itemIndent, { text: rest, number: line.number }));
109
+ }
110
+ else {
111
+ items.push(this.scalar(rest, line.number, indent));
112
+ }
113
+ }
114
+ }
115
+ isMappingStart(text) {
116
+ return this.splitKey(text) !== undefined;
117
+ }
118
+ splitKey(text) {
119
+ if (text.startsWith('"') || text.startsWith("'")) {
120
+ const quote = text[0] ?? '"';
121
+ let end = 1;
122
+ while (end < text.length) {
123
+ if (text[end] === "\\" && quote === '"')
124
+ end += 2;
125
+ else if (text[end] === quote && quote === "'" && text[end + 1] === "'")
126
+ end += 2;
127
+ else if (text[end] === quote)
128
+ break;
129
+ else
130
+ end++;
131
+ }
132
+ const after = text.slice(end + 1);
133
+ if (!/^\s*:(\s|$)/.test(after))
134
+ return undefined;
135
+ return {
136
+ key: this.quoted(text.slice(0, end + 1), 0),
137
+ rest: after.replace(/^\s*:/, "").trim()
138
+ };
139
+ }
140
+ const match = /^([^\s:#][^:#]*?|[^\s:#]):(\s+|$)(.*)$/.exec(text);
141
+ if (match === null)
142
+ return undefined;
143
+ return { key: (match[1] ?? "").trim(), rest: match[3] ?? "" };
144
+ }
145
+ mapping(indent, first) {
146
+ const entries = {};
147
+ const add = (text, number, lineIndent) => {
148
+ const split = this.splitKey(text);
149
+ if (split === undefined)
150
+ this.fail(number, `expected "key: value", found ${JSON.stringify(text)}`);
151
+ if (Object.hasOwn(entries, split.key))
152
+ this.fail(number, `duplicate key ${split.key}`);
153
+ if (split.rest === "") {
154
+ const next = this.lines[this.index];
155
+ entries[split.key] =
156
+ next !== undefined &&
157
+ (next.indent > lineIndent ||
158
+ (next.indent === lineIndent && (next.text === "-" || next.text.startsWith("- "))))
159
+ ? this.block(next.indent)
160
+ : null;
161
+ }
162
+ else {
163
+ entries[split.key] = this.scalar(split.rest, number, lineIndent);
164
+ }
165
+ };
166
+ if (first !== undefined)
167
+ add(first.text, first.number, indent);
168
+ for (;;) {
169
+ const line = this.lines[this.index];
170
+ if (line === undefined || line.indent < indent)
171
+ return entries;
172
+ if (line.indent > indent)
173
+ this.fail(line.number, "unexpected indentation");
174
+ if (line.text === "-" || line.text.startsWith("- "))
175
+ return entries;
176
+ this.index++;
177
+ add(line.text, line.number, indent);
178
+ }
179
+ }
180
+ scalar(text, number, indent) {
181
+ if (text === "~" || text === "null" || text === "Null" || text === "NULL")
182
+ return null;
183
+ if (text === "[]")
184
+ return [];
185
+ if (text === "{}")
186
+ return {};
187
+ if (text.startsWith('"') || text.startsWith("'"))
188
+ return this.quoted(text, number);
189
+ if (text === "|" || text === ">" || /^[|>][-+]?$/.test(text))
190
+ return this.blockScalar(text, indent, number);
191
+ if (/^[[{&*!%@`]/.test(text)) {
192
+ this.fail(number, `unsupported YAML construct at ${JSON.stringify(text.slice(0, 12))}`);
193
+ }
194
+ return text;
195
+ }
196
+ quoted(text, number) {
197
+ const quote = text[0];
198
+ if (text.length < 2 || text[text.length - 1] !== quote) {
199
+ this.fail(number, "unterminated quoted scalar");
200
+ }
201
+ const body = text.slice(1, -1);
202
+ if (quote === "'")
203
+ return body.replace(/''/g, "'");
204
+ return body.replace(/\\(u[0-9A-Fa-f]{4}|.)/g, (_, escape) => {
205
+ if (escape.startsWith("u"))
206
+ return String.fromCharCode(Number.parseInt(escape.slice(1), 16));
207
+ const map = {
208
+ n: "\n",
209
+ t: "\t",
210
+ r: "\r",
211
+ b: "\b",
212
+ f: "\f",
213
+ '"': '"',
214
+ "\\": "\\",
215
+ "/": "/",
216
+ "0": "\0"
217
+ };
218
+ const value = map[escape];
219
+ if (value === undefined)
220
+ this.fail(number, `unknown escape \\${escape}`);
221
+ return value;
222
+ });
223
+ }
224
+ blockScalar(header, indent, headerLine) {
225
+ const folded = header.startsWith(">");
226
+ const keep = header.endsWith("+");
227
+ const strip = header.endsWith("-");
228
+ // Block scalar content keeps its comments and blank lines, so it is read
229
+ // from the raw source rather than the comment-stripped lines.
230
+ // Content starts on the raw line after the header, even when that line
231
+ // looks like a comment (# heading); its indentation is the first
232
+ // non-blank line's.
233
+ let rawIndex = headerLine;
234
+ const firstContent = this.raw.slice(rawIndex).find((raw) => raw.trim() !== "");
235
+ const contentIndent = firstContent === undefined ? 0 : firstContent.length - firstContent.trimStart().length;
236
+ if (firstContent === undefined || contentIndent <= indent)
237
+ return "";
238
+ const collected = [];
239
+ while (rawIndex < this.raw.length) {
240
+ const raw = this.raw[rawIndex] ?? "";
241
+ const rawIndent = raw.length - raw.trimStart().length;
242
+ if (raw.trim() !== "" && rawIndent < contentIndent)
243
+ break;
244
+ collected.push(raw.slice(contentIndent));
245
+ rawIndex++;
246
+ }
247
+ while (this.lines[this.index] !== undefined &&
248
+ (this.lines[this.index]?.number ?? 0) <= rawIndex) {
249
+ this.index++;
250
+ }
251
+ while (collected.length > 0 && collected[collected.length - 1]?.trim() === "")
252
+ collected.pop();
253
+ const text = folded
254
+ ? collected.join("\n").replace(/([^\n])\n(?=[^\n])/g, "$1 ")
255
+ : collected.join("\n");
256
+ return strip ? text : keep ? `${text}\n` : `${text}\n`;
257
+ }
258
+ }
259
+ export const parseYaml = (source) => Effect.try({
260
+ try: () => new Reader(source).document(),
261
+ catch: (cause) => cause instanceof YamlError ? cause : new YamlError({ line: 0, detail: "unreadable YAML" })
262
+ });
263
+ // ---------------------------------------------------------------------------
264
+ // Writing
265
+ const plainUnsafe = /^[-?:,[\]{}#&*!|>'"%@`\s]|[\s]$|:$|: |\s#|^(~|null|Null|NULL)$|[\n\r\t]/;
266
+ /** A scalar as YAML: plain when it reads back unchanged, otherwise double-quoted. */
267
+ export const yamlScalar = (value) => {
268
+ if (value === null)
269
+ return "~";
270
+ if (value !== "" &&
271
+ !plainUnsafe.test(value) &&
272
+ ![...value].some((char) => char.charCodeAt(0) < 32))
273
+ return value;
274
+ return JSON.stringify(value);
275
+ };
276
+ export const yamlKey = (key) => /^[A-Za-z_@][A-Za-z0-9_.\-@#]*$/.test(key) ? key : JSON.stringify(key);
277
+ /** Serialize a value (no comments); mappings keep insertion order. */
278
+ export const renderYaml = (value, indent = 0) => {
279
+ const pad = " ".repeat(indent);
280
+ if (value === null || typeof value === "string")
281
+ return `${pad}${yamlScalar(value)}\n`;
282
+ if (isYamlList(value)) {
283
+ if (value.length === 0)
284
+ return `${pad}[]\n`;
285
+ return value
286
+ .map((item) => {
287
+ if (item === null || typeof item === "string")
288
+ return `${pad}- ${yamlScalar(item)}\n`;
289
+ const nested = renderYaml(item, indent + 2);
290
+ return `${pad}- ${nested.slice(indent + 2)}`;
291
+ })
292
+ .join("");
293
+ }
294
+ const entries = Object.entries(value);
295
+ if (entries.length === 0)
296
+ return `${pad}{}\n`;
297
+ return entries
298
+ .map(([key, item]) => {
299
+ if (item === null || typeof item === "string") {
300
+ return `${pad}${yamlKey(key)}: ${yamlScalar(item)}\n`;
301
+ }
302
+ if ((isYamlList(item) && item.length === 0) ||
303
+ (isYamlMap(item) && Object.keys(item).length === 0)) {
304
+ return `${pad}${yamlKey(key)}: ${isYamlList(item) ? "[]" : "{}"}\n`;
305
+ }
306
+ return `${pad}${yamlKey(key)}:\n${renderYaml(item, isYamlList(item) ? indent : indent + 2)}`;
307
+ })
308
+ .join("");
309
+ };
@@ -0,0 +1,167 @@
1
+ // Turn SOAP evidence into a REST design: analysis of masked samples, the 1:1 mapping, an evidence-citing design proposal, its check, and the projected OpenAPI 3.1 contract.
2
+ //
3
+ // Runs after soap-sample, rooted at the same repository. The task text is a
4
+ // command:
5
+ //
6
+ // llm4ts run soap-design --repo . "analyse [operation]" analysis/<op>.md + .json from the exchanges
7
+ // llm4ts run soap-design --repo . mapping design/mapping.json + .md, the 1:1 base
8
+ // llm4ts run soap-design --repo . design the reasoning seat drafts design/api-design.md
9
+ // llm4ts run soap-design --repo . revise redraft against the check's findings
10
+ // llm4ts run soap-design --repo . check the deterministic design review
11
+ // llm4ts run soap-design --repo . openapi design/openapi.yaml from an approved design
12
+ //
13
+ // The design is a typed overlay on the mapping: endpoints name their SOAP
14
+ // operations, models bind to XSD types, properties cite XSD paths, errors
15
+ // cite outcome codes and faults (see the kit README). `check` verifies
16
+ // coverage of every operation, verbs against the confirmed classes in
17
+ // operations.md, the style guide (packs/ace12-rest/api-style.md), every source path, enum maps
18
+ // against declared and observed values, and that every business error the
19
+ // samples showed is mapped. api-design.md is the approval file: editing its
20
+ // JSON block is the review, `Status: approved` the sign-off; an existing
21
+ // file is never overwritten by `design`. openapi refuses a design that is
22
+ // not approved or has check errors. LLM4TS_REASONER picks the seat that
23
+ // drafts (default claude); LLM4TS_SOAP_SERVICE picks the service.
24
+ import { readFile } from "node:fs/promises";
25
+ import { fileURLToPath } from "node:url";
26
+ import * as Effect from "effect/Effect";
27
+ import { FlowAborted, Info, makeNodeWorkspace, mock, resolveFlowInput, runFlowMain, runNode, ScriptUsage } from "@llm4ts/runner";
28
+ import { coderFor } from "@llm4ts/runner/Connectors";
29
+ import { analysisPaths, writeAnalyses } from "./lib/soap/Analysis.js";
30
+ import { operationByName } from "./lib/soap/Catalog.js";
31
+ import { blockingIssues, checkDesign, designPaths, parseDesignFile, renderDesignFile } from "./lib/soap/Design.js";
32
+ import { proposeDesign, reviseDesign } from "./lib/soap/DesignProposal.js";
33
+ import { loadCatalog, loadOperationsFile, readIfPresent, serviceDirectory } from "./lib/soap/Discover.js";
34
+ import { encodeMapping, mapService, renderMapping } from "./lib/soap/Mapping.js";
35
+ import { projectOpenApi, renderTypedYaml } from "./lib/soap/OpenApi.js";
36
+ const usage = 'usage: soap-design "analyse [operation] | mapping | design | revise | check | openapi"';
37
+ const styleGuide = () => Effect.tryPromise({
38
+ try: () => readFile(fileURLToPath(new URL("../packs/ace12-rest/api-style.md", import.meta.url)), "utf8"),
39
+ catch: () => FlowAborted.make({ message: "cannot read the kit's packs/ace12-rest/api-style.md" })
40
+ });
41
+ const program = Effect.gen(function* () {
42
+ const input = yield* resolveFlowInput("analyse");
43
+ const [command = "analyse", ...args] = input.prompt.trim().split(/\s+/);
44
+ const environment = process.env;
45
+ const drafting = command === "design" || command === "revise";
46
+ const reasoner = drafting
47
+ ? coderFor((environment.LLM4TS_REASONER ?? "claude").trim() || "claude")
48
+ : undefined;
49
+ if (drafting && reasoner === undefined) {
50
+ return yield* ScriptUsage.make({
51
+ message: `unknown LLM4TS_REASONER '${environment.LLM4TS_REASONER ?? ""}'`
52
+ });
53
+ }
54
+ yield* runNode({
55
+ workDir: input.workDir,
56
+ workspace: input.workspace,
57
+ userPrompt: input.prompt,
58
+ coder: mock,
59
+ ...(reasoner === undefined ? {} : { reasoning: reasoner }),
60
+ environment
61
+ }, (context) => Effect.gen(function* () {
62
+ const say = (message) => context.events.publish(Info.make({ message }));
63
+ const workspace = yield* makeNodeWorkspace(input.workDir);
64
+ const requested = environment.LLM4TS_SOAP_SERVICE?.trim();
65
+ const services = (yield* workspace.discover(".llm4ts/soap/*/catalog.json")).map((path) => path.split("/")[2] ?? "");
66
+ const service = requested !== undefined && requested !== ""
67
+ ? requested
68
+ : services.length === 1
69
+ ? services[0]
70
+ : undefined;
71
+ if (service === undefined) {
72
+ return yield* FlowAborted.make({
73
+ message: services.length === 0
74
+ ? "no discovered service here; run soap-discover first"
75
+ : `several services (${services.join(", ")}); set LLM4TS_SOAP_SERVICE`
76
+ });
77
+ }
78
+ const catalog = yield* loadCatalog(workspace, service);
79
+ const operations = yield* loadOperationsFile(workspace, service);
80
+ const paths = designPaths(serviceDirectory(service));
81
+ const mapping = mapService(catalog);
82
+ const allAnalyses = () => writeAnalyses(workspace, catalog, service);
83
+ const report = (issues) => Effect.forEach(issues, (issue) => say(` ${issue.severity} ${issue.where}: ${issue.detail}`), { discard: true });
84
+ switch (command) {
85
+ case "analyse":
86
+ case "analyze": {
87
+ const names = args.length === 0 ? catalog.operations.map((operation) => operation.name) : args;
88
+ for (const name of names) {
89
+ if (operationByName(catalog, name) === undefined)
90
+ return yield* FlowAborted.make({ message: `unknown operation ${name}` });
91
+ }
92
+ for (const analysis of yield* writeAnalyses(workspace, catalog, service, names)) {
93
+ yield* say(`${analysis.operation}: ${analysis.exchanges} exchanges, ${analysis.outcomes.length} outcome codes, ${analysis.faults.length} faults, ${analysis.findings.length} schema findings, ${analysis.observations.length} observations → ${analysisPaths(service, analysis.operation).markdown}`);
94
+ }
95
+ return;
96
+ }
97
+ case "mapping": {
98
+ yield* workspace.write(paths.mapping, encodeMapping(mapping));
99
+ yield* workspace.write(paths.mappingSummary, renderMapping(mapping));
100
+ yield* say(`wrote ${paths.mapping} and ${paths.mappingSummary}`);
101
+ return;
102
+ }
103
+ case "design":
104
+ case "revise": {
105
+ const existing = yield* readIfPresent(workspace, paths.design);
106
+ if (command === "design" && existing !== undefined) {
107
+ return yield* FlowAborted.make({
108
+ message: `${paths.design} exists; edit it, run "check", or "revise" to redraft it`
109
+ });
110
+ }
111
+ if (command === "revise" && existing === undefined) {
112
+ return yield* FlowAborted.make({
113
+ message: `no ${paths.design} yet; run "design" first`
114
+ });
115
+ }
116
+ const analyses = yield* allAnalyses();
117
+ const style = yield* styleGuide();
118
+ yield* workspace.write(paths.mapping, encodeMapping(mapping));
119
+ yield* workspace.write(paths.mappingSummary, renderMapping(mapping));
120
+ const base = { catalog, operations, mapping, analyses, style };
121
+ const draft = existing === undefined
122
+ ? yield* proposeDesign(context.reasoning, base)
123
+ : yield* Effect.flatMap(parseDesignFile(existing), (file) => reviseDesign(context.reasoning, {
124
+ ...base,
125
+ current: file.design,
126
+ issues: checkDesign(file.design, { catalog, operations, analyses })
127
+ }));
128
+ const issues = checkDesign(draft, { catalog, operations, analyses });
129
+ yield* workspace.write(paths.design, renderDesignFile(draft, issues, "proposed"));
130
+ yield* say(`wrote ${paths.design}: ${draft.endpoints.length} endpoints, ${draft.models.length} models, ${blockingIssues(issues).length} errors, ${issues.length - blockingIssues(issues).length} warnings`);
131
+ yield* report(issues);
132
+ return;
133
+ }
134
+ case "check":
135
+ case "openapi": {
136
+ const text = yield* readIfPresent(workspace, paths.design);
137
+ if (text === undefined)
138
+ return yield* FlowAborted.make({
139
+ message: `no ${paths.design} yet; run "design" first`
140
+ });
141
+ const file = yield* parseDesignFile(text);
142
+ const analyses = yield* allAnalyses();
143
+ const issues = checkDesign(file.design, { catalog, operations, analyses });
144
+ const errors = blockingIssues(issues);
145
+ yield* workspace.write(paths.design, renderDesignFile(file.design, issues, file.approved ? "approved" : "proposed"));
146
+ yield* say(`${paths.design}: ${file.approved ? "approved" : "proposed"}, ${errors.length} errors, ${issues.length - errors.length} warnings`);
147
+ yield* report(issues);
148
+ if (command === "check")
149
+ return;
150
+ if (!file.approved)
151
+ return yield* FlowAborted.make({
152
+ message: "the design is not approved; set Status: approved after review"
153
+ });
154
+ if (errors.length > 0)
155
+ return yield* FlowAborted.make({
156
+ message: `the design has ${errors.length} check errors; fix them first`
157
+ });
158
+ yield* workspace.write(paths.openapi, renderTypedYaml(projectOpenApi(file.design)));
159
+ yield* say(`wrote ${paths.openapi} (projected; do not edit)`);
160
+ return;
161
+ }
162
+ default:
163
+ return yield* FlowAborted.make({ message: `unknown command ${command}\n${usage}` });
164
+ }
165
+ }));
166
+ });
167
+ runFlowMain(program);
@@ -0,0 +1,85 @@
1
+ // Discover a SOAP service: typed catalog of operations and types from its WSDL, open questions, and proposed read/mutating classes; no LLM unless a judgment seat is set.
2
+ //
3
+ // The task text is the WSDL location, a path (relative to the launch
4
+ // directory) or an http(s) URL:
5
+ //
6
+ // llm4ts run soap-discover --repo ~/work/conti-api ./wsdl/DemoBank.wsdl
7
+ //
8
+ // Writes under <repo>/.llm4ts/soap/<service>/:
9
+ //
10
+ // catalog.json the typed catalog every later soap-* step reads
11
+ // catalog.md a human summary: endpoints, operations, fields, open questions
12
+ // operations.md proposed read/mutating classes; review, then set
13
+ // `Status: confirmed`. An existing file is kept as is.
14
+ //
15
+ // The catalog is read by code (document/literal SOAP 1.1/1.2 bindings, WSDL
16
+ // 1.1 and 2.0); anything it does not model is an open question, never a
17
+ // guess. Classes are proposed by a name heuristic and, when
18
+ // LLM4TS_JUDGMENT_PROVIDER names a judgment seat, by a typed judgment.
19
+ // LLM4TS_SOAP_SERVICE overrides the service directory name. For a URL, the
20
+ // fetch side of <repo>/.llm4ts/soap/<LLM4TS_SOAP_SERVICE>/auth.json (Basic,
21
+ // Bearer, mTLS; references only) authenticates every document request.
22
+ import { isAbsolute, resolve } from "node:path";
23
+ import * as Effect from "effect/Effect";
24
+ import { Info, makeNodeWorkspace, mock, resolveFlowInput, runFlowMain, runNode } from "@llm4ts/runner";
25
+ import { judgmentConnectorFromEnvironment } from "@llm4ts/runner/Connectors";
26
+ import { nodeSecretSource, resolveSide } from "./lib/soap/Auth.js";
27
+ import { discoverService, loadAuthProfile } from "./lib/soap/Discover.js";
28
+ import { makeNodeSoapTransport, makeTransportDocumentLoader } from "./lib/soap/Transport.js";
29
+ import { DocumentLoader } from "./lib/soap/Wsdl.js";
30
+ const isUrl = (location) => /^https?:\/\//i.test(location);
31
+ const program = Effect.gen(function* () {
32
+ const input = yield* resolveFlowInput();
33
+ const raw = input.prompt.trim();
34
+ const location = isUrl(raw) || isAbsolute(raw) ? raw : resolve(input.workspace, raw);
35
+ const judgment = yield* judgmentConnectorFromEnvironment();
36
+ const service = process.env.LLM4TS_SOAP_SERVICE?.trim();
37
+ yield* runNode({
38
+ workDir: input.workDir,
39
+ workspace: input.workspace,
40
+ userPrompt: input.prompt,
41
+ // Discovery is deterministic; the mock seat only satisfies the context shape.
42
+ coder: mock,
43
+ ...(judgment === undefined ? {} : { judgment }),
44
+ environment: process.env
45
+ }, (context) => Effect.gen(function* () {
46
+ const say = (message) => context.events.publish(Info.make({ message }));
47
+ const workspace = yield* makeNodeWorkspace(input.workDir);
48
+ const profile = service === undefined || service === ""
49
+ ? undefined
50
+ : yield* loadAuthProfile(workspace, service);
51
+ if (isUrl(location) && profile === undefined) {
52
+ yield* say("fetching without credentials (set LLM4TS_SOAP_SERVICE and write its auth.json for an authenticated WSDL)");
53
+ }
54
+ const fetchSide = yield* resolveSide(nodeSecretSource(), profile?.fetch);
55
+ const loader = makeTransportDocumentLoader(makeNodeSoapTransport(), fetchSide);
56
+ const result = yield* discoverService({
57
+ workspace,
58
+ location,
59
+ ...(service === undefined || service === "" ? {} : { service }),
60
+ ...(judgment === undefined || context.judgment === undefined
61
+ ? {}
62
+ : { judgment: context.judgment })
63
+ }).pipe(Effect.provideService(DocumentLoader, loader));
64
+ const { catalog } = result;
65
+ yield* say(`${result.service}: ${catalog.operations.length} operations, ${catalog.endpoints.length} endpoints, ${catalog.documents.length} documents, ${catalog.openQuestions.length} open questions`);
66
+ for (const question of catalog.openQuestions) {
67
+ yield* say(` open question [${question.code}] ${question.subject}: ${question.detail}`);
68
+ }
69
+ if (result.proposals !== undefined) {
70
+ const count = (kind) => result.proposals?.filter((proposal) => proposal.proposed === kind).length ?? 0;
71
+ yield* say(`proposed classes: ${count("read")} read, ${count("mutating")} mutating, ${count("unclassified")} unclassified — review ${result.paths.operations} and set Status: confirmed`);
72
+ }
73
+ else if (result.existing !== undefined) {
74
+ yield* say(`kept ${result.paths.operations} (${result.existing.confirmed ? "confirmed" : "still proposed"})`);
75
+ if (result.drift.missing.length > 0) {
76
+ yield* say(` not classified there (treated as unclassified): ${result.drift.missing.join(", ")}`);
77
+ }
78
+ if (result.drift.unknown.length > 0) {
79
+ yield* say(` no longer in the WSDL: ${result.drift.unknown.join(", ")}`);
80
+ }
81
+ }
82
+ yield* say(`wrote ${result.paths.catalog} and ${result.paths.summary}`);
83
+ }));
84
+ });
85
+ runFlowMain(program);