@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,239 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import { stableHash } from "@llm4ts/flow/Plan";
4
+ import { Story, StoryPlan, storyPlanViolations } from "@llm4ts/flow/StoryPlan";
5
+ import { ApiDesign } from "./Design.js";
6
+ import { encodeMapping, mapService } from "./Mapping.js";
7
+ import { projectOpenApi, renderTypedYaml } from "./OpenApi.js";
8
+ // From an approved design to an ACE 12 implementation plan. The story split
9
+ // is derived, not generated: a skeleton story owns the REST API project's
10
+ // descriptors and main flow, three foundation stories own the policy
11
+ // project, the shared library, and the test stubs, one story per resource
12
+ // owns that resource's broker schema folder and tests, and a contract-check
13
+ // story closes the epic. The plan is written where the engine's
14
+ // `epic-stories` flow looks for it, so that flow executes it unchanged
15
+ // (worktrees, gates, judge, board — ADR 0013); an existing plan wins.
16
+ /** `DemoBankService` → `DemoBankApi`; `LLM4TS_ACE_API` overrides. */
17
+ export const apiNameFor = (service) => {
18
+ const base = service.replace(/[^A-Za-z0-9]/g, "").replace(/(Service|Ws|WS|Soap)$/, "");
19
+ const name = `${base.charAt(0).toUpperCase()}${base.slice(1)}Api`;
20
+ return /^[A-Za-z]/.test(name) ? name : `Api${name}`;
21
+ };
22
+ /** `credit-transfers` → `creditTransfers`: a valid ESQL broker schema name. */
23
+ export const schemaFolder = (resource) => resource.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
24
+ /** Endpoints grouped by their first path segment (the resource). */
25
+ export const resourcesOf = (design) => {
26
+ const groups = new Map();
27
+ for (const endpoint of design.endpoints) {
28
+ const resource = endpoint.path.split("/")[1] ?? "root";
29
+ groups.set(resource, [...(groups.get(resource) ?? []), endpoint]);
30
+ }
31
+ return [...groups];
32
+ };
33
+ /** The epic id `epic-stories` derives from the epic text: slug of four words plus a hash. */
34
+ export const epicIdFor = (epic) => {
35
+ const slug = epic
36
+ .toLowerCase()
37
+ .replace(/[^a-z0-9]+/g, "-")
38
+ .replace(/^-+|-+$/g, "")
39
+ .split("-")
40
+ .filter((part) => part.length > 0)
41
+ .slice(0, 4)
42
+ .join("-");
43
+ return `${slug.length === 0 ? "epic" : slug}-${stableHash(epic).slice(0, 6)}`;
44
+ };
45
+ export const epicSentence = (design, api, service) => `Implement the ${api} REST API on IBM ACE 12: ${design.endpoints.length} endpoints of "${design.title}" over the ${service} SOAP service, from the approved contract in contracts/.`;
46
+ const endpointLine = (endpoint) => `${endpoint.method} ${endpoint.path} (${endpoint.operationId}) from ${endpoint.sources.join(" + ")}${endpoint.errors.length === 0 ? "" : `; errors ${endpoint.errors.map((error) => `${error.status} ${error.code} ← ${error.from.join("/")}`).join(", ")}`}`;
47
+ export const storyPlanFor = (options) => {
48
+ const { design, api, service } = options;
49
+ const lib = `${api}Lib`;
50
+ const policies = `${api}Policies`;
51
+ const tests = `${api}_Test`;
52
+ const resources = resourcesOf(design);
53
+ const contracts = ["contracts", "docs", "test-data", "CONTRIBUTING.md"];
54
+ const foundation = ["api-skeleton", "policies", "shared-lib", "backend-stub"];
55
+ const stories = [
56
+ Story.make({
57
+ id: "api-skeleton",
58
+ title: `${api} REST API project skeleton`,
59
+ description: [
60
+ `Create the ACE 12 REST API project ${api}/ from contracts/openapi-ace.yaml: .project, restapi.descriptor, application.descriptor, the imported OpenAPI document as ${api}/openapi.yaml, and the generated main flow under ${api}/gen/ routing each operation to its subflow.`,
61
+ `Each operation's subflow is ${api}/resources/<schema>/<operationId>.subflow, in the resource's broker schema (resources.<schema>); create the routing only — the resource stories implement the subflows (create each as an empty pass-through the resource story will replace).`,
62
+ "Operations and their subflows:",
63
+ ...resources.flatMap(([resource, endpoints]) => endpoints.map((endpoint) => `- resources.${schemaFolder(resource)}.${endpoint.operationId}: ${endpoint.method} ${design.basePath}${endpoint.path}`))
64
+ ].join("\n"),
65
+ owned: [
66
+ `${api}/.project`,
67
+ `${api}/restapi.descriptor`,
68
+ `${api}/application.descriptor`,
69
+ `${api}/openapi.yaml`,
70
+ `${api}/gen`
71
+ ],
72
+ sharedReadOnly: contracts,
73
+ provides: resources.flatMap(([resource, endpoints]) => endpoints.map((endpoint) => `${api}/resources/${schemaFolder(resource)}/${endpoint.operationId}.subflow is routed from the main flow`))
74
+ }),
75
+ Story.make({
76
+ id: "policies",
77
+ title: `${policies} policy project for dev, test, and uat`,
78
+ description: [
79
+ `Create the ACE policy project ${policies}/ holding, for each environment (dev, test, uat), the backend endpoint of the ${service} SOAP service, the TLS client identity and truststore references, and the WS-Security user token reference, as policies the SOAPRequest node reads.`,
80
+ "No value is hard-coded outside the policies; credentials are references to the integration server's vault or keystore, never literals."
81
+ ].join("\n"),
82
+ owned: [policies],
83
+ sharedReadOnly: contracts,
84
+ provides: [
85
+ `${policies}: one backend policy per environment, selected by the integration server's configuration`
86
+ ]
87
+ }),
88
+ Story.make({
89
+ id: "shared-lib",
90
+ title: `${lib} shared library: WSDL, backend call, error mapping`,
91
+ description: [
92
+ `Create the shared library ${lib}/: import the WSDL and XSDs from contracts/soap/, a subflow that calls the backend through a SOAPRequest node configured from ${policies}, and the error subflow that turns an esito code or SOAP fault into the RFC 9457 problem the contract names (x-problem-codes), defaulting to 502 backend-error.`,
93
+ "Follow docs/patterns/PAT-ACE-001 and PAT-ACE-002."
94
+ ].join("\n"),
95
+ owned: [lib],
96
+ sharedReadOnly: contracts,
97
+ provides: [`${lib}: CallBackend and MapError subflows; the imported WSDL`]
98
+ }),
99
+ Story.make({
100
+ id: "backend-stub",
101
+ title: `${tests} project and recorded backend stubs`,
102
+ description: [
103
+ `Create the ACE unit test project ${tests}/ (JUnit, ACE 12.0.7+ test framework) and turn every masked exchange in test-data/ into a stub the tests can play back in place of the SOAP backend, one per recorded response, named <operation>/<sample>.`,
104
+ "Add the shared test support (loading a stub, asserting a JSON body against contracts/openapi-ace.yaml) under the support package."
105
+ ].join("\n"),
106
+ owned: [`${tests}/.project`, `${tests}/.classpath`, `${tests}/stubs`, `${tests}/src/support`],
107
+ sharedReadOnly: contracts,
108
+ provides: [
109
+ `${tests}/stubs/<operation>/<sample>.xml and the support package for every resource's tests`
110
+ ]
111
+ })
112
+ ];
113
+ for (const [resource, endpoints] of resources) {
114
+ const folder = schemaFolder(resource);
115
+ const sources = [...new Set(endpoints.flatMap((endpoint) => endpoint.sources))];
116
+ const findings = options.analyses
117
+ .filter((analysis) => sources.includes(analysis.operation))
118
+ .flatMap((analysis) => analysis.observations.map((line) => `- ${analysis.operation}: ${line}`));
119
+ stories.push(Story.make({
120
+ id: `resource-${resource}`,
121
+ title: `${resource}: ${endpoints.map((endpoint) => endpoint.operationId).join(", ")}`,
122
+ description: [
123
+ `Implement the ${resource} resource in the broker schema folder ${api}/resources/${folder}/: one subflow per operation (replacing the skeleton's pass-through) with its ESQL mapping modules, calling the backend through ${lib}, and its tests in ${tests}/src/resources/${folder}/ against the recorded stubs.`,
124
+ "Endpoints:",
125
+ ...endpoints.map((endpoint) => `- ${endpointLine(endpoint)}`),
126
+ ...(findings.length === 0
127
+ ? []
128
+ : ["What the samples showed (handle every case):", ...findings]),
129
+ "Follow CONTRIBUTING.md and docs/patterns/."
130
+ ].join("\n"),
131
+ dependsOn: foundation,
132
+ owned: [`${api}/resources/${folder}`, `${tests}/src/resources/${folder}`],
133
+ sharedReadOnly: [
134
+ ...contracts,
135
+ `${api}/gen`,
136
+ lib,
137
+ policies,
138
+ `${tests}/stubs`,
139
+ `${tests}/src/support`
140
+ ],
141
+ provides: endpoints.map((endpoint) => `${endpoint.method} ${design.basePath}${endpoint.path}`)
142
+ }));
143
+ }
144
+ stories.push(Story.make({
145
+ id: "contract-check",
146
+ title: "Contract check across every operation",
147
+ description: [
148
+ `Add ${tests}/src/contract/: one test per operation of contracts/openapi-ace.yaml that calls it through the deployed flow with a recorded stub and validates status, headers, and body against the contract, plus one per mapped problem code.`,
149
+ "A mismatch is a finding for the resource story that owns the operation, reported with BLOCKED_ON."
150
+ ].join("\n"),
151
+ dependsOn: resources.map(([resource]) => `resource-${resource}`),
152
+ owned: [`${tests}/src/contract`],
153
+ sharedReadOnly: [...contracts, api, lib, policies, `${tests}/stubs`, `${tests}/src/support`],
154
+ provides: ["every operation of the contract exercised end to end"]
155
+ }));
156
+ const epic = epicSentence(design, api, service);
157
+ return StoryPlan.make({ epicId: epicIdFor(epic), epic, stories });
158
+ };
159
+ export const planViolations = storyPlanViolations;
160
+ export const seedFiles = (options) => {
161
+ const substitute = (text) => text.replace(/__API__/g, options.api).replace(/__SERVICE__/g, options.service);
162
+ return [
163
+ ...options.scaffold.map((file) => ({
164
+ path: substitute(file.path),
165
+ contents: substitute(file.contents),
166
+ regenerated: false
167
+ })),
168
+ {
169
+ path: "contracts/api-design.json",
170
+ contents: `${JSON.stringify(Schema.encodeSync(ApiDesign)(options.design), null, 2)}\n`,
171
+ regenerated: true
172
+ },
173
+ {
174
+ path: "contracts/openapi.yaml",
175
+ contents: renderTypedYaml(projectOpenApi(options.design)),
176
+ regenerated: true
177
+ },
178
+ {
179
+ path: "contracts/openapi-ace.yaml",
180
+ contents: renderTypedYaml(projectOpenApi(options.design, { dialect: "3.0" })),
181
+ regenerated: true
182
+ },
183
+ {
184
+ path: "contracts/mapping.json",
185
+ contents: encodeMapping(mapService(options.catalog)),
186
+ regenerated: true
187
+ },
188
+ ...options.soapDocuments.map((file) => ({
189
+ path: `contracts/soap/${file.path}`,
190
+ contents: file.contents,
191
+ regenerated: true
192
+ })),
193
+ ...options.analyses.map((analysis) => ({
194
+ path: `docs/analysis/${analysis.operation}.md`,
195
+ contents: analysis.markdown,
196
+ regenerated: true
197
+ })),
198
+ ...options.patterns.map((file) => ({
199
+ path: `docs/patterns/${file.path}`,
200
+ contents: file.contents,
201
+ regenerated: true
202
+ })),
203
+ ...options.exchanges.flatMap((exchange) => exchange.response === undefined || exchange.response.envelope === ""
204
+ ? []
205
+ : [
206
+ {
207
+ path: `test-data/${exchange.operation}/${exchange.name}.response.xml`,
208
+ contents: `<!-- masked ${exchange.provenance} sample: ${exchange.purpose.replace(/--/g, "- -")} -->\n${exchange.response.envelope}\n`,
209
+ regenerated: true
210
+ },
211
+ ...(exchange.request === undefined
212
+ ? []
213
+ : [
214
+ {
215
+ path: `test-data/${exchange.operation}/${exchange.name}.request.xml`,
216
+ contents: `${exchange.request.envelope}\n`,
217
+ regenerated: true
218
+ }
219
+ ])
220
+ ])
221
+ ];
222
+ };
223
+ /** Write regenerated files always and team-owned scaffold files only when absent. */
224
+ export const writeSeed = (target, files) => Effect.gen(function* () {
225
+ let written = 0;
226
+ let kept = 0;
227
+ for (const file of files) {
228
+ if (!file.regenerated) {
229
+ const existing = yield* target.discover(file.path);
230
+ if (existing.includes(file.path)) {
231
+ kept++;
232
+ continue;
233
+ }
234
+ }
235
+ yield* target.write(file.path, file.contents);
236
+ written++;
237
+ }
238
+ return { written, kept };
239
+ });