@llm4ts/shell 2.11.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 +25 -5
  2. package/flows/lib/epic-stories.js +76 -8
  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,254 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import { choice } from "@llm4ts/core/judgment/Schemas";
4
+ import { certaintyOf, decide } from "@llm4ts/flow/Judgment";
5
+ import { ComplexTypeDef, elementByName, typeByName } from "./Catalog.js";
6
+ // Operation classification (read / mutating) decides two things: whether
7
+ // the tool may call an operation at all in a given environment, and which
8
+ // REST verb the design gives it. The tool only proposes: a name heuristic
9
+ // always, a `Judgment` answer when a judgment seat is configured. The human
10
+ // confirms by editing `operations.md` and flipping its status to
11
+ // `confirmed`; until then every operation counts as unclassified and cannot
12
+ // be called. An existing file is never overwritten.
13
+ export const OperationClass = Schema.Literals(["read", "mutating", "unclassified"]);
14
+ export class OperationsFileError extends Schema.TaggedError()("OperationsFileError", {
15
+ line: Schema.Int,
16
+ detail: Schema.String
17
+ }) {
18
+ get message() {
19
+ return `operations.md:${this.line}: ${this.detail}`;
20
+ }
21
+ }
22
+ // Italian and English verbs seen in bank SOAP estates. Matched against the
23
+ // operation name's leading word (camelCase, snake_case, or PascalCase).
24
+ const readVerbs = [
25
+ "cerca",
26
+ "ricerca",
27
+ "leggi",
28
+ "lettura",
29
+ "lista",
30
+ "elenca",
31
+ "elenco",
32
+ "dettaglio",
33
+ "visualizza",
34
+ "consulta",
35
+ "verifica",
36
+ "controlla",
37
+ "calcola",
38
+ "simula",
39
+ "stato",
40
+ "get",
41
+ "list",
42
+ "find",
43
+ "search",
44
+ "read",
45
+ "inquiry",
46
+ "inquire",
47
+ "query",
48
+ "retrieve",
49
+ "check",
50
+ "validate",
51
+ "fetch",
52
+ "lookup",
53
+ "view",
54
+ "count"
55
+ ];
56
+ const mutatingVerbs = [
57
+ "inserisci",
58
+ "inserimento",
59
+ "crea",
60
+ "creazione",
61
+ "aggiorna",
62
+ "aggiornamento",
63
+ "modifica",
64
+ "cancella",
65
+ "elimina",
66
+ "revoca",
67
+ "annulla",
68
+ "conferma",
69
+ "esegui",
70
+ "blocca",
71
+ "sblocca",
72
+ "autorizza",
73
+ "invia",
74
+ "registra",
75
+ "apri",
76
+ "chiudi",
77
+ "attiva",
78
+ "disattiva",
79
+ "storna",
80
+ "create",
81
+ "insert",
82
+ "add",
83
+ "update",
84
+ "modify",
85
+ "set",
86
+ "delete",
87
+ "remove",
88
+ "cancel",
89
+ "confirm",
90
+ "execute",
91
+ "submit",
92
+ "approve",
93
+ "authorize",
94
+ "block",
95
+ "unblock",
96
+ "open",
97
+ "close",
98
+ "activate",
99
+ "deactivate",
100
+ "send",
101
+ "register",
102
+ "post",
103
+ "put",
104
+ "transfer",
105
+ "pay"
106
+ ];
107
+ /** The operation name's first word, lower-cased. */
108
+ export const leadingWord = (name) => {
109
+ const match = /^[a-z]+|^[A-Z][a-z]+|^[A-Z]+(?![a-z])/.exec(name.replace(/^[_\W]+/, ""));
110
+ return (match?.[0] ?? name).toLowerCase();
111
+ };
112
+ export const heuristicClass = (name) => {
113
+ const word = leadingWord(name);
114
+ if (readVerbs.includes(word))
115
+ return { class: "read", reason: `name starts with "${word}"` };
116
+ if (mutatingVerbs.includes(word)) {
117
+ return { class: "mutating", reason: `name starts with "${word}"` };
118
+ }
119
+ return { class: "unclassified", reason: `no known verb in "${word}"` };
120
+ };
121
+ const fieldNames = (catalog, element) => {
122
+ if (element === undefined)
123
+ return [];
124
+ const type = typeByName(catalog, elementByName(catalog, element)?.type ?? "");
125
+ return type instanceof ComplexTypeDef ? type.fields.map((field) => field.name) : [];
126
+ };
127
+ const classQuestion = choice("Classify this SOAP operation of a bank's backend service. Would calling it change state in the backend (create, update, delete, confirm, execute, block, send), or does it only read?", {
128
+ read: "Only reads or computes: inquiries, lists, details, checks, simulations. Calling it twice has no additional effect.",
129
+ mutating: "Changes state: creates, updates, deletes, confirms, executes, blocks, revokes, or sends something."
130
+ });
131
+ /** Ask the judgment for one operation; `undefined` when it fails or does not answer. */
132
+ export const judgeClass = (judgment, catalog, operation) => judgment
133
+ .judge({
134
+ state: {
135
+ operation: operation.name,
136
+ soapAction: operation.soapAction,
137
+ documentation: operation.documentation ?? "",
138
+ requestFields: fieldNames(catalog, operation.input),
139
+ responseFields: fieldNames(catalog, operation.output),
140
+ faults: operation.faults.map((fault) => fault.name)
141
+ },
142
+ questions: { class: classQuestion }
143
+ })
144
+ .pipe(Effect.map((result) => {
145
+ const answer = result.answers["class"];
146
+ if (answer?.type !== "choice")
147
+ return undefined;
148
+ if (answer.choice !== "read" && answer.choice !== "mutating")
149
+ return undefined;
150
+ return { class: answer.choice, confidence: certaintyOf(answer), decision: decide(answer) };
151
+ }), Effect.orElseSucceed(() => undefined));
152
+ /**
153
+ * Combine the two signals. A judgment the policy would act on wins; one it
154
+ * would hold is ignored; in between, it wins only when the heuristic had no
155
+ * opinion. Disagreement stays visible in the file for the human to settle.
156
+ */
157
+ export const proposeClass = (operation, heuristic, judged) => {
158
+ const proposed = judged === undefined || judged.decision === "hold"
159
+ ? heuristic.class
160
+ : judged.decision === "act" || heuristic.class === "unclassified"
161
+ ? judged.class
162
+ : heuristic.class;
163
+ return { operation, proposed, heuristic, judged };
164
+ };
165
+ export const classifyOperations = (catalog, judgment) => Effect.forEach(catalog.operations, (operation) => Effect.map(judgment === undefined
166
+ ? Effect.succeed(undefined)
167
+ : judgeClass(judgment, catalog, operation), (judged) => proposeClass(operation.name, heuristicClass(operation.name), judged)));
168
+ const describeProposal = (proposal) => {
169
+ const judged = proposal.judged === undefined
170
+ ? ""
171
+ : `; judgment: ${proposal.judged.class} ${proposal.judged.confidence.toFixed(2)} (${proposal.judged.decision})`;
172
+ return `heuristic: ${proposal.heuristic.class}, ${proposal.heuristic.reason}${judged}`;
173
+ };
174
+ export const renderOperationsFile = (service, catalog, proposals) => {
175
+ const lines = [
176
+ `# Operations: ${service}`,
177
+ "",
178
+ "Status: proposed",
179
+ "",
180
+ "Review every `class:` below (read | mutating | unclassified), then change the",
181
+ "status above to `confirmed`. Until then every operation is unclassified:",
182
+ "nothing can be called, and the design cannot choose REST verbs. An",
183
+ "unclassified operation stays uncallable after confirmation. This file is",
184
+ "never overwritten; delete it to get fresh proposals.",
185
+ ""
186
+ ];
187
+ for (const proposal of proposals) {
188
+ const operation = catalog.operations.find((candidate) => candidate.name === proposal.operation);
189
+ lines.push(`## ${proposal.operation}`, "");
190
+ if (operation?.documentation !== undefined)
191
+ lines.push(operation.documentation, "");
192
+ lines.push(`- class: ${proposal.proposed}`, `- evidence: ${describeProposal(proposal)}`, "");
193
+ }
194
+ return lines.join("\n");
195
+ };
196
+ export const parseOperationsFile = (text) => Effect.suspend(() => {
197
+ const lines = text.split(/\r?\n/);
198
+ let confirmed;
199
+ let current;
200
+ const classes = new Map();
201
+ for (const [index, raw] of lines.entries()) {
202
+ const line = raw.trim();
203
+ const status = /^status:\s*(\S+)\s*$/i.exec(line);
204
+ if (status !== null && current === undefined) {
205
+ const value = status[1]?.toLowerCase();
206
+ if (value !== "proposed" && value !== "confirmed") {
207
+ return Effect.fail(new OperationsFileError({
208
+ line: index + 1,
209
+ detail: `status must be proposed or confirmed, found ${status[1] ?? ""}`
210
+ }));
211
+ }
212
+ confirmed = value === "confirmed";
213
+ continue;
214
+ }
215
+ const heading = /^##\s+(\S+)\s*$/.exec(line);
216
+ if (heading !== null) {
217
+ current = heading[1];
218
+ continue;
219
+ }
220
+ const declared = /^-\s*class:\s*(\S+)\s*$/i.exec(line);
221
+ if (declared !== null) {
222
+ if (current === undefined) {
223
+ return Effect.fail(new OperationsFileError({ line: index + 1, detail: "class: outside an operation" }));
224
+ }
225
+ const value = declared[1]?.toLowerCase();
226
+ if (value !== "read" && value !== "mutating" && value !== "unclassified") {
227
+ return Effect.fail(new OperationsFileError({
228
+ line: index + 1,
229
+ detail: `class of ${current} must be read, mutating, or unclassified`
230
+ }));
231
+ }
232
+ if (classes.has(current)) {
233
+ return Effect.fail(new OperationsFileError({ line: index + 1, detail: `${current} is classified twice` }));
234
+ }
235
+ classes.set(current, value);
236
+ }
237
+ }
238
+ if (confirmed === undefined) {
239
+ return Effect.fail(new OperationsFileError({ line: 1, detail: "missing Status: line" }));
240
+ }
241
+ return Effect.succeed({ confirmed, classes });
242
+ });
243
+ /** The class the tool acts on: only a confirmed file counts. */
244
+ export const effectiveClass = (file, operation) => file === undefined || !file.confirmed
245
+ ? "unclassified"
246
+ : (file.classes.get(operation) ?? "unclassified");
247
+ /** Catalog operations the file does not mention, and file entries the catalog lacks. */
248
+ export const operationsDrift = (file, catalog) => {
249
+ const names = new Set(catalog.operations.map((operation) => operation.name));
250
+ return {
251
+ missing: [...names].filter((name) => !file.classes.has(name)),
252
+ unknown: [...file.classes.keys()].filter((name) => !names.has(name))
253
+ };
254
+ };