@llm4ts/shell 2.15.0 → 2.16.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/kits/soap-ace/README.md
CHANGED
|
@@ -14,15 +14,56 @@ source) and shares nothing with this kit.
|
|
|
14
14
|
|
|
15
15
|
## Status
|
|
16
16
|
|
|
17
|
-
| Piece
|
|
18
|
-
|
|
|
19
|
-
| Strict XML reader
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
| `
|
|
17
|
+
| Piece | State |
|
|
18
|
+
| ------------------------------------------------------------------------ | ----- |
|
|
19
|
+
| Strict XML reader, WSDL 1.1/2.0 + XSD catalog | done |
|
|
20
|
+
| `soap-discover` flow and operation classification | done |
|
|
21
|
+
| Masking, auth profile, transport, sample authoring | done |
|
|
22
|
+
| Analysis, REST design, OpenAPI projection | done |
|
|
23
|
+
| `ace12-rest` pack, scaffold, `soap-epic` | done |
|
|
24
|
+
| `soap-explore`: the discovery half in one command | done |
|
|
25
|
+
| `demo-bank-soap` fixture ([runbook](fixtures/demo-bank-soap/RUNBOOK.md)) | done |
|
|
26
|
+
|
|
27
|
+
## Quick start: soap-explore
|
|
28
|
+
|
|
29
|
+
One command from a WSDL to a design draft, the way SoapUI turns a WSDL into
|
|
30
|
+
a project. It stops only where a person decides, and a rerun continues where
|
|
31
|
+
it stopped:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
llm4ts run soap-explore --repo ~/work/conti-analysis ./wsdl/ContiService.wsdl # or an https URL
|
|
35
|
+
# write .llm4ts/soap/<service>/auth.json: at least {"environment": "test"}
|
|
36
|
+
# review operations.md, set Status: confirmed
|
|
37
|
+
llm4ts run soap-explore --repo ~/work/conti-analysis # continue
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Step | What happens |
|
|
41
|
+
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| discover | `catalog.json`/`.md`, `operations.md` with proposed classes (as `soap-discover`) |
|
|
43
|
+
| requests | `samples/<op>/happy-path.request.yaml` for **every** operation, example values from the XSD |
|
|
44
|
+
| probe | calls the **read** operations once, producers first: the IBAN `cercaConti` returns is what `dettaglioConto` is called with. Live values stay in memory; the request file on disk carries the masked one. Mutating: never called |
|
|
45
|
+
| evidence | `analysis/<op>.md`, `design/mapping.json`/`.md` |
|
|
46
|
+
| design | the reasoning seat drafts `design/api-design.md`, `check`, one `revise` round when errors remain; an existing design is only checked; `--no-design` skips |
|
|
47
|
+
| report | `explore.md`: per operation the probe decision and result, the chained values, the design state, and the next command for every gap |
|
|
48
|
+
|
|
49
|
+
**Who may be called.** Without `auth.json` nothing is called (explore never
|
|
50
|
+
writes it: the environment label is yours to declare). With `operations.md`
|
|
51
|
+
confirmed, its `read` operations are called. Before confirmation, an
|
|
52
|
+
operation is called only in dev or test, and only when the file still lists
|
|
53
|
+
it as `read`, the name heuristic says `read`, and the judgment seat
|
|
54
|
+
(`LLM4TS_JUDGMENT_PROVIDER`) answers `read` with an `act` decision; without
|
|
55
|
+
a judgment seat, confirm first. uat always needs confirmation.
|
|
56
|
+
|
|
57
|
+
**Files it owns.** A request explore writes starts with a `# soap-explore:
|
|
58
|
+
generated` line and is regenerated when probed again. Delete that line to
|
|
59
|
+
own the file: explore then sends it as written and never touches it.
|
|
60
|
+
`--refresh` calls the probed operations again; everything else is kept.
|
|
61
|
+
|
|
62
|
+
Approvals stay human: `Status: confirmed` in `operations.md`,
|
|
63
|
+
`Status: approved` in `api-design.md`. Then `soap-design "openapi"` and
|
|
64
|
+
`soap-epic` as below. The individual flows remain for everything past the
|
|
65
|
+
first pass: more scenarios per operation (`soap-sample "propose <op>"`),
|
|
66
|
+
mutating calls, SoapUI import, design revisions.
|
|
26
67
|
|
|
27
68
|
## soap-discover
|
|
28
69
|
|
|
@@ -16,6 +16,22 @@ cd ~/work/conti-analysis
|
|
|
16
16
|
The analysis repository holds `.llm4ts/soap/<service>/`: catalog, samples,
|
|
17
17
|
analysis, design. The ACE repository receives the contracts and the code.
|
|
18
18
|
|
|
19
|
+
## Fast path: soap-explore
|
|
20
|
+
|
|
21
|
+
Steps 1 to 4 in one command, rerun after each decision:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
llm4ts run soap-explore --repo . ./wsdl/ContiService.wsdl # discover, requests, report
|
|
25
|
+
# write auth.json (step 2), review and confirm operations.md
|
|
26
|
+
llm4ts run soap-explore --repo . # probe reads, analyse, draft the design
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Read `.llm4ts/soap/<service>/explore.md` after each run: it lists what was
|
|
30
|
+
called, what was not and why, and the next command. On the fixture:
|
|
31
|
+
`LLM4TS_SOAP_STUB=<kit>/fixtures/demo-bank-soap/responses`. The steps below
|
|
32
|
+
are the same work one command at a time, and what to use past the first
|
|
33
|
+
pass.
|
|
34
|
+
|
|
19
35
|
## 1. Discover
|
|
20
36
|
|
|
21
37
|
```bash
|
|
@@ -69,7 +69,7 @@ export const callOperation = (options) => Effect.gen(function* () {
|
|
|
69
69
|
detail: `no auth profile; write ${`.llm4ts/soap/${service}/auth.json`} with at least {"environment": "dev"}`
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
-
const request = yield* readRequestFile(workspace, catalog, service, operation.name, name);
|
|
72
|
+
const request = options.request ?? (yield* readRequestFile(workspace, catalog, service, operation.name, name));
|
|
73
73
|
const issues = [
|
|
74
74
|
...request.readIssues,
|
|
75
75
|
...validateInstance(catalog, input, request.body)
|
|
@@ -130,6 +130,7 @@ export const callOperation = (options) => Effect.gen(function* () {
|
|
|
130
130
|
// which quote values) is read from the masked, security-stripped tree:
|
|
131
131
|
// nothing unmasked reaches the exchange.
|
|
132
132
|
let fault;
|
|
133
|
+
let payload;
|
|
133
134
|
if (parsedResponse._tag === "None") {
|
|
134
135
|
responseIssues.push({
|
|
135
136
|
path: "(response)",
|
|
@@ -148,11 +149,14 @@ export const callOperation = (options) => Effect.gen(function* () {
|
|
|
148
149
|
}
|
|
149
150
|
else if (operation.output !== undefined) {
|
|
150
151
|
const output = elementByName(catalog, operation.output);
|
|
151
|
-
const
|
|
152
|
-
if (output !== undefined &&
|
|
153
|
-
responseIssues.push(...instanceFromXml(catalog, output,
|
|
152
|
+
const masked = read.value.payload;
|
|
153
|
+
if (output !== undefined && masked !== undefined) {
|
|
154
|
+
responseIssues.push(...instanceFromXml(catalog, output, masked).issues);
|
|
155
|
+
const live = yield* readEnvelope(stripSecurity(parsedResponse.value)).pipe(Effect.option);
|
|
156
|
+
if (live._tag === "Some" && live.value.payload !== undefined)
|
|
157
|
+
payload = { live: live.value.payload, masked };
|
|
154
158
|
}
|
|
155
|
-
else if (
|
|
159
|
+
else if (masked === undefined) {
|
|
156
160
|
responseIssues.push({ path: "(response)", detail: "empty Body" });
|
|
157
161
|
}
|
|
158
162
|
}
|
|
@@ -182,5 +186,5 @@ export const callOperation = (options) => Effect.gen(function* () {
|
|
|
182
186
|
masking: mergeReports([maskedRequest.report, maskedResponse.report])
|
|
183
187
|
});
|
|
184
188
|
const path = yield* writeExchange(workspace, service, exchange);
|
|
185
|
-
return { exchange, path, permission };
|
|
189
|
+
return { exchange, path, permission, ...(payload === undefined ? {} : { payload }) };
|
|
186
190
|
});
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { schemaPaths, writeAnalyses } from "./Analysis.js";
|
|
3
|
+
import { callOperation, RequestInvalid } from "./Call.js";
|
|
4
|
+
import { elementByName } from "./Catalog.js";
|
|
5
|
+
import { heuristicClass } from "./Classification.js";
|
|
6
|
+
import { blockingIssues, checkDesign, designPaths, parseDesignFile, renderDesignFile } from "./Design.js";
|
|
7
|
+
import { proposeDesign, reviseDesign } from "./DesignProposal.js";
|
|
8
|
+
import { readIfPresent, serviceDirectory, servicePaths } from "./Discover.js";
|
|
9
|
+
import { instanceFromXml } from "./Instance.js";
|
|
10
|
+
import { encodeMapping, mapService, renderMapping } from "./Mapping.js";
|
|
11
|
+
import { collectSeeds, readExchanges, renderRequestFile, samplePaths, seedsForService } from "./Samples.js";
|
|
12
|
+
import { isYamlMap, parseYaml } from "./Yaml.js";
|
|
13
|
+
// soap-explore: the whole discovery half in one pass, stopping only where a
|
|
14
|
+
// person decides. Given a catalog it writes a request for every operation,
|
|
15
|
+
// calls the read operations once in producer-first order (values from one
|
|
16
|
+
// response seed the next request), and reports every gap with the command
|
|
17
|
+
// that closes it. Each step keeps what exists, so a rerun continues.
|
|
18
|
+
//
|
|
19
|
+
// Chained values are live only in memory: the call sends the value the
|
|
20
|
+
// service answered with, the request file on disk carries its masked form.
|
|
21
|
+
export const exploreRequestName = "happy-path";
|
|
22
|
+
/** First line of a request file soap-explore owns; deleting it hands the file to the user. */
|
|
23
|
+
export const exploreMarker = "# soap-explore: generated. Delete this line to keep your edits; soap-explore then leaves the file alone.";
|
|
24
|
+
export const explorePaths = (service) => ({
|
|
25
|
+
report: `${serviceDirectory(service)}/explore.md`
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Which operations the probe may call. Only reads, ever. A confirmed
|
|
29
|
+
* operations.md decides alone; before confirmation, a provisional probe needs
|
|
30
|
+
* dev or test, the file still saying read, the name heuristic saying read,
|
|
31
|
+
* and the judgment saying read with an `act` decision. `judged` undefined
|
|
32
|
+
* means no judgment seat is configured.
|
|
33
|
+
*/
|
|
34
|
+
export const probeDecisions = (options) => options.catalog.operations.map(({ name }) => {
|
|
35
|
+
const skip = (reason) => ({ operation: name, probe: false, reason });
|
|
36
|
+
if (options.environment === undefined)
|
|
37
|
+
return skip("no auth.json: nothing is called");
|
|
38
|
+
const file = options.operations;
|
|
39
|
+
const listed = file?.classes.get(name) ?? "unclassified";
|
|
40
|
+
if (file?.confirmed === true) {
|
|
41
|
+
if (listed === "read") {
|
|
42
|
+
return { operation: name, probe: true, basis: "confirmed", reason: "confirmed read" };
|
|
43
|
+
}
|
|
44
|
+
return skip(listed === "mutating"
|
|
45
|
+
? "mutating: never probed"
|
|
46
|
+
: "unclassified in operations.md: not callable");
|
|
47
|
+
}
|
|
48
|
+
if (options.environment === "uat")
|
|
49
|
+
return skip("uat: confirm operations.md before any call");
|
|
50
|
+
if (listed !== "read") {
|
|
51
|
+
return skip(listed === "mutating" ? "mutating: never probed" : "unclassified: confirm operations.md");
|
|
52
|
+
}
|
|
53
|
+
const heuristic = heuristicClass(name);
|
|
54
|
+
if (heuristic.class !== "read") {
|
|
55
|
+
return skip(`heuristic says ${heuristic.class}: confirm operations.md to call it`);
|
|
56
|
+
}
|
|
57
|
+
if (options.judged === undefined) {
|
|
58
|
+
return skip("no judgment seat (LLM4TS_JUDGMENT_PROVIDER): confirm operations.md to call it");
|
|
59
|
+
}
|
|
60
|
+
const judged = options.judged.get(name);
|
|
61
|
+
if (judged === undefined)
|
|
62
|
+
return skip("the judgment did not answer: confirm operations.md");
|
|
63
|
+
if (judged.class !== "read" || judged.decision !== "act") {
|
|
64
|
+
return skip(`judgment says ${judged.class} ${judged.confidence.toFixed(2)} (${judged.decision}): confirm operations.md`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
operation: name,
|
|
68
|
+
probe: true,
|
|
69
|
+
basis: "provisional",
|
|
70
|
+
reason: `provisional read: heuristic and judgment agree (${judged.confidence.toFixed(2)})`
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Producer-first order
|
|
75
|
+
const fieldName = (path) => (path.split(".").pop() ?? path).replace(/\[\]$/, "").replace(/^@/, "");
|
|
76
|
+
/** Leaf field names of an element; `requiredOnly` drops optional ones and those under optional parents. */
|
|
77
|
+
export const leafNames = (catalog, element, requiredOnly) => {
|
|
78
|
+
const declared = element === undefined ? undefined : elementByName(catalog, element);
|
|
79
|
+
if (declared === undefined)
|
|
80
|
+
return new Set();
|
|
81
|
+
const paths = schemaPaths(catalog, declared);
|
|
82
|
+
const optional = paths.filter((path) => path.optional).map((path) => path.path);
|
|
83
|
+
return new Set(paths
|
|
84
|
+
.filter((path) => path.leaf &&
|
|
85
|
+
(!requiredOnly ||
|
|
86
|
+
(!path.optional &&
|
|
87
|
+
!optional.some((prefix) => path.path.startsWith(`${prefix}.`) || path.path.startsWith(`${prefix}@`)))))
|
|
88
|
+
.map((path) => fieldName(path.path)));
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Order `names` so producers run first: A precedes B when a leaf of A's
|
|
92
|
+
* response is a required input field of B. Kahn's algorithm; ties and cycles
|
|
93
|
+
* resolve in catalog order.
|
|
94
|
+
*/
|
|
95
|
+
export const probeOrder = (catalog, names) => {
|
|
96
|
+
const operations = catalog.operations.filter((operation) => names.includes(operation.name));
|
|
97
|
+
const producers = new Map();
|
|
98
|
+
for (const consumer of operations) {
|
|
99
|
+
const needs = leafNames(catalog, consumer.input, true);
|
|
100
|
+
const links = [];
|
|
101
|
+
for (const producer of operations) {
|
|
102
|
+
if (producer === consumer)
|
|
103
|
+
continue;
|
|
104
|
+
const gives = leafNames(catalog, producer.output, false);
|
|
105
|
+
const fields = [...needs].filter((field) => gives.has(field));
|
|
106
|
+
if (fields.length > 0)
|
|
107
|
+
links.push({ producer: producer.name, fields });
|
|
108
|
+
}
|
|
109
|
+
producers.set(consumer.name, links);
|
|
110
|
+
}
|
|
111
|
+
const order = [];
|
|
112
|
+
const remaining = operations.map((operation) => operation.name);
|
|
113
|
+
while (remaining.length > 0) {
|
|
114
|
+
const ready = remaining.find((name) => (producers.get(name) ?? []).every((link) => !remaining.includes(link.producer))) ?? remaining[0];
|
|
115
|
+
if (ready === undefined)
|
|
116
|
+
break;
|
|
117
|
+
order.push(ready);
|
|
118
|
+
remaining.splice(remaining.indexOf(ready), 1);
|
|
119
|
+
}
|
|
120
|
+
return { order, producers };
|
|
121
|
+
};
|
|
122
|
+
/** First value per field name in a response payload. */
|
|
123
|
+
export const payloadSeeds = (catalog, operation, payload) => {
|
|
124
|
+
const output = operation.output === undefined ? undefined : elementByName(catalog, operation.output);
|
|
125
|
+
if (output === undefined)
|
|
126
|
+
return new Map();
|
|
127
|
+
const seeds = new Map();
|
|
128
|
+
for (const [key, value] of collectSeeds([instanceFromXml(catalog, output, payload).value])) {
|
|
129
|
+
if (key === "#text")
|
|
130
|
+
continue;
|
|
131
|
+
const name = key.replace(/^@/, "");
|
|
132
|
+
if (!seeds.has(name))
|
|
133
|
+
seeds.set(name, value);
|
|
134
|
+
}
|
|
135
|
+
return seeds;
|
|
136
|
+
};
|
|
137
|
+
const describeExchange = (exchange) => exchange.fault !== undefined
|
|
138
|
+
? `fault ${exchange.fault.code} ${exchange.fault.reason}`
|
|
139
|
+
: `HTTP ${exchange.response?.status ?? "?"}`;
|
|
140
|
+
const withSeeds = (base, chained, fields) => {
|
|
141
|
+
const seeds = new Map(base);
|
|
142
|
+
for (const field of fields) {
|
|
143
|
+
const value = chained.get(field);
|
|
144
|
+
if (value !== undefined)
|
|
145
|
+
seeds.set(field, value);
|
|
146
|
+
}
|
|
147
|
+
return seeds;
|
|
148
|
+
};
|
|
149
|
+
export const exploreService = (options) => Effect.gen(function* () {
|
|
150
|
+
const { workspace, catalog, service } = options;
|
|
151
|
+
const progress = options.progress ?? (() => Effect.void);
|
|
152
|
+
const decisions = probeDecisions({
|
|
153
|
+
catalog,
|
|
154
|
+
operations: options.operations,
|
|
155
|
+
environment: options.profile?.environment,
|
|
156
|
+
judged: options.judged
|
|
157
|
+
});
|
|
158
|
+
const probed = decisions.filter((decision) => decision.probe).map((d) => d.operation);
|
|
159
|
+
const plan = probeOrder(catalog, probed);
|
|
160
|
+
// The probe calls only operations it decided on, as confirmed reads.
|
|
161
|
+
const probeFile = {
|
|
162
|
+
confirmed: true,
|
|
163
|
+
classes: new Map(probed.map((name) => [name, "read"]))
|
|
164
|
+
};
|
|
165
|
+
const serviceSeeds = yield* seedsForService(workspace, catalog, service);
|
|
166
|
+
const live = new Map();
|
|
167
|
+
const masked = new Map();
|
|
168
|
+
// Which operation's response supplied each chained field.
|
|
169
|
+
const suppliedBy = new Map();
|
|
170
|
+
const reports = new Map();
|
|
171
|
+
/**
|
|
172
|
+
* Write the request when absent; regenerate one explore owns only before
|
|
173
|
+
* calling it. A file the user owns (no marker line, or the XML form) is
|
|
174
|
+
* never touched.
|
|
175
|
+
*/
|
|
176
|
+
const ensureRequest = (operation, chainedFields, notes, regenerate) => Effect.gen(function* () {
|
|
177
|
+
const paths = samplePaths(service, operation.name, exploreRequestName);
|
|
178
|
+
const yaml = yield* readIfPresent(workspace, paths.yaml);
|
|
179
|
+
const xml = yield* readIfPresent(workspace, paths.xml);
|
|
180
|
+
if (xml !== undefined ||
|
|
181
|
+
(yaml !== undefined && (!regenerate || !yaml.startsWith(exploreMarker)))) {
|
|
182
|
+
return "kept";
|
|
183
|
+
}
|
|
184
|
+
const text = renderRequestFile({
|
|
185
|
+
catalog,
|
|
186
|
+
operation: operation.name,
|
|
187
|
+
purpose: "happy path",
|
|
188
|
+
seeds: withSeeds(serviceSeeds, masked, chainedFields)
|
|
189
|
+
});
|
|
190
|
+
yield* workspace.write(paths.yaml, [exploreMarker, ...notes, text].join("\n"));
|
|
191
|
+
return yaml === undefined ? "written" : "regenerated";
|
|
192
|
+
});
|
|
193
|
+
for (const name of plan.order) {
|
|
194
|
+
const operation = catalog.operations.find((candidate) => candidate.name === name);
|
|
195
|
+
const decision = decisions.find((candidate) => candidate.operation === name);
|
|
196
|
+
if (operation === undefined || decision === undefined)
|
|
197
|
+
continue;
|
|
198
|
+
const listed = options.operations?.classes.get(name) ?? "unclassified";
|
|
199
|
+
const paths = samplePaths(service, name, exploreRequestName);
|
|
200
|
+
const recorded = yield* readIfPresent(workspace, paths.exchange);
|
|
201
|
+
const links = (plan.producers.get(name) ?? [])
|
|
202
|
+
.map((link) => ({
|
|
203
|
+
...link,
|
|
204
|
+
fields: link.fields.filter((field) => suppliedBy.get(field) === link.producer)
|
|
205
|
+
}))
|
|
206
|
+
.filter((link) => link.fields.length > 0);
|
|
207
|
+
const chainedFields = new Set(links.flatMap((link) => link.fields));
|
|
208
|
+
if (recorded !== undefined && !options.refresh) {
|
|
209
|
+
const request = yield* ensureRequest(operation, new Set(), [], false);
|
|
210
|
+
reports.set(name, {
|
|
211
|
+
operation: name,
|
|
212
|
+
listed,
|
|
213
|
+
decision,
|
|
214
|
+
request,
|
|
215
|
+
outcome: { kind: "recorded", detail: "recorded earlier; --refresh calls it again" },
|
|
216
|
+
chained: []
|
|
217
|
+
});
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const notes = links.length === 0
|
|
221
|
+
? []
|
|
222
|
+
: [
|
|
223
|
+
`# Chained at probe time: ${links.map((link) => `${link.fields.join(", ")} from ${link.producer}`).join("; ")}.`,
|
|
224
|
+
"# Personal values appear masked here; soap-explore sent the live ones and never stores them."
|
|
225
|
+
];
|
|
226
|
+
const request = yield* ensureRequest(operation, chainedFields, notes, true);
|
|
227
|
+
// A file explore owns is sent with the live chained values; the file on
|
|
228
|
+
// disk keeps the masked ones. A user's file is sent as written.
|
|
229
|
+
const override = request === "kept" || chainedFields.size === 0
|
|
230
|
+
? undefined
|
|
231
|
+
: yield* parseYaml(renderRequestFile({
|
|
232
|
+
catalog,
|
|
233
|
+
operation: name,
|
|
234
|
+
purpose: "happy path",
|
|
235
|
+
seeds: withSeeds(serviceSeeds, live, chainedFields)
|
|
236
|
+
})).pipe(Effect.option);
|
|
237
|
+
const body = override !== undefined && override._tag === "Some" && isYamlMap(override.value)
|
|
238
|
+
? override.value["body"]
|
|
239
|
+
: undefined;
|
|
240
|
+
const outcome = yield* callOperation({
|
|
241
|
+
workspace,
|
|
242
|
+
catalog,
|
|
243
|
+
service,
|
|
244
|
+
operation: name,
|
|
245
|
+
name: exploreRequestName,
|
|
246
|
+
profile: options.profile,
|
|
247
|
+
operations: probeFile,
|
|
248
|
+
secrets: options.secrets,
|
|
249
|
+
transport: options.transport,
|
|
250
|
+
allowMutating: undefined,
|
|
251
|
+
confirm: () => Effect.succeed(false),
|
|
252
|
+
...(body === undefined
|
|
253
|
+
? {}
|
|
254
|
+
: { request: { purpose: "happy path", body, form: "yaml", readIssues: [] } }),
|
|
255
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
256
|
+
...(options.random === undefined ? {} : { random: options.random })
|
|
257
|
+
}).pipe(Effect.result);
|
|
258
|
+
let result;
|
|
259
|
+
if (outcome._tag === "Failure") {
|
|
260
|
+
// A request that did not validate may carry live chained values in
|
|
261
|
+
// its issue text: report where, never what.
|
|
262
|
+
const error = outcome.failure;
|
|
263
|
+
result = {
|
|
264
|
+
kind: "failed",
|
|
265
|
+
detail: error instanceof RequestInvalid
|
|
266
|
+
? `not sent, the request does not validate at ${error.issues.map((issue) => issue.path).join(", ")}`
|
|
267
|
+
: error.message
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const { exchange, payload } = outcome.success;
|
|
272
|
+
result = {
|
|
273
|
+
kind: "called",
|
|
274
|
+
detail: `${describeExchange(exchange)}, ${exchange.response?.elapsedMs ?? 0} ms`,
|
|
275
|
+
findings: exchange.responseIssues.length
|
|
276
|
+
};
|
|
277
|
+
if (payload !== undefined) {
|
|
278
|
+
for (const [field, value] of payloadSeeds(catalog, operation, payload.live)) {
|
|
279
|
+
if (!live.has(field)) {
|
|
280
|
+
live.set(field, value);
|
|
281
|
+
suppliedBy.set(field, name);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const [field, value] of payloadSeeds(catalog, operation, payload.masked)) {
|
|
285
|
+
if (!masked.has(field))
|
|
286
|
+
masked.set(field, value);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
yield* progress(`${name}: ${result.detail}${links.length === 0 ? "" : ` (chained ${[...chainedFields].join(", ")})`}`);
|
|
291
|
+
reports.set(name, {
|
|
292
|
+
operation: name,
|
|
293
|
+
listed,
|
|
294
|
+
decision,
|
|
295
|
+
request,
|
|
296
|
+
outcome: result,
|
|
297
|
+
chained: links
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
// Every other operation gets a request to edit, seeded with what the
|
|
301
|
+
// probe learnt (masked), and the reason it was not called.
|
|
302
|
+
for (const operation of catalog.operations) {
|
|
303
|
+
if (reports.has(operation.name))
|
|
304
|
+
continue;
|
|
305
|
+
const decision = decisions.find((candidate) => candidate.operation === operation.name);
|
|
306
|
+
if (decision === undefined)
|
|
307
|
+
continue;
|
|
308
|
+
const needs = leafNames(catalog, operation.input, true);
|
|
309
|
+
const request = yield* ensureRequest(operation, new Set([...needs].filter((field) => masked.has(field))), [], false);
|
|
310
|
+
const recorded = yield* readIfPresent(workspace, samplePaths(service, operation.name, exploreRequestName).exchange);
|
|
311
|
+
reports.set(operation.name, {
|
|
312
|
+
operation: operation.name,
|
|
313
|
+
listed: options.operations?.classes.get(operation.name) ?? "unclassified",
|
|
314
|
+
decision,
|
|
315
|
+
request,
|
|
316
|
+
outcome: recorded === undefined
|
|
317
|
+
? { kind: "not-called", detail: decision.reason }
|
|
318
|
+
: { kind: "recorded", detail: "recorded earlier" },
|
|
319
|
+
chained: []
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
decisions,
|
|
324
|
+
plan,
|
|
325
|
+
operations: catalog.operations.flatMap((operation) => {
|
|
326
|
+
const report = reports.get(operation.name);
|
|
327
|
+
return report === undefined ? [] : [report];
|
|
328
|
+
})
|
|
329
|
+
};
|
|
330
|
+
});
|
|
331
|
+
/** analysis/<op>.md for every operation, then design/mapping.json + .md. */
|
|
332
|
+
export const writeEvidence = (workspace, catalog, service) => Effect.gen(function* () {
|
|
333
|
+
const analyses = yield* writeAnalyses(workspace, catalog, service);
|
|
334
|
+
const mapping = mapService(catalog);
|
|
335
|
+
const paths = designPaths(serviceDirectory(service));
|
|
336
|
+
yield* workspace.write(paths.mapping, encodeMapping(mapping));
|
|
337
|
+
yield* workspace.write(paths.mappingSummary, renderMapping(mapping));
|
|
338
|
+
const exchanges = (yield* readExchanges(workspace, service)).length;
|
|
339
|
+
return { analyses, exchanges };
|
|
340
|
+
});
|
|
341
|
+
/**
|
|
342
|
+
* The design step: an existing api-design.md is only checked; otherwise,
|
|
343
|
+
* with evidence and a reasoning seat, draft it, check it, and revise once
|
|
344
|
+
* when errors remain. The file is always written as proposed.
|
|
345
|
+
*/
|
|
346
|
+
export const designStep = (options) => Effect.gen(function* () {
|
|
347
|
+
const { workspace, catalog, operations } = options;
|
|
348
|
+
const analyses = options.evidence.analyses;
|
|
349
|
+
const paths = designPaths(serviceDirectory(options.service));
|
|
350
|
+
const existing = yield* readIfPresent(workspace, paths.design);
|
|
351
|
+
if (existing !== undefined) {
|
|
352
|
+
const file = yield* parseDesignFile(existing).pipe(Effect.result);
|
|
353
|
+
if (file._tag === "Failure")
|
|
354
|
+
return { kind: "failed", detail: file.failure.message };
|
|
355
|
+
const issues = checkDesign(file.success.design, { catalog, operations, analyses });
|
|
356
|
+
yield* workspace.write(paths.design, renderDesignFile(file.success.design, issues, file.success.approved ? "approved" : "proposed"));
|
|
357
|
+
const errors = blockingIssues(issues).length;
|
|
358
|
+
return {
|
|
359
|
+
kind: "checked",
|
|
360
|
+
approved: file.success.approved,
|
|
361
|
+
errors,
|
|
362
|
+
warnings: issues.length - errors
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if ("off" in options.reasoning)
|
|
366
|
+
return { kind: "skipped", reason: options.reasoning.off };
|
|
367
|
+
if (options.evidence.exchanges === 0) {
|
|
368
|
+
return { kind: "skipped", reason: "no exchanges yet: the design needs evidence" };
|
|
369
|
+
}
|
|
370
|
+
const reasoning = options.reasoning;
|
|
371
|
+
const base = {
|
|
372
|
+
catalog,
|
|
373
|
+
operations,
|
|
374
|
+
mapping: mapService(catalog),
|
|
375
|
+
analyses,
|
|
376
|
+
style: options.style
|
|
377
|
+
};
|
|
378
|
+
const drafted = yield* proposeDesign(reasoning, base).pipe(Effect.result);
|
|
379
|
+
if (drafted._tag === "Failure") {
|
|
380
|
+
return { kind: "failed", detail: `drafting failed: ${drafted.failure.message}` };
|
|
381
|
+
}
|
|
382
|
+
let design = drafted.success;
|
|
383
|
+
let issues = checkDesign(design, { catalog, operations, analyses });
|
|
384
|
+
let kind = "drafted";
|
|
385
|
+
if (blockingIssues(issues).length > 0) {
|
|
386
|
+
const revised = yield* reviseDesign(reasoning, { ...base, current: design, issues }).pipe(Effect.result);
|
|
387
|
+
if (revised._tag === "Success") {
|
|
388
|
+
design = revised.success;
|
|
389
|
+
issues = checkDesign(design, { catalog, operations, analyses });
|
|
390
|
+
kind = "revised";
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
yield* workspace.write(paths.design, renderDesignFile(design, issues, "proposed"));
|
|
394
|
+
const errors = blockingIssues(issues).length;
|
|
395
|
+
return { kind, approved: false, errors, warnings: issues.length - errors };
|
|
396
|
+
});
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Report
|
|
399
|
+
const outcomeText = (outcome) => outcome.kind === "called"
|
|
400
|
+
? `called: ${outcome.detail}${outcome.findings === 0 ? "" : `, ${outcome.findings} schema findings`}`
|
|
401
|
+
: `${outcome.kind}: ${outcome.detail}`;
|
|
402
|
+
const cell = (text) => text.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
403
|
+
export const renderExploreReport = (options) => {
|
|
404
|
+
const { service, catalog, result, design } = options;
|
|
405
|
+
const paths = servicePaths(service);
|
|
406
|
+
const lines = [
|
|
407
|
+
`# Explore: ${service}`,
|
|
408
|
+
"",
|
|
409
|
+
"Regenerated by every `soap-explore` run. Every value in the files it points at is masked.",
|
|
410
|
+
"",
|
|
411
|
+
`- operations: ${catalog.operations.length}, open questions: ${catalog.openQuestions.length} (see catalog.md)`,
|
|
412
|
+
`- environment: ${options.profile?.environment ?? "none (no auth.json)"}`,
|
|
413
|
+
`- operations.md: ${options.operations === undefined ? "missing" : options.operations.confirmed ? "confirmed" : "proposed"}`,
|
|
414
|
+
`- exchanges recorded: ${options.evidence.exchanges}`,
|
|
415
|
+
"",
|
|
416
|
+
"## Operations",
|
|
417
|
+
"",
|
|
418
|
+
"| Operation | Class | Probe | Request | Result |",
|
|
419
|
+
"| --- | --- | --- | --- | --- |"
|
|
420
|
+
];
|
|
421
|
+
for (const report of result.operations) {
|
|
422
|
+
const request = `samples/${report.operation}/${exploreRequestName} (${report.request})`;
|
|
423
|
+
lines.push(`| ${cell(report.operation)} | ${report.listed}${options.operations?.confirmed === true ? "" : " (proposed)"} | ${report.decision.probe ? `yes, ${report.decision.basis ?? ""}` : "no"} | ${cell(request)} | ${cell(report.decision.probe ? outcomeText(report.outcome) : report.outcome.kind === "recorded" ? outcomeText(report.outcome) : `not called: ${report.decision.reason}`)} |`);
|
|
424
|
+
}
|
|
425
|
+
const chained = result.operations.filter((report) => report.chained.length > 0);
|
|
426
|
+
if (chained.length > 0) {
|
|
427
|
+
lines.push("", "## Chained values", "");
|
|
428
|
+
for (const report of chained) {
|
|
429
|
+
lines.push(`- ${report.operation} ← ${report.chained.map((link) => `${link.producer} (${link.fields.join(", ")})`).join("; ")}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
lines.push("", "## Design", "");
|
|
433
|
+
switch (design.kind) {
|
|
434
|
+
case "skipped":
|
|
435
|
+
lines.push(`Not drafted: ${design.reason}.`);
|
|
436
|
+
break;
|
|
437
|
+
case "failed":
|
|
438
|
+
lines.push(`Not drafted: ${design.detail}`);
|
|
439
|
+
break;
|
|
440
|
+
default:
|
|
441
|
+
lines.push(`design/api-design.md ${design.kind}: ${design.approved ? "approved" : "proposed"}, ${design.errors} errors, ${design.warnings} warnings.`);
|
|
442
|
+
}
|
|
443
|
+
const next = [];
|
|
444
|
+
if (options.profile === undefined) {
|
|
445
|
+
next.push(`Write \`${paths.auth}\` to enable calls, at least \`{"environment": "test"}\` (dev, test, or uat; add "endpoint" when the WSDL names another environment, and auth by env:/file: reference). Then rerun soap-explore.`);
|
|
446
|
+
}
|
|
447
|
+
if (options.operations !== undefined && !options.operations.confirmed) {
|
|
448
|
+
next.push(`Review the classes in \`${paths.operations}\` and set \`Status: confirmed\`; then rerun soap-explore to probe every read operation.`);
|
|
449
|
+
}
|
|
450
|
+
for (const report of result.operations) {
|
|
451
|
+
if (report.outcome.kind === "failed") {
|
|
452
|
+
next.push(`${report.operation}: fix \`samples/${report.operation}/${exploreRequestName}.request.yaml\` (delete its first line to keep your edits), then \`soap-sample "call ${report.operation}/${exploreRequestName}"\`.`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const mutating = result.operations
|
|
456
|
+
.filter((report) => report.listed === "mutating" && report.outcome.kind === "not-called")
|
|
457
|
+
.map((report) => report.operation);
|
|
458
|
+
if (options.profile !== undefined && mutating.length > 0) {
|
|
459
|
+
next.push(`Mutating operations are never probed (${mutating.join(", ")}): edit a request, then \`soap-sample -- --allow-mutating <operation> "call <operation>/${exploreRequestName}"\` when a test call is acceptable.`);
|
|
460
|
+
}
|
|
461
|
+
if (options.evidence.exchanges > 0) {
|
|
462
|
+
next.push('More evidence per operation (empty result, business fault, paging): `soap-sample "propose <operation>"`, then `soap-sample "call <operation>"`, then rerun soap-explore.');
|
|
463
|
+
}
|
|
464
|
+
if (design.kind === "skipped" || design.kind === "failed") {
|
|
465
|
+
if (options.evidence.exchanges > 0)
|
|
466
|
+
next.push('Draft the design: `soap-design "design"`.');
|
|
467
|
+
}
|
|
468
|
+
else if (!design.approved) {
|
|
469
|
+
next.push('Review `design/api-design.md`, run `soap-design "check"` until it has no errors, set `Status: approved`, then `soap-design "openapi"` and `soap-epic`.');
|
|
470
|
+
}
|
|
471
|
+
else if (design.errors === 0) {
|
|
472
|
+
next.push('The design is approved: `soap-design "openapi"`, then `soap-epic -- --target <ace-repo> plan`.');
|
|
473
|
+
}
|
|
474
|
+
lines.push("", "## Next", "", ...next.map((line) => `- ${line}`), "");
|
|
475
|
+
return lines.join("\n");
|
|
476
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// One pass from a WSDL to a design draft: discover, a request for every operation, a read-only probe with values chained between calls, analyses, the 1:1 mapping, and a drafted REST design; stops only where a person decides.
|
|
2
|
+
//
|
|
3
|
+
// The task text is the WSDL location (path or http(s) URL), as for
|
|
4
|
+
// soap-discover; without one it continues from the discovered service:
|
|
5
|
+
//
|
|
6
|
+
// llm4ts run soap-explore --repo ~/work/conti-api ./wsdl/DemoBank.wsdl
|
|
7
|
+
// llm4ts run soap-explore --repo . # rerun: continue where it stopped
|
|
8
|
+
// llm4ts run soap-explore --repo . -- --refresh # call the probed operations again
|
|
9
|
+
//
|
|
10
|
+
// Steps, each keeping what already exists (so a rerun continues):
|
|
11
|
+
//
|
|
12
|
+
// 1. discover catalog.json/.md; operations.md with proposed classes
|
|
13
|
+
// 2. requests samples/<op>/happy-path.request.yaml for every operation
|
|
14
|
+
// 3. probe calls read operations only, producers first; values a
|
|
15
|
+
// response returns seed the next request (live in memory,
|
|
16
|
+
// masked on disk). Before operations.md is confirmed an
|
|
17
|
+
// operation is probed only in dev/test when the name
|
|
18
|
+
// heuristic and the judgment seat (LLM4TS_JUDGMENT_PROVIDER)
|
|
19
|
+
// both say read. Mutating operations are never called.
|
|
20
|
+
// 4. evidence analysis/<op>.md, design/mapping.json/.md
|
|
21
|
+
// 5. design the reasoning seat (LLM4TS_REASONER, default claude)
|
|
22
|
+
// drafts design/api-design.md, check, one revise round;
|
|
23
|
+
// an existing design is only checked. --no-design skips.
|
|
24
|
+
// 6. report explore.md: per operation what happened, and the next
|
|
25
|
+
// command for every gap
|
|
26
|
+
//
|
|
27
|
+
// Calls need auth.json (at least {"environment": "test"}); explore never
|
|
28
|
+
// writes it. Approval stays human: operations.md `Status: confirmed`,
|
|
29
|
+
// api-design.md `Status: approved`. LLM4TS_SOAP_SERVICE names the service
|
|
30
|
+
// (and the auth.json used to fetch a URL); LLM4TS_SOAP_STUB=<dir> answers
|
|
31
|
+
// calls from <dir>/<operation>.xml instead of the network.
|
|
32
|
+
import { readFile } from "node:fs/promises";
|
|
33
|
+
import { isAbsolute, resolve } from "node:path";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
35
|
+
import * as Effect from "effect/Effect";
|
|
36
|
+
import { FlowAborted, Info, makeNodeWorkspace, mock, resolveFlowInput, runFlowMain, runNode, ScriptUsage } from "@llm4ts/runner";
|
|
37
|
+
import { coderFor, judgmentConnectorFromEnvironment } from "@llm4ts/runner/Connectors";
|
|
38
|
+
import { nodeSecretSource, resolveSide } from "./lib/soap/Auth.js";
|
|
39
|
+
import { heuristicClass, judgeClass } from "./lib/soap/Classification.js";
|
|
40
|
+
import { discoverService, loadAuthProfile, loadCatalog, loadOperationsFile } from "./lib/soap/Discover.js";
|
|
41
|
+
import { designStep, explorePaths, exploreService, renderExploreReport, writeEvidence } from "./lib/soap/Explore.js";
|
|
42
|
+
import { makeDirectoryStubTransport } from "./lib/soap/Stub.js";
|
|
43
|
+
import { makeNodeSoapTransport, makeTransportDocumentLoader } from "./lib/soap/Transport.js";
|
|
44
|
+
import { DocumentLoader } from "./lib/soap/Wsdl.js";
|
|
45
|
+
const usage = 'usage: soap-explore [--refresh] [--no-design] "<wsdl path or URL>"';
|
|
46
|
+
const isUrl = (location) => /^https?:\/\//i.test(location);
|
|
47
|
+
const parseExploreFlags = (argv) => Effect.gen(function* () {
|
|
48
|
+
let refresh = false;
|
|
49
|
+
let design = true;
|
|
50
|
+
const rest = [];
|
|
51
|
+
for (const argument of argv) {
|
|
52
|
+
if (argument === "--refresh")
|
|
53
|
+
refresh = true;
|
|
54
|
+
else if (argument === "--no-design")
|
|
55
|
+
design = false;
|
|
56
|
+
else if (argument.startsWith("--refresh=") || argument.startsWith("--no-design="))
|
|
57
|
+
return yield* ScriptUsage.make({ message: `${argument} takes no value\n${usage}` });
|
|
58
|
+
else
|
|
59
|
+
rest.push(argument);
|
|
60
|
+
}
|
|
61
|
+
return { refresh, design, rest };
|
|
62
|
+
});
|
|
63
|
+
const styleGuide = () => Effect.tryPromise({
|
|
64
|
+
try: () => readFile(fileURLToPath(new URL("../packs/ace12-rest/api-style.md", import.meta.url)), "utf8"),
|
|
65
|
+
catch: () => FlowAborted.make({ message: "cannot read the kit's packs/ace12-rest/api-style.md" })
|
|
66
|
+
});
|
|
67
|
+
const program = Effect.gen(function* () {
|
|
68
|
+
const flags = yield* parseExploreFlags(process.argv.slice(2));
|
|
69
|
+
const input = yield* resolveFlowInput("", flags.rest);
|
|
70
|
+
const raw = input.prompt.trim();
|
|
71
|
+
const location = raw === "" ? undefined : isUrl(raw) || isAbsolute(raw) ? raw : resolve(input.workspace, raw);
|
|
72
|
+
const environment = process.env;
|
|
73
|
+
const requested = environment.LLM4TS_SOAP_SERVICE?.trim() || undefined;
|
|
74
|
+
const judgment = yield* judgmentConnectorFromEnvironment();
|
|
75
|
+
const reasonerName = (environment.LLM4TS_REASONER ?? "claude").trim() || "claude";
|
|
76
|
+
const reasoner = flags.design ? coderFor(reasonerName) : undefined;
|
|
77
|
+
if (flags.design && reasoner === undefined) {
|
|
78
|
+
return yield* ScriptUsage.make({ message: `unknown LLM4TS_REASONER '${reasonerName}'` });
|
|
79
|
+
}
|
|
80
|
+
yield* runNode({
|
|
81
|
+
workDir: input.workDir,
|
|
82
|
+
workspace: input.workspace,
|
|
83
|
+
userPrompt: input.prompt,
|
|
84
|
+
coder: mock,
|
|
85
|
+
...(reasoner === undefined ? {} : { reasoning: reasoner }),
|
|
86
|
+
...(judgment === undefined ? {} : { judgment }),
|
|
87
|
+
environment
|
|
88
|
+
}, (context) => Effect.gen(function* () {
|
|
89
|
+
const say = (message) => context.events.publish(Info.make({ message }));
|
|
90
|
+
const workspace = yield* makeNodeWorkspace(input.workDir);
|
|
91
|
+
const seat = judgment === undefined ? undefined : context.judgment;
|
|
92
|
+
// 1. discover (or continue from the discovered service)
|
|
93
|
+
let service;
|
|
94
|
+
let catalog;
|
|
95
|
+
let judged;
|
|
96
|
+
if (location !== undefined) {
|
|
97
|
+
const fetchProfile = requested === undefined ? undefined : yield* loadAuthProfile(workspace, requested);
|
|
98
|
+
const fetchSide = yield* resolveSide(nodeSecretSource(environment), fetchProfile?.fetch);
|
|
99
|
+
const discovered = yield* discoverService({
|
|
100
|
+
workspace,
|
|
101
|
+
location,
|
|
102
|
+
...(requested === undefined ? {} : { service: requested }),
|
|
103
|
+
...(seat === undefined ? {} : { judgment: seat })
|
|
104
|
+
}).pipe(Effect.provideService(DocumentLoader, makeTransportDocumentLoader(makeNodeSoapTransport(), fetchSide)));
|
|
105
|
+
service = discovered.service;
|
|
106
|
+
catalog = discovered.catalog;
|
|
107
|
+
// Classes judged at discovery are reused; otherwise judged below.
|
|
108
|
+
if (discovered.proposals !== undefined && seat !== undefined) {
|
|
109
|
+
judged = new Map(discovered.proposals.map((proposal) => [proposal.operation, proposal.judged]));
|
|
110
|
+
}
|
|
111
|
+
yield* say(`discovered ${service}: ${catalog.operations.length} operations, ${catalog.openQuestions.length} open questions`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const services = (yield* workspace.discover(".llm4ts/soap/*/catalog.json")).map((path) => path.split("/")[2] ?? "");
|
|
115
|
+
const picked = requested ?? (services.length === 1 ? services[0] : undefined);
|
|
116
|
+
if (picked === undefined) {
|
|
117
|
+
return yield* FlowAborted.make({
|
|
118
|
+
message: services.length === 0
|
|
119
|
+
? `nothing discovered here; give the WSDL\n${usage}`
|
|
120
|
+
: `several services (${services.join(", ")}); set LLM4TS_SOAP_SERVICE`
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
service = picked;
|
|
124
|
+
catalog = yield* loadCatalog(workspace, service);
|
|
125
|
+
yield* say(`continuing ${service}: ${catalog.operations.length} operations`);
|
|
126
|
+
}
|
|
127
|
+
const profile = yield* loadAuthProfile(workspace, service);
|
|
128
|
+
const operations = yield* loadOperationsFile(workspace, service);
|
|
129
|
+
// Provisional probing asks the judgment only where the listed class
|
|
130
|
+
// and the heuristic already say read.
|
|
131
|
+
if (seat !== undefined && judged === undefined && operations?.confirmed !== true) {
|
|
132
|
+
const candidates = catalog.operations.filter((operation) => operations?.classes.get(operation.name) === "read" &&
|
|
133
|
+
heuristicClass(operation.name).class === "read");
|
|
134
|
+
judged = new Map(yield* Effect.forEach(candidates, (operation) => Effect.map(judgeClass(seat, catalog, operation), (answer) => [operation.name, answer])));
|
|
135
|
+
}
|
|
136
|
+
// 2-3. requests and the probe
|
|
137
|
+
const stub = environment.LLM4TS_SOAP_STUB?.trim();
|
|
138
|
+
const transport = stub === undefined || stub === ""
|
|
139
|
+
? makeNodeSoapTransport()
|
|
140
|
+
: makeDirectoryStubTransport(resolve(input.workspace, stub), catalog);
|
|
141
|
+
if (stub !== undefined && stub !== "")
|
|
142
|
+
yield* say(`LLM4TS_SOAP_STUB: answering from ${stub}, not the network`);
|
|
143
|
+
const result = yield* exploreService({
|
|
144
|
+
workspace,
|
|
145
|
+
catalog,
|
|
146
|
+
service,
|
|
147
|
+
profile,
|
|
148
|
+
operations,
|
|
149
|
+
judged,
|
|
150
|
+
secrets: nodeSecretSource(environment),
|
|
151
|
+
transport,
|
|
152
|
+
refresh: flags.refresh,
|
|
153
|
+
progress: (line) => say(` ${line}`)
|
|
154
|
+
});
|
|
155
|
+
const probed = result.decisions.filter((decision) => decision.probe).length;
|
|
156
|
+
yield* say(`probe: ${probed} of ${catalog.operations.length} operations callable now${profile === undefined ? " (no auth.json)" : ""}`);
|
|
157
|
+
// 4. evidence
|
|
158
|
+
const evidence = yield* writeEvidence(workspace, catalog, service);
|
|
159
|
+
// 5. design
|
|
160
|
+
const design = yield* designStep({
|
|
161
|
+
workspace,
|
|
162
|
+
catalog,
|
|
163
|
+
service,
|
|
164
|
+
operations,
|
|
165
|
+
evidence,
|
|
166
|
+
reasoning: flags.design ? context.reasoning : { off: "--no-design" },
|
|
167
|
+
style: yield* styleGuide()
|
|
168
|
+
});
|
|
169
|
+
yield* say(design.kind === "skipped" || design.kind === "failed"
|
|
170
|
+
? `design: not drafted (${design.kind === "skipped" ? design.reason : design.detail})`
|
|
171
|
+
: `design: ${design.kind}, ${design.approved ? "approved" : "proposed"}, ${design.errors} errors, ${design.warnings} warnings`);
|
|
172
|
+
// 6. report
|
|
173
|
+
const report = renderExploreReport({
|
|
174
|
+
service,
|
|
175
|
+
catalog,
|
|
176
|
+
profile,
|
|
177
|
+
operations,
|
|
178
|
+
result,
|
|
179
|
+
design,
|
|
180
|
+
evidence
|
|
181
|
+
});
|
|
182
|
+
const path = explorePaths(service).report;
|
|
183
|
+
yield* workspace.write(path, report);
|
|
184
|
+
const next = report.split("## Next")[1]?.trim().split("\n") ?? [];
|
|
185
|
+
yield* say(`wrote ${path}; next:`);
|
|
186
|
+
for (const line of next)
|
|
187
|
+
yield* say(` ${line}`);
|
|
188
|
+
}));
|
|
189
|
+
});
|
|
190
|
+
runFlowMain(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llm4ts/shell",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@effect/platform-node": "4.0.0-rc.115",
|
|
54
54
|
"@effect/platform-node-shared": "4.0.0-rc.115",
|
|
55
|
-
"@llm4ts/core": "2.
|
|
56
|
-
"@llm4ts/
|
|
57
|
-
"@llm4ts/
|
|
55
|
+
"@llm4ts/core": "2.16.0",
|
|
56
|
+
"@llm4ts/flow": "2.16.0",
|
|
57
|
+
"@llm4ts/runner": "2.16.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"effect": "4.0.0-rc.115"
|