@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.
- package/flows/epic-stories.js +9 -3
- package/flows/lib/epic-stories.js +24 -4
- package/kits/soap-ace/README.md +313 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/RUNBOOK.md +102 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/design/api-design.md +620 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/cercaConti.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/cercaMovimenti.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/confermaBonifico.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/dettaglioConto.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/inserisciBonifico.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/revocaBonifico.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/soapui/DemoBank-soapui-project.xml +39 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/DemoBank.wsdl +186 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/LegacyRpc.wsdl +29 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/banking.xsd +10 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/bonifici.xsd +107 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/common.xsd +93 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/conti.xsd +71 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/movimenti.xsd +55 -0
- package/kits/soap-ace/flows/lib/soap/Analysis.js +508 -0
- package/kits/soap-ace/flows/lib/soap/Auth.js +179 -0
- package/kits/soap-ace/flows/lib/soap/Call.js +186 -0
- package/kits/soap-ace/flows/lib/soap/CallPolicy.js +50 -0
- package/kits/soap-ace/flows/lib/soap/Catalog.js +157 -0
- package/kits/soap-ace/flows/lib/soap/Classification.js +254 -0
- package/kits/soap-ace/flows/lib/soap/Design.js +497 -0
- package/kits/soap-ace/flows/lib/soap/DesignProposal.js +204 -0
- package/kits/soap-ace/flows/lib/soap/Discover.js +139 -0
- package/kits/soap-ace/flows/lib/soap/Envelope.js +132 -0
- package/kits/soap-ace/flows/lib/soap/Epic.js +239 -0
- package/kits/soap-ace/flows/lib/soap/Instance.js +675 -0
- package/kits/soap-ace/flows/lib/soap/Mapping.js +130 -0
- package/kits/soap-ace/flows/lib/soap/Masking.js +508 -0
- package/kits/soap-ace/flows/lib/soap/OpenApi.js +288 -0
- package/kits/soap-ace/flows/lib/soap/Samples.js +287 -0
- package/kits/soap-ace/flows/lib/soap/Scenarios.js +132 -0
- package/kits/soap-ace/flows/lib/soap/SoapUiImport.js +200 -0
- package/kits/soap-ace/flows/lib/soap/Stub.js +34 -0
- package/kits/soap-ace/flows/lib/soap/Transport.js +167 -0
- package/kits/soap-ace/flows/lib/soap/Wsdl.js +533 -0
- package/kits/soap-ace/flows/lib/soap/Xml.js +453 -0
- package/kits/soap-ace/flows/lib/soap/Xsd.js +471 -0
- package/kits/soap-ace/flows/lib/soap/Yaml.js +309 -0
- package/kits/soap-ace/flows/soap-design.js +167 -0
- package/kits/soap-ace/flows/soap-discover.js +85 -0
- package/kits/soap-ace/flows/soap-epic.js +204 -0
- package/kits/soap-ace/flows/soap-sample.js +265 -0
- package/kits/soap-ace/packs/ace12-rest/api-style.md +63 -0
- package/kits/soap-ace/packs/ace12-rest/pack.md +18 -0
- package/kits/soap-ace/packs/ace12-rest/prompts/implement.md +8 -0
- package/kits/soap-ace/packs/ace12-rest/reviewers/contract.md +5 -0
- package/kits/soap-ace/patterns/PAT-ACE-001-esito-to-problem.md +8 -0
- package/kits/soap-ace/patterns/PAT-ACE-002-fault-to-problem.md +8 -0
- package/kits/soap-ace/patterns/PAT-ACE-003-amounts-and-enums.md +8 -0
- package/kits/soap-ace/patterns/PAT-ACE-004-pagination.md +7 -0
- package/kits/soap-ace/patterns/PAT-ACE-005-nil-and-absence.md +7 -0
- package/kits/soap-ace/scaffolds/ace12-rest-api/CONTRIBUTING.md +56 -0
- package/kits/soap-ace/scaffolds/ace12-rest-api/README.md +16 -0
- package/kits/soap-ace/scaffolds/ace12-rest-api/scripts/ace-gates-container.sh +8 -0
- package/kits/soap-ace/scaffolds/ace12-rest-api/scripts/ace-gates.sh +41 -0
- 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
|
+
};
|