@cmflow/agents 3.4.0-alpha.1
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/README.md +403 -0
- package/bin/cm-agents.ts +0 -0
- package/dist/bin/cm-agents.d.mts +2 -0
- package/dist/bin/cm-agents.mjs +372 -0
- package/dist/bin/cm-agents.mjs.map +1 -0
- package/package.json +37 -0
- package/scripts/clubmed-models.json +33 -0
- package/scripts/setup.ps1 +63 -0
- package/scripts/setup.sh +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
<div style="text-align: center" align="center">
|
|
2
|
+
<img src="https://ns.clubmed.com/fbs/RWD/branding2023/Logo/MicrosoftTeams-image%20(9).png" width="200" alt="Club Med"/>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
6
|
+
<h1>@cmflow/atlas</h1>
|
|
7
|
+
<hr />
|
|
8
|
+
|
|
9
|
+
[](https://badge.fury.io/js/%40cmflow%2Fatlas)
|
|
10
|
+
[](https://github.com/semantic-release/semantic-release)
|
|
11
|
+
[](https://oxc.rs/docs/guide/usage/formatter)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
Atlas builds an API-to-backend mapping catalogue from an OpenAPI contract and static TypeScript analysis. It generates a reviewable YAML document per route, can optionally infer unresolved mappings, and synchronizes confirmed mappings with Directus.
|
|
16
|
+
|
|
17
|
+
## What Atlas produces
|
|
18
|
+
|
|
19
|
+
For each API route, Atlas writes a route-centric YAML document and a companion `.graph.yaml` file. The graph records the resolved path from the API handler to its backends; the YAML records inputs, outputs, mapping evidence and fields that require review.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install --save-dev @cmflow/atlas
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yarn add -D @cmflow/atlas
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add -D @cmflow/atlas
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Initialize a target API project
|
|
36
|
+
|
|
37
|
+
Create `atlas.config.ts` at the root of the API project; it does not belong in the Atlas package.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
atlas init
|
|
41
|
+
# or create it at an explicit location
|
|
42
|
+
atlas --config /path/to/api/atlas.config.ts init
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
By default, Atlas loads `atlas.config.ts` from the directory where the command is launched. Use `--config` (or `-c`) to reference another configuration file. Use `--analysis-dir` only to override the API source directory to analyze.
|
|
46
|
+
|
|
47
|
+
The configuration imports `defineConfig` from Atlas:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { defineConfig } from "@cmflow/atlas";
|
|
51
|
+
|
|
52
|
+
export default defineConfig({
|
|
53
|
+
outputDir: "./tmp/atlas",
|
|
54
|
+
envs: {
|
|
55
|
+
XM_API_URL: "https://xm.example/openapi.json"
|
|
56
|
+
},
|
|
57
|
+
openapiUrl: "https://api.example.com/openapi.json",
|
|
58
|
+
openapiTimeoutMs: 60_000,
|
|
59
|
+
directusUrl: "https://cms.api.clubmed",
|
|
60
|
+
analysis: {
|
|
61
|
+
rootDir: "./app",
|
|
62
|
+
backends: ["ICC", "CMS", "CMS_B2C"]
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`process.cwd()` is always the project root: `outputDir`, `tsconfig.json`, Git paths and serialized source paths are resolved from it. `analysis.rootDir` only defines the source tree to analyze and may therefore point to a subdirectory such as `./app`. Atlas reads `compilerOptions.paths` from the root `tsconfig.json` and uses them as module aliases. Add `resolver.alias` only to override or complement those aliases.
|
|
68
|
+
|
|
69
|
+
`outputDir` is resolved from the project root, not from `analysis.rootDir`. With the configuration above, artifacts are written to `<project-root>/tmp/atlas`, never to `<project-root>/app/tmp/atlas`.
|
|
70
|
+
|
|
71
|
+
Use `analysis.excluded` for project-root-relative technical files that must not enter dependency traversal or `analysis_files`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
analysis: {
|
|
75
|
+
rootDir: "./app",
|
|
76
|
+
excluded: ["tools/build/*-reference.json"],
|
|
77
|
+
backends: [/* ... */]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Backend sources
|
|
82
|
+
|
|
83
|
+
Atlas starts from accesses actually detected in code on a backend reached by the route graph. It then consolidates those code candidates against the canonical properties returned by the matching `BackendSource`. Atlas never creates a confirmed mapping from API field-name similarity alone.
|
|
84
|
+
|
|
85
|
+
For a backend with no configured source, Atlas preserves code candidates for optional AI inference. A configured source is authoritative: only its resolved properties can validate the code access.
|
|
86
|
+
|
|
87
|
+
A backend can be either a string or a `BackendSource` created with `defineBackendSource`:
|
|
88
|
+
|
|
89
|
+
- a string (for example `"ICC"`) has no field reference; Atlas keeps the current code-analysis and optional inference flow;
|
|
90
|
+
- a `BackendSource` provides a `resolve` function that returns the backend's known properties. Atlas assigns its `name` as `backend` on every resolved property.
|
|
91
|
+
|
|
92
|
+
Any backend source can persist its resolved properties in a YAML cache. When the cache exists, Atlas uses it and does not call the remote source. Refresh it explicitly with `atlas backend-source --refresh` (or the compatible `atlas backend-sources --refresh`). Use `atlas backend-sources --refresh --all` to refresh every configured source without an interactive prompt.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
export const xm = defineBackendSource({
|
|
96
|
+
name: "XM",
|
|
97
|
+
cache: `${__dirname}/cache.yaml`,
|
|
98
|
+
resolve: () => openapiSource({ url: "https://xm.example/openapi.json" })
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
When a string and a source share the same name, the last declaration wins. This lets a source enrich a list of backend names, for example `[..., Object.values(BackendTypes), quableBackendSource]`.
|
|
103
|
+
|
|
104
|
+
The resolver returns one of the following shapes. Document-oriented backends have no route; REST backends have no document.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
type BackendProperty =
|
|
108
|
+
| {
|
|
109
|
+
document: string;
|
|
110
|
+
field: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
route: string;
|
|
115
|
+
method: string;
|
|
116
|
+
field: string;
|
|
117
|
+
description?: string;
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The generated mapping preserves this shape. In particular, `document` is a separate property: Atlas does not turn a Quable field into an artificial path such as `products.product_geographical_area`.
|
|
122
|
+
|
|
123
|
+
## Resolved backend metadata artifacts
|
|
124
|
+
|
|
125
|
+
At the start of `generate:graph` and `generate:catalog`, Atlas resolves every configured backend source and writes its metadata for inspection:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
<outputDir>/backends/<backend>.yaml
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For example, `<outputDir>/backends/quable_rest.yaml` contains the source name, generation date and canonical properties returned by Quable. Backends declared as strings still receive an artifact with `resolved: false`; this makes missing metadata sources visible during analysis.
|
|
132
|
+
|
|
133
|
+
### Quable
|
|
134
|
+
|
|
135
|
+
Attach rules that are specific to Quable directly to its source. They inherit the source backend automatically and their candidates are validated by the Quable property list.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { defineBackendSource, httpClient } from "@cmflow/atlas";
|
|
139
|
+
import { quableI18nFieldRule } from "./atlas-rules";
|
|
140
|
+
|
|
141
|
+
export const quable = defineBackendSource({
|
|
142
|
+
name: "QUABLE",
|
|
143
|
+
rules: [quableI18nFieldRule],
|
|
144
|
+
resolve: async () => {
|
|
145
|
+
const documentTypes = await httpClient.get<QuableDocumentType[]>("https://quable.example/document-types");
|
|
146
|
+
|
|
147
|
+
return documentTypes.flatMap((document) =>
|
|
148
|
+
document.properties.map((property) => ({
|
|
149
|
+
document: document.code,
|
|
150
|
+
field: property.code,
|
|
151
|
+
description: "récupéré via le backend resolver"
|
|
152
|
+
}))
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
For example, the resolver above may return:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
{
|
|
162
|
+
backend: "QUABLE",
|
|
163
|
+
document: "products",
|
|
164
|
+
field: "product_geographical_area",
|
|
165
|
+
description: "récupéré via le backend resolver"
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### CMS Directus
|
|
170
|
+
|
|
171
|
+
CMS Directus follows the same document-oriented contract. Its `cmsI18nFieldRule` is owned by the CMS source rather than registered as a global expression rule.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { defineBackendSource } from "@cmflow/atlas";
|
|
175
|
+
import { cmsI18nFieldRule } from "./atlas-rules";
|
|
176
|
+
|
|
177
|
+
export const cmsDirectus = defineBackendSource({
|
|
178
|
+
name: "CMS",
|
|
179
|
+
rules: [cmsI18nFieldRule],
|
|
180
|
+
resolve: async () => directusResolver.listProperties()
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`directusResolver.listProperties()` returns items such as `{ document: "offers", field: "title", description: "…" }`.
|
|
185
|
+
|
|
186
|
+
### CMS Legacy
|
|
187
|
+
|
|
188
|
+
CMS Legacy is configured identically, with its own resolver and backend identifier. It may reuse `cmsI18nFieldRule` when the backend uses `CmsI18n` helpers.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { defineBackendSource } from "@cmflow/atlas";
|
|
192
|
+
import { cmsI18nFieldRule } from "./atlas-rules";
|
|
193
|
+
|
|
194
|
+
export const cmsLegacy = defineBackendSource({
|
|
195
|
+
name: "CMS_LEGACY",
|
|
196
|
+
rules: [cmsI18nFieldRule],
|
|
197
|
+
resolve: async () => cmsLegacyResolver.listProperties()
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Custom REST backend with OpenAPI
|
|
202
|
+
|
|
203
|
+
`openapiSource` reuses Atlas's OpenAPI parsing utilities: downloading the document, resolving references, traversing operations, and extracting field descriptions. A project therefore only supplies the URL.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { constant, defineBackendSource, openapiSource } from "@cmflow/atlas";
|
|
207
|
+
|
|
208
|
+
export const xm = defineBackendSource({
|
|
209
|
+
name: "XM",
|
|
210
|
+
cache: `${__dirname}/xm-openapi.yaml`,
|
|
211
|
+
resolve: () =>
|
|
212
|
+
openapiSource({
|
|
213
|
+
url: constant<string>("XM_API_URL", "https://xm.example/openapi.json")
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`constant` resolves a value declared in `envs`, falling back to its second argument. No resolver context is required.
|
|
219
|
+
|
|
220
|
+
### Environment constants
|
|
221
|
+
|
|
222
|
+
Use `constant` when a backend source needs a configurable value such as a URL or an access token. Declare injected values at the root of `atlas.config.ts`; the value is resolved when the source runs:
|
|
223
|
+
|
|
224
|
+
1. Atlas uses `envs[name]` when it is set.
|
|
225
|
+
2. Otherwise, it returns the supplied default value.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { constant, defineBackendSource, openapiSource } from "@cmflow/atlas";
|
|
229
|
+
|
|
230
|
+
export const xmBackendSource = defineBackendSource({
|
|
231
|
+
name: "XM",
|
|
232
|
+
resolve: () =>
|
|
233
|
+
openapiSource({
|
|
234
|
+
url: constant<string>("XM_API_URL", "https://xm.example/openapi.json")
|
|
235
|
+
})
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
For example, configure the endpoint used by the source:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
export default defineConfig({
|
|
243
|
+
envs: {
|
|
244
|
+
XM_API_URL: "https://xm.internal/openapi.json"
|
|
245
|
+
}
|
|
246
|
+
// …other Atlas options
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`envs` can itself be populated from `process.env` when appropriate for the project.
|
|
251
|
+
|
|
252
|
+
This returns entries such as:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
{
|
|
256
|
+
backend: "XM",
|
|
257
|
+
route: "/path/to",
|
|
258
|
+
method: "GET",
|
|
259
|
+
field: "path.to.field",
|
|
260
|
+
description: "Description extraite du Swagger"
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Register strings and sources together:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { defineConfig } from "@cmflow/atlas";
|
|
268
|
+
import { cmsDirectus, cmsLegacy, quable, xm } from "./backend-sources";
|
|
269
|
+
|
|
270
|
+
export default defineConfig({
|
|
271
|
+
// …other Atlas options
|
|
272
|
+
analysis: {
|
|
273
|
+
backends: ["ICC", quable, cmsDirectus, cmsLegacy, xm]
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Custom expression rules
|
|
279
|
+
|
|
280
|
+
Use `defineExpressionRule` when a project-specific helper hides a backend field or wraps an expression that Atlas should follow. Define those rules in the analyzed project, add a cross-backend rule to `analysis.rules` in `atlas.config.ts`, and attach backend-specific rules to `defineBackendSource({ rules: [...] })` instead.
|
|
281
|
+
|
|
282
|
+
`match` is a cheap predicate that selects the `ts-morph` expression. `parse` returns the information Atlas should use: a backend field, a transparent wrapper, or both.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { defineConfig, defineExpressionRule } from "@cmflow/atlas";
|
|
286
|
+
import { Node } from "ts-morph";
|
|
287
|
+
|
|
288
|
+
const localizedFieldRule = defineExpressionRule({
|
|
289
|
+
name: "localized-field",
|
|
290
|
+
match: (expression) => Node.isCallExpression(expression) && expression.getExpression().getText() === "localizedField",
|
|
291
|
+
parse: (expression) => {
|
|
292
|
+
if (!Node.isCallExpression(expression)) return undefined;
|
|
293
|
+
|
|
294
|
+
const [source, field] = expression.getArguments();
|
|
295
|
+
if (!source || !Node.isExpression(source) || !Node.isStringLiteral(field)) return undefined;
|
|
296
|
+
|
|
297
|
+
return { backendField: `${source.getText()}.${field.getLiteralValue()}` };
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
export default defineConfig({
|
|
302
|
+
// …other Atlas options
|
|
303
|
+
analysis: {
|
|
304
|
+
// …other analysis options
|
|
305
|
+
rules: [localizedFieldRule]
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
For a wrapper that does not change the field path, return `transparent: true`:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
const unwrapRule = defineExpressionRule({
|
|
314
|
+
name: "unwrap-api-value",
|
|
315
|
+
match: (expression) => Node.isCallExpression(expression) && expression.getExpression().getText() === "unwrap",
|
|
316
|
+
parse: () => ({ transparent: true })
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`parse` may also set `mapperType` to annotate the generated mapping, or `apiMapping: true` when the helper maps an API value rather than a backend value.
|
|
321
|
+
|
|
322
|
+
Create a `.env.local` in the target API project when pushing to Directus:
|
|
323
|
+
|
|
324
|
+
```dotenv
|
|
325
|
+
DIRECTUS_URL=https://****
|
|
326
|
+
DIRECTUS_TOKEN=replace-with-a-static-token
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Workflow
|
|
330
|
+
|
|
331
|
+
Generate the topology first, then the route catalogue:
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
atlas --analysis-dir /path/to/api generate:graph
|
|
335
|
+
atlas --analysis-dir /path/to/api generate:catalog --output .tmp/datasource-catalogue
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Each route produces a YAML document and a `.graph.yaml` topology artifact. The catalogue requires a valid graph and leaves ambiguous fields marked `needs_review`.
|
|
339
|
+
|
|
340
|
+
When changing `analysis.rootDir`, backend sources or exclusions, regenerate the graph before the catalogue so the route graph and metadata artifacts use the same configuration:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
atlas generate:graph "GET /v2/products"
|
|
344
|
+
atlas generate:catalog "GET /v2/products"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Check configured coverage baselines:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
atlas --analysis-dir /path/to/api generate:test
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Review unresolved mappings or run optional AI inference:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
atlas --analysis-dir /path/to/api needs-review --sort percentage --limit 25
|
|
357
|
+
atlas --analysis-dir /path/to/api infer .tmp/datasource-catalogue
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The inference pass only reads analysis files referenced by each route graph. Accepted suggestions remain marked `inferred`; suggestions below the configured confidence threshold are retained for manual review.
|
|
361
|
+
|
|
362
|
+
Push route artifacts to Directus with an explicit write flag:
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
atlas --analysis-dir /path/to/api push .tmp/datasource-catalogue --update
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Without `--update`, the command performs a dry run. Use `clean-orphans` to inspect Directus links that no longer reference an API or backend property.
|
|
369
|
+
|
|
370
|
+
## Command reference
|
|
371
|
+
|
|
372
|
+
```text
|
|
373
|
+
atlas init Create atlas.config.ts
|
|
374
|
+
atlas backend-source [--refresh] Display backend metadata; refresh configured source caches when requested
|
|
375
|
+
atlas backend-sources [--refresh] [--all] Compatible plural alias; --all skips the source picker
|
|
376
|
+
atlas generate:graph Trace API routes to backends
|
|
377
|
+
atlas generate:catalog [route] Generate route review documents
|
|
378
|
+
atlas generate:test Check configured coverage baselines
|
|
379
|
+
atlas needs-review Rank unresolved routes
|
|
380
|
+
atlas infer [route|directory] Optionally enrich unresolved mappings
|
|
381
|
+
atlas push [directory|route] Preview or push mappings to Directus
|
|
382
|
+
atlas clean-orphans Inspect or delete orphaned Directus links
|
|
383
|
+
atlas report:changed Report coverage for changed routes
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## CI
|
|
387
|
+
|
|
388
|
+
The recommended static CI flow is `generate:graph`, `generate:catalog`, then `push`. Do not run optional inference in CI. Supply `DIRECTUS_URL` and `DIRECTUS_TOKEN` through the CI secret context; skip the push if either value is absent.
|
|
389
|
+
|
|
390
|
+
## Development
|
|
391
|
+
|
|
392
|
+
From the monorepo root:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
yarn workspace @cmflow/atlas build
|
|
396
|
+
yarn workspace @cmflow/atlas test
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## License
|
|
400
|
+
|
|
401
|
+
LicenseThe MIT License (MIT)Copyright (c) 2016 - Today ClubMed
|
|
402
|
+
|
|
403
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/bin/cm-agents.ts
ADDED
|
File without changes
|