@junejuly-lockstep/reconciler 0.1.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -0
  3. package/dist/boot.d.ts +51 -0
  4. package/dist/boot.d.ts.map +1 -0
  5. package/dist/boot.js +162 -0
  6. package/dist/boot.js.map +1 -0
  7. package/dist/cli.d.ts +4 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +180 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +55 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +140 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/contract.d.ts +61 -0
  16. package/dist/contract.d.ts.map +1 -0
  17. package/dist/contract.js +154 -0
  18. package/dist/contract.js.map +1 -0
  19. package/dist/determinism.d.ts +23 -0
  20. package/dist/determinism.d.ts.map +1 -0
  21. package/dist/determinism.js +102 -0
  22. package/dist/determinism.js.map +1 -0
  23. package/dist/errors.d.ts +25 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +35 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/exitCodes.d.ts +33 -0
  28. package/dist/exitCodes.d.ts.map +1 -0
  29. package/dist/exitCodes.js +49 -0
  30. package/dist/exitCodes.js.map +1 -0
  31. package/dist/extract.d.ts +30 -0
  32. package/dist/extract.d.ts.map +1 -0
  33. package/dist/extract.js +376 -0
  34. package/dist/extract.js.map +1 -0
  35. package/dist/index.d.ts +35 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +19 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/init.d.ts +17 -0
  40. package/dist/init.d.ts.map +1 -0
  41. package/dist/init.js +55 -0
  42. package/dist/init.js.map +1 -0
  43. package/dist/journeys.d.ts +34 -0
  44. package/dist/journeys.d.ts.map +1 -0
  45. package/dist/journeys.js +276 -0
  46. package/dist/journeys.js.map +1 -0
  47. package/dist/paths.d.ts +26 -0
  48. package/dist/paths.d.ts.map +1 -0
  49. package/dist/paths.js +80 -0
  50. package/dist/paths.js.map +1 -0
  51. package/dist/reconcile.d.ts +63 -0
  52. package/dist/reconcile.d.ts.map +1 -0
  53. package/dist/reconcile.js +316 -0
  54. package/dist/reconcile.js.map +1 -0
  55. package/dist/report.d.ts +33 -0
  56. package/dist/report.d.ts.map +1 -0
  57. package/dist/report.js +97 -0
  58. package/dist/report.js.map +1 -0
  59. package/dist/sync.d.ts +46 -0
  60. package/dist/sync.d.ts.map +1 -0
  61. package/dist/sync.js +110 -0
  62. package/dist/sync.js.map +1 -0
  63. package/dist/upload.d.ts +33 -0
  64. package/dist/upload.d.ts.map +1 -0
  65. package/dist/upload.js +86 -0
  66. package/dist/upload.js.map +1 -0
  67. package/dist/util.d.ts +24 -0
  68. package/dist/util.d.ts.map +1 -0
  69. package/dist/util.js +97 -0
  70. package/dist/util.js.map +1 -0
  71. package/dist/visual.d.ts +49 -0
  72. package/dist/visual.d.ts.map +1 -0
  73. package/dist/visual.js +145 -0
  74. package/dist/visual.js.map +1 -0
  75. package/dist/walk.d.ts +147 -0
  76. package/dist/walk.d.ts.map +1 -0
  77. package/dist/walk.js +433 -0
  78. package/dist/walk.js.map +1 -0
  79. package/package.json +78 -0
  80. package/templates/example.journey.ts +90 -0
  81. package/templates/lockstep-reconcile.yml +219 -0
  82. package/templates/lockstep.config.example.ts +46 -0
  83. package/templates/msw-handlers.example.ts +113 -0
  84. package/templates/scenarios.example.ts +32 -0
@@ -0,0 +1,316 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { createIngestClient, LockstepApiError } from '@junejuly-lockstep/sdk';
6
+ import { API_KEY_ENV, checkContract, assertContract, loadConfig, } from './config.js';
7
+ import { snapshotContract } from './contract.js';
8
+ import { compareHashMaps, formatDeterminismReport, hashPngs } from './determinism.js';
9
+ import { ContractViolation, InfrastructureError } from './errors.js';
10
+ import { EXIT, exitCodeForResult } from './exitCodes.js';
11
+ import { extractGraph } from './extract.js';
12
+ import { loadJourneys, loadScenarios } from './journeys.js';
13
+ import { assembleRunPayload, summarize } from './report.js';
14
+ import { collectArtifacts, uploadArtifacts } from './upload.js';
15
+ import { ensureDir, reconcilerVersion } from './util.js';
16
+ import { compareWalk, downloadBaselines } from './visual.js';
17
+ import { walk } from './walk.js';
18
+ const SHA_RE = /^[0-9a-f]{7,40}$/;
19
+ export function resolveCommitSha(ref, cwd, env = process.env) {
20
+ const candidate = ref ?? env['GITHUB_SHA'] ?? gitExec(cwd, ['rev-parse', 'HEAD']);
21
+ if (!candidate)
22
+ throw new InfrastructureError('could not determine the commit SHA; pass --ref <sha>');
23
+ const sha = candidate.trim().toLowerCase();
24
+ if (!SHA_RE.test(sha))
25
+ throw new InfrastructureError(`--ref must be a commit SHA (7-40 hex chars), got "${candidate}"`);
26
+ return sha;
27
+ }
28
+ export const API_BASE_URL_ENV = 'LOCKSTEP_API_BASE_URL';
29
+ /** `--api-base-url`, then `LOCKSTEP_API_BASE_URL`, then the config; must be an http(s) URL. */
30
+ export function resolveApiBaseUrl(override, fromConfig, env = process.env) {
31
+ const candidate = (override ?? env[API_BASE_URL_ENV] ?? '').trim();
32
+ if (!candidate)
33
+ return fromConfig;
34
+ let url;
35
+ try {
36
+ url = new URL(candidate);
37
+ }
38
+ catch {
39
+ throw new InfrastructureError(`--api-base-url must be an absolute http(s) URL, got "${candidate}"`);
40
+ }
41
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
42
+ throw new InfrastructureError(`--api-base-url must be an http(s) URL, got "${candidate}"`);
43
+ return candidate.replace(/\/+$/, '');
44
+ }
45
+ /** Run git and return trimmed stdout, or null when git or the ref is unavailable. */
46
+ export function gitExec(cwd, args) {
47
+ try {
48
+ return execFileSync('git', args, {
49
+ cwd,
50
+ encoding: 'utf8',
51
+ stdio: ['ignore', 'pipe', 'ignore'],
52
+ }).trim();
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ /** `git show <ref>:<path>`, or null when the ref or file does not exist. */
59
+ export function gitShowFile(cwd, ref, file) {
60
+ // `./` makes the path relative to cwd (the config dir) rather than the repository root.
61
+ return gitExec(cwd, ['show', `${ref}:./${file.replace(/\\/g, '/')}`]);
62
+ }
63
+ /** The whole pipeline, in SPEC order. Throws ContractViolation or InfrastructureError. */
64
+ export async function reconcile(opts) {
65
+ const log = opts.log ?? ((line) => console.log(line));
66
+ const now = opts.deps?.now ?? (() => new Date());
67
+ const cwd = opts.cwd ?? process.cwd();
68
+ const dryRun = opts.dryRun ?? false;
69
+ const startedAt = now();
70
+ // 1. Contract.
71
+ const loaded = await loadConfig(opts.configPath, cwd);
72
+ const { config, rootDir } = loaded;
73
+ const report = await checkContract(loaded, { requireApiKey: !dryRun });
74
+ for (const w of report.warnings)
75
+ log(`warning: ${w}`);
76
+ assertContract(report);
77
+ const outDir = path.resolve(rootDir, opts.outDir ?? '.lockstep');
78
+ const artifactsDir = path.join(outDir, 'artifacts');
79
+ rmSync(artifactsDir, { recursive: true, force: true });
80
+ ensureDir(artifactsDir);
81
+ // Every file below describes this run only; a previous run's leftovers must not survive it.
82
+ for (const stale of [
83
+ 'artifacts-determinism',
84
+ 'determinism.txt',
85
+ 'ingest-response.json',
86
+ 'run-payload.json',
87
+ 'walk.json',
88
+ ])
89
+ rmSync(path.join(outDir, stale), { recursive: true, force: true });
90
+ const commitSha = resolveCommitSha(opts.ref, rootDir);
91
+ const prNumber = opts.kind === 'pr' ? (opts.pr ?? prFromEnv()) : null;
92
+ if (opts.kind === 'pr' && prNumber === null) {
93
+ throw new ContractViolation('run:pr', 'PR runs need --pr <number> (or GITHUB_REF of the form refs/pull/<n>/merge)');
94
+ }
95
+ log(`lockstep reconcile ${reconcilerVersion()} ${config.projectKey} ${opts.kind} ${commitSha.slice(0, 12)} ${opts.env}${dryRun ? ' (dry run)' : ''}`);
96
+ const apiBaseUrl = resolveApiBaseUrl(opts.apiBaseUrl, config.apiBaseUrl);
97
+ if (apiBaseUrl !== config.apiBaseUrl)
98
+ log(`api: ${apiBaseUrl} (overriding ${config.apiBaseUrl} from the config)`);
99
+ const client = dryRun
100
+ ? (opts.deps?.client ?? null)
101
+ : (opts.deps?.client ??
102
+ createIngestClient({
103
+ baseUrl: apiBaseUrl,
104
+ apiKey: process.env[API_KEY_ENV],
105
+ fetch: opts.deps?.fetch,
106
+ }));
107
+ // 2. Journeys and scenarios.
108
+ const journeys = (await loadJourneys(rootDir, config.journeysGlob)).map((j) => j.model);
109
+ const scenarios = await loadScenarios(rootDir, config.scenariosPath);
110
+ log(`journeys: ${journeys.map((j) => j.key).join(', ')}; scenarios: ${scenarios.map((s) => s.name).join(', ')}`);
111
+ // 3. Extract.
112
+ const extractedGraph = await extractGraph({ rootDir });
113
+ writeJson(path.join(outDir, 'extracted-graph.json'), extractedGraph);
114
+ log(`extracted ${extractedGraph.routes.length} routes, ${extractedGraph.navigations.length} navigations (${extractedGraph.unresolvedCount} unresolved)`);
115
+ // 4. Contract snapshot.
116
+ const previousSdl = config.schema
117
+ ? gitShowFile(rootDir, opts.baseRef ?? 'HEAD~1', config.schema.sdlPath)
118
+ : null;
119
+ const contract = await snapshotContract({ rootDir, schema: config.schema, outDir, previousSdl });
120
+ writeJson(path.join(outDir, 'contract-snapshot.json'), contract.snapshot);
121
+ if (contract.changes)
122
+ log(`contract: ${contract.changes.length} schema changes vs ${opts.baseRef ?? 'HEAD~1'} (see contract-changes.md)`);
123
+ // 5. Baselines (before the walk).
124
+ let manifest = null;
125
+ let baselines = new Map();
126
+ if (client) {
127
+ manifest = await fetchManifest(client, config.projectKey, log);
128
+ if (manifest) {
129
+ baselines = await downloadBaselines(manifest, path.join(outDir, 'baselines'), opts.deps?.fetch ?? fetch, log);
130
+ log(`baselines: ${manifest.entries.length}`);
131
+ }
132
+ }
133
+ // 6. Walk.
134
+ const walkArgs = {
135
+ journeys,
136
+ scenarios,
137
+ config,
138
+ rootDir,
139
+ log,
140
+ deps: opts.deps?.walk,
141
+ noBoot: opts.deps?.noBoot,
142
+ };
143
+ const walkResult = await walk({ ...walkArgs, artifactsDir });
144
+ writeJson(path.join(outDir, 'walk.json'), walkResult);
145
+ if (opts.determinismCheck) {
146
+ const secondDir = path.join(outDir, 'artifacts-determinism');
147
+ rmSync(secondDir, { recursive: true, force: true });
148
+ ensureDir(secondDir);
149
+ log('determinism check: walking a second time');
150
+ await walk({ ...walkArgs, artifactsDir: secondDir });
151
+ const comparison = compareHashMaps(await hashPngs(path.join(artifactsDir, 'shots')), await hashPngs(path.join(secondDir, 'shots')));
152
+ const text = formatDeterminismReport(comparison);
153
+ writeFileSync(path.join(outDir, 'determinism.txt'), text + '\n');
154
+ log(text);
155
+ if (!comparison.deterministic) {
156
+ throw new InfrastructureError(`determinism check failed: ${comparison.different.length + comparison.onlyInFirst.length + comparison.onlyInSecond.length} screenshots differ between two walks of the same commit and seed`);
157
+ }
158
+ }
159
+ // 7. Visual diff.
160
+ const visual = compareWalk({ walk: walkResult, manifest, artifactsDir, baselines });
161
+ // 8. Upload.
162
+ let runId;
163
+ let keyFor;
164
+ if (client && !dryRun) {
165
+ const files = await collectArtifacts(artifactsDir);
166
+ const upload = await uploadArtifacts({ client, files, fetchImpl: opts.deps?.fetch, log });
167
+ runId = upload.runId;
168
+ keyFor = (rel) => upload.keys.get(rel) ?? `${upload.prefix}${rel}`;
169
+ log(`uploaded ${upload.uploaded} artifacts under ${upload.prefix}`);
170
+ }
171
+ else {
172
+ runId = randomUUID();
173
+ keyFor = (rel) => `runs/${runId}/${rel}`;
174
+ }
175
+ // 9. Payload.
176
+ const payload = assembleRunPayload({
177
+ projectKey: config.projectKey,
178
+ kind: opts.kind,
179
+ commitSha,
180
+ prNumber,
181
+ deployLabel: opts.deployLabel ?? null,
182
+ env: opts.env,
183
+ startedAt,
184
+ finishedAt: now(),
185
+ journeys,
186
+ scenarios,
187
+ extractedGraph,
188
+ contract: contract.snapshot,
189
+ walk: walkResult,
190
+ visual,
191
+ declaredSeams: opts.declaredSeams ?? [],
192
+ runId,
193
+ keyFor,
194
+ });
195
+ writeJson(path.join(outDir, 'run-payload.json'), payload);
196
+ // 10. Post.
197
+ let response = null;
198
+ if (client && !dryRun) {
199
+ try {
200
+ response = await client.postRun(payload);
201
+ }
202
+ catch (err) {
203
+ throw new InfrastructureError('posting the run to /api/ingest/runs failed', err);
204
+ }
205
+ writeJson(path.join(outDir, 'ingest-response.json'), response);
206
+ }
207
+ log(summarize(payload, {
208
+ visualDiffThreshold: config.visualDiffThreshold,
209
+ response,
210
+ dryRun,
211
+ outDir,
212
+ }));
213
+ const exitCode = response
214
+ ? exitCodeForResult({
215
+ judgmentCount: response.judgmentCount,
216
+ kind: opts.kind,
217
+ failOnVerdicts: opts.failOnVerdicts,
218
+ })
219
+ : EXIT.CLEAN;
220
+ return { payload, response, exitCode, outDir };
221
+ }
222
+ async function fetchManifest(client, projectKey, log) {
223
+ try {
224
+ return await client.getBaselines(projectKey);
225
+ }
226
+ catch (err) {
227
+ if (err instanceof LockstepApiError && err.status === 404) {
228
+ log('baselines: none yet (404)');
229
+ return null;
230
+ }
231
+ throw new InfrastructureError('fetching the baseline manifest failed', err);
232
+ }
233
+ }
234
+ function prFromEnv(env = process.env) {
235
+ const m = /^refs\/pull\/(\d+)\//.exec(env['GITHUB_REF'] ?? '');
236
+ return m ? Number(m[1]) : null;
237
+ }
238
+ function writeJson(file, data) {
239
+ ensureDir(path.dirname(file));
240
+ writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
241
+ }
242
+ /** `lockstep validate`: contract only, no walk. Returns a printable checklist and whether it passed. */
243
+ export async function validate(opts) {
244
+ const cwd = opts.cwd ?? process.cwd();
245
+ const lines = [];
246
+ const mark = (ok, text) => lines.push(`${ok ? ' ok ' : ' FAIL'} ${text}`);
247
+ let ok = true;
248
+ let loaded;
249
+ try {
250
+ loaded = await loadConfig(opts.configPath, cwd);
251
+ mark(true, `lockstep.config.ts valid (project ${loaded.config.projectKey}, ${loaded.config.seams.length} seams)`);
252
+ }
253
+ catch (err) {
254
+ if (err instanceof ContractViolation)
255
+ for (const i of err.items)
256
+ mark(false, `[${i.item}] ${i.message}`);
257
+ else
258
+ mark(false, String(err));
259
+ return { ok: false, lines };
260
+ }
261
+ const report = await checkContract(loaded, { requireApiKey: !opts.offline });
262
+ for (const v of report.violations)
263
+ mark(false, `[${v.item}] ${v.message}`);
264
+ for (const w of report.warnings)
265
+ lines.push(` warn ${w}`);
266
+ if (report.journeyFiles.length)
267
+ mark(true, `${report.journeyFiles.length} journey file(s): ${report.journeyFiles.join(', ')}`);
268
+ if (!report.violations.some((v) => v.item === 'scenarios'))
269
+ mark(true, `scenarios file ${loaded.config.scenariosPath}`);
270
+ if (!report.violations.some((v) => v.item === 'workflow'))
271
+ mark(true, '.github/workflows/lockstep-reconcile.yml present');
272
+ if (!opts.offline && !report.violations.some((v) => v.item === 'apiKey'))
273
+ mark(true, `${API_KEY_ENV} set`);
274
+ ok = report.violations.length === 0;
275
+ try {
276
+ const journeys = await loadJourneys(loaded.rootDir, loaded.config.journeysGlob);
277
+ for (const j of journeys) {
278
+ mark(true, `journey ${j.model.key}: ${Object.keys(j.model.machine.states).length} states, ${j.model.meta.bindings.length} bindings (${j.file})`);
279
+ }
280
+ }
281
+ catch (err) {
282
+ ok = false;
283
+ if (err instanceof ContractViolation)
284
+ for (const i of err.items)
285
+ mark(false, `[${i.item}] ${i.message}`);
286
+ else
287
+ mark(false, String(err));
288
+ }
289
+ try {
290
+ const scenarios = await loadScenarios(loaded.rootDir, loaded.config.scenariosPath);
291
+ mark(true, `scenarios: ${scenarios.map((s) => `${s.name} (seed ${s.seed})`).join(', ')}`);
292
+ }
293
+ catch (err) {
294
+ ok = false;
295
+ if (err instanceof ContractViolation)
296
+ for (const i of err.items)
297
+ mark(false, `[${i.item}] ${i.message}`);
298
+ else
299
+ mark(false, String(err));
300
+ }
301
+ if (loaded.config.schema) {
302
+ const sdl = path.resolve(loaded.rootDir, loaded.config.schema.sdlPath);
303
+ const codegen = path.resolve(loaded.rootDir, loaded.config.schema.codegenOutput);
304
+ mark(existsSync(sdl), `schema SDL ${loaded.config.schema.sdlPath}`);
305
+ mark(existsSync(codegen), `codegen output ${loaded.config.schema.codegenOutput}`);
306
+ ok = ok && existsSync(sdl) && existsSync(codegen);
307
+ }
308
+ else {
309
+ lines.push(' warn no schema configured; contract snapshot will be empty');
310
+ }
311
+ const lock = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'].find((f) => existsSync(path.join(loaded.rootDir, f)));
312
+ lines.push(lock ? ` ok lockfile ${lock}` : ' warn no lockfile found; lockfile hash will be "none"');
313
+ return { ok, lines };
314
+ }
315
+ export { readFileSync as _readFileSync };
316
+ //# sourceMappingURL=reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,IAAI,MAAM,WAAW,CAAC;AAQ7B,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAqB,MAAM,wBAAwB,CAAC;AACjG,OAAO,EACL,WAAW,EACX,aAAa,EACb,cAAc,EACd,UAAU,GAEX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,uBAAuB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAsC,MAAM,gBAAgB,CAAC;AAC7F,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAiB,MAAM,WAAW,CAAC;AAyChD,MAAM,MAAM,GAAG,kBAAkB,CAAC;AAElC,MAAM,UAAU,gBAAgB,CAC9B,GAAuB,EACvB,GAAW,EACX,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,SAAS,GAAG,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAClF,IAAI,CAAC,SAAS;QACZ,MAAM,IAAI,mBAAmB,CAAC,sDAAsD,CAAC,CAAC;IACxF,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;QACnB,MAAM,IAAI,mBAAmB,CAC3B,qDAAqD,SAAS,GAAG,CAClE,CAAC;IACJ,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,uBAAuB,CAAC;AAExD,+FAA+F;AAC/F,MAAM,UAAU,iBAAiB,CAC/B,QAA4B,EAC5B,UAAkB,EAClB,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,SAAS,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACnE,IAAI,CAAC,SAAS;QAAE,OAAO,UAAU,CAAC;IAClC,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,mBAAmB,CAC3B,wDAAwD,SAAS,GAAG,CACrE,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ;QACvD,MAAM,IAAI,mBAAmB,CAAC,+CAA+C,SAAS,GAAG,CAAC,CAAC;IAC7F,OAAO,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,IAAc;IACjD,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE;YAC/B,GAAG;YACH,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;SACpC,CAAC,CAAC,IAAI,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,GAAW,EAAE,IAAY;IAChE,wFAAwF;IACxF,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,0FAA0F;AAC1F,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAsB;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;IACpC,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC;IAExB,eAAe;IACf,MAAM,MAAM,GAAiB,MAAM,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACpE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IACnC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;IACvE,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ;QAAE,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACtD,cAAc,CAAC,MAAM,CAAC,CAAC;IACvB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,IAAI,WAAW,CAAC,CAAC;IACjE,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpD,MAAM,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,SAAS,CAAC,YAAY,CAAC,CAAC;IACxB,4FAA4F;IAC5F,KAAK,MAAM,KAAK,IAAI;QAClB,uBAAuB;QACvB,iBAAiB;QACjB,sBAAsB;QACtB,kBAAkB;QAClB,WAAW;KACZ;QACC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrE,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,iBAAiB,CACzB,QAAQ,EACR,4EAA4E,CAC7E,CAAC;IACJ,CAAC;IACD,GAAG,CACD,sBAAsB,iBAAiB,EAAE,KAAK,MAAM,CAAC,UAAU,KAAK,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,EAAE,CACtJ,CAAC;IACF,MAAM,UAAU,GAAG,iBAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACzE,IAAI,UAAU,KAAK,MAAM,CAAC,UAAU;QAClC,GAAG,CAAC,QAAQ,UAAU,gBAAgB,MAAM,CAAC,UAAU,mBAAmB,CAAC,CAAC;IAE9E,MAAM,MAAM,GAAwB,MAAM;QACxC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC;QAC7B,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM;YAClB,kBAAkB,CAAC;gBACjB,OAAO,EAAE,UAAU;gBACnB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAE;gBACjC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK;aACxB,CAAC,CAAC,CAAC;IAER,6BAA6B;IAC7B,MAAM,QAAQ,GAAG,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACxF,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;IACrE,GAAG,CACD,aAAa,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC5G,CAAC;IAEF,cAAc;IACd,MAAM,cAAc,GAAG,MAAM,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;IACvD,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAAE,cAAc,CAAC,CAAC;IACrE,GAAG,CACD,aAAa,cAAc,CAAC,MAAM,CAAC,MAAM,YAAY,cAAc,CAAC,WAAW,CAAC,MAAM,iBAAiB,cAAc,CAAC,eAAe,cAAc,CACpJ,CAAC;IAEF,wBAAwB;IACxB,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM;QAC/B,CAAC,CAAC,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;QACvE,CAAC,CAAC,IAAI,CAAC;IACT,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACjG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,wBAAwB,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC1E,IAAI,QAAQ,CAAC,OAAO;QAClB,GAAG,CACD,aAAa,QAAQ,CAAC,OAAO,CAAC,MAAM,sBAAsB,IAAI,CAAC,OAAO,IAAI,QAAQ,4BAA4B,CAC/G,CAAC;IAEJ,kCAAkC;IAClC,IAAI,QAAQ,GAA4B,IAAI,CAAC;IAC7C,IAAI,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,IAAI,MAAM,EAAE,CAAC;QACX,QAAQ,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QAC/D,IAAI,QAAQ,EAAE,CAAC;YACb,SAAS,GAAG,MAAM,iBAAiB,CACjC,QAAQ,EACR,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,EAC9B,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,KAAK,EACzB,GAAG,CACJ,CAAC;YACF,GAAG,CAAC,cAAc,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,WAAW;IACX,MAAM,QAAQ,GAAG;QACf,QAAQ;QACR,SAAS;QACT,MAAM;QACN,OAAO;QACP,GAAG;QACH,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI;QACrB,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM;KAC1B,CAAC;IACF,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,EAAE,GAAG,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC;IAC7D,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,UAAU,CAAC,CAAC;IAEtD,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;QAC7D,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,SAAS,CAAC,SAAS,CAAC,CAAC;QACrB,GAAG,CAAC,0CAA0C,CAAC,CAAC;QAChD,MAAM,IAAI,CAAC,EAAE,GAAG,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC;QACrD,MAAM,UAAU,GAAG,eAAe,CAChC,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,EAChD,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAC9C,CAAC;QACF,MAAM,IAAI,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC;QACjD,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC,CAAC;QACjE,GAAG,CAAC,IAAI,CAAC,CAAC;QACV,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE,CAAC;YAC9B,MAAM,IAAI,mBAAmB,CAC3B,6BAA6B,UAAU,CAAC,SAAS,CAAC,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,MAAM,GAAG,UAAU,CAAC,YAAY,CAAC,MAAM,mEAAmE,CAC7L,CAAC;QACJ,CAAC;IACH,CAAC;IAED,kBAAkB;IAClB,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC;IAEpF,aAAa;IACb,IAAI,KAAa,CAAC;IAClB,IAAI,MAA+B,CAAC;IACpC,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,YAAY,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1F,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QACrB,MAAM,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QACnE,GAAG,CAAC,YAAY,MAAM,CAAC,QAAQ,oBAAoB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACtE,CAAC;SAAM,CAAC;QACN,KAAK,GAAG,UAAU,EAAE,CAAC;QACrB,MAAM,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,KAAK,IAAI,GAAG,EAAE,CAAC;IAC3C,CAAC;IAED,cAAc;IACd,MAAM,OAAO,GAAG,kBAAkB,CAAC;QACjC,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,SAAS;QACT,QAAQ;QACR,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;QACrC,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,SAAS;QACT,UAAU,EAAE,GAAG,EAAE;QACjB,QAAQ;QACR,SAAS;QACT,cAAc;QACd,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,IAAI,EAAE,UAAU;QAChB,MAAM;QACN,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,EAAE;QACvC,KAAK;QACL,MAAM;KACP,CAAC,CAAC;IACH,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,kBAAkB,CAAC,EAAE,OAAO,CAAC,CAAC;IAE1D,YAAY;IACZ,IAAI,QAAQ,GAA6B,IAAI,CAAC;IAC9C,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACtB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,mBAAmB,CAAC,4CAA4C,EAAE,GAAG,CAAC,CAAC;QACnF,CAAC;QACD,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAAE,QAAQ,CAAC,CAAC;IACjE,CAAC;IAED,GAAG,CACD,SAAS,CAAC,OAAO,EAAE;QACjB,mBAAmB,EAAE,MAAM,CAAC,mBAAmB;QAC/C,QAAQ;QACR,MAAM;QACN,MAAM;KACP,CAAC,CACH,CAAC;IACF,MAAM,QAAQ,GAAG,QAAQ;QACvB,CAAC,CAAC,iBAAiB,CAAC;YAChB,aAAa,EAAE,QAAQ,CAAC,aAAa;YACrC,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,cAAc,EAAE,IAAI,CAAC,cAAc;SACpC,CAAC;QACJ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;IACf,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AACjD,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,MAAoB,EACpB,UAAkB,EAClB,GAAwB;IAExB,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,gBAAgB,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC1D,GAAG,CAAC,2BAA2B,CAAC,CAAC;YACjC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,IAAI,mBAAmB,CAAC,uCAAuC,EAAE,GAAG,CAAC,CAAC;IAC9E,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,MAAyB,OAAO,CAAC,GAAG;IACrD,MAAM,CAAC,GAAG,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/D,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjC,CAAC;AAED,SAAS,SAAS,CAAC,IAAY,EAAE,IAAa;IAC5C,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9B,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAC5D,CAAC;AAED,wGAAwG;AACxG,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAI9B;IACC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACtC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,CAAC,EAAW,EAAE,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC,CAAC;IAC9F,IAAI,EAAE,GAAG,IAAI,CAAC;IACd,IAAI,MAAoB,CAAC;IACzB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QAChD,IAAI,CACF,IAAI,EACJ,qCAAqC,MAAM,CAAC,MAAM,CAAC,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,SAAS,CACtG,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,iBAAiB;YAClC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,KAAK;gBAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;;YAChE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7E,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,UAAU;QAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3E,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAC5D,IAAI,MAAM,CAAC,YAAY,CAAC,MAAM;QAC5B,IAAI,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,qBAAqB,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC;QACxD,IAAI,CAAC,IAAI,EAAE,kBAAkB,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;IAC9D,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC;QACvD,IAAI,CAAC,IAAI,EAAE,kDAAkD,CAAC,CAAC;IACjE,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC;QACtE,IAAI,CAAC,IAAI,EAAE,GAAG,WAAW,MAAM,CAAC,CAAC;IACnC,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC;IAEpC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAChF,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACzB,IAAI,CACF,IAAI,EACJ,WAAW,CAAC,CAAC,KAAK,CAAC,GAAG,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,YAAY,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,cAAc,CAAC,CAAC,IAAI,GAAG,CACrI,CAAC;QACJ,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,EAAE,GAAG,KAAK,CAAC;QACX,IAAI,GAAG,YAAY,iBAAiB;YAClC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,KAAK;gBAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;;YAChE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,CAAC;IACD,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACnF,IAAI,CAAC,IAAI,EAAE,cAAc,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5F,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,EAAE,GAAG,KAAK,CAAC;QACX,IAAI,GAAG,YAAY,iBAAiB;YAClC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,KAAK;gBAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;;YAChE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACvE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACjF,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,cAAc,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,kBAAkB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;QAClF,EAAE,GAAG,EAAE,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;IACpD,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,+DAA+D,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAC3E,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CACzC,CAAC;IACF,KAAK,CAAC,IAAI,CACR,IAAI,CAAC,CAAC,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC,CAAC,yDAAyD,CAC7F,CAAC;IACF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;AACvB,CAAC;AAED,OAAO,EAAE,YAAY,IAAI,aAAa,EAAE,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { RunPayload, type ContractSnapshot, type Env, type ExtractedGraph, type IngestRunResponse, type JourneyModel, type RunKind, type Scenario, type VisualComparison, type WalkResult } from '@junejuly-lockstep/model';
2
+ export interface RunParts {
3
+ projectKey: string;
4
+ kind: RunKind;
5
+ commitSha: string;
6
+ prNumber: number | null;
7
+ deployLabel: string | null;
8
+ env: Env;
9
+ startedAt: Date;
10
+ finishedAt: Date;
11
+ journeys: JourneyModel[];
12
+ scenarios: Scenario[];
13
+ extractedGraph: ExtractedGraph;
14
+ contract: ContractSnapshot;
15
+ walk: WalkResult;
16
+ visual: VisualComparison[];
17
+ declaredSeams?: string[];
18
+ runId: string;
19
+ /** Map an artifact-relative path (shots/..., traces/..., diffs/...) to its S3 key. */
20
+ keyFor: (relPath: string) => string;
21
+ reconcilerVersion?: string;
22
+ }
23
+ /** Assemble and validate the payload posted to POST /api/ingest/runs. */
24
+ export declare function assembleRunPayload(parts: RunParts): RunPayload;
25
+ export interface SummaryOptions {
26
+ visualDiffThreshold: number;
27
+ response: IngestRunResponse | null;
28
+ dryRun: boolean;
29
+ outDir: string;
30
+ }
31
+ /** Plain-text summary table for CI logs. */
32
+ export declare function summarize(payload: RunPayload, opts: SummaryOptions): string;
33
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,KAAK,gBAAgB,EACrB,KAAK,GAAG,EACR,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAChB,MAAM,0BAA0B,CAAC;AAGlC,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,EAAE,GAAG,CAAC;IACT,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;IACjB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,yEAAyE;AACzE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,GAAG,UAAU,CAsC9D;AAED,MAAM,WAAW,cAAc;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,4CAA4C;AAC5C,wBAAgB,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CA8D3E"}
package/dist/report.js ADDED
@@ -0,0 +1,97 @@
1
+ import { RunPayload, } from '@junejuly-lockstep/model';
2
+ import { reconcilerVersion } from './util.js';
3
+ /** Assemble and validate the payload posted to POST /api/ingest/runs. */
4
+ export function assembleRunPayload(parts) {
5
+ const walk = {
6
+ ...parts.walk,
7
+ paths: parts.walk.paths.map((p) => ({
8
+ ...p,
9
+ steps: p.steps.map((s) => ({
10
+ ...s,
11
+ screenshotKey: s.screenshotKey ? parts.keyFor(s.screenshotKey) : null,
12
+ traceKey: s.traceKey ? parts.keyFor(s.traceKey) : null,
13
+ })),
14
+ })),
15
+ };
16
+ const visual = parts.visual.map((v) => ({
17
+ ...v,
18
+ screenshotKey: parts.keyFor(v.screenshotKey),
19
+ diffKey: v.diffKey ? parts.keyFor(v.diffKey) : null,
20
+ }));
21
+ return RunPayload.parse({
22
+ schemaVersion: 1,
23
+ projectKey: parts.projectKey,
24
+ kind: parts.kind,
25
+ ref: parts.kind === 'pr' && parts.prNumber !== null ? String(parts.prNumber) : parts.commitSha,
26
+ commitSha: parts.commitSha,
27
+ prNumber: parts.prNumber,
28
+ deployLabel: parts.deployLabel,
29
+ env: parts.env,
30
+ reconcilerVersion: parts.reconcilerVersion ?? reconcilerVersion(),
31
+ startedAt: parts.startedAt.toISOString(),
32
+ finishedAt: parts.finishedAt.toISOString(),
33
+ journeys: parts.journeys,
34
+ scenarios: parts.scenarios,
35
+ extractedGraph: parts.extractedGraph,
36
+ contract: parts.contract,
37
+ walk,
38
+ visual,
39
+ declaredSeams: parts.declaredSeams ?? [],
40
+ runId: parts.runId,
41
+ });
42
+ }
43
+ function row(label, value) {
44
+ return ` ${label.padEnd(22)} ${value}`;
45
+ }
46
+ /** Plain-text summary table for CI logs. */
47
+ export function summarize(payload, opts) {
48
+ const steps = payload.walk.paths.flatMap((p) => p.steps);
49
+ const count = (status) => steps.filter((s) => s.status === status).length;
50
+ const changed = payload.visual.filter((v) => v.baselineKey && v.score > opts.visualDiffThreshold);
51
+ const noBaseline = payload.visual.filter((v) => !v.baselineKey).length;
52
+ const lines = [
53
+ '',
54
+ `Lockstep reconcile ${payload.projectKey} ${payload.kind} ${payload.commitSha.slice(0, 12)}${payload.prNumber ? ` PR #${payload.prNumber}` : ''} ${payload.env}${payload.deployLabel ? ` ${payload.deployLabel}` : ''}`,
55
+ row('run id', payload.runId + (opts.dryRun ? ' (dry run, not posted)' : '')),
56
+ row('journeys', payload.journeys.map((j) => j.key).join(', ') || '-'),
57
+ row('scenarios', payload.scenarios.map((s) => s.name).join(', ') || '-'),
58
+ row('routes extracted', payload.extractedGraph.routes.length),
59
+ row('navigations', `${payload.extractedGraph.navigations.length} (${payload.extractedGraph.unresolvedCount} unresolved)`),
60
+ row('contract', `sdl ${short(payload.contract.sdlHash)} ops ${Object.keys(payload.contract.operations).length} lock ${short(payload.contract.lockfileHash)}`),
61
+ row('paths walked', payload.walk.paths.length),
62
+ row('steps', `${count('pass')} pass, ${count('fail')} fail, ${count('skipped')} skipped`),
63
+ row('screenshots', `${payload.visual.length} compared, ${changed.length} changed (> ${opts.visualDiffThreshold}), ${noBaseline} without baseline`),
64
+ ];
65
+ const failures = steps.filter((s) => s.status === 'fail');
66
+ if (failures.length) {
67
+ lines.push('', ' failed steps:');
68
+ for (const p of payload.walk.paths) {
69
+ for (const s of p.steps) {
70
+ if (s.status !== 'fail')
71
+ continue;
72
+ const t = s.transition
73
+ ? `${s.transition.from} --${s.transition.event}--> ${s.transition.to}`
74
+ : `initial ${s.stateKey}`;
75
+ lines.push(` ${p.journeyKey}/${p.scenario}: ${t}${s.error ? ` (${s.error})` : ''}`);
76
+ }
77
+ }
78
+ }
79
+ if (changed.length) {
80
+ lines.push('', ' visual changes:');
81
+ for (const v of changed)
82
+ lines.push(` ${v.journeyKey}/${v.stateKey}/${v.scenario}: ${(v.score * 100).toFixed(3)}% pixels`);
83
+ }
84
+ if (opts.response) {
85
+ const vc = Object.entries(opts.response.verdictCounts)
86
+ .sort()
87
+ .map(([k, v]) => `${k} ${v}`)
88
+ .join(', ');
89
+ lines.push('', row('ingest status', opts.response.status), row('verdicts', vc || '-'), row('needs judgment', opts.response.judgmentCount), row('cards created', opts.response.cardsCreated.join(', ') || '-'));
90
+ }
91
+ lines.push(row('local output', opts.outDir), '');
92
+ return lines.join('\n');
93
+ }
94
+ function short(hash) {
95
+ return hash.length > 12 ? hash.slice(0, 12) : hash;
96
+ }
97
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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"}
package/dist/sync.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { ModelSyncPayload, type ModelSyncResponse } from '@junejuly-lockstep/model';
2
+ import { type IngestClient } from '@junejuly-lockstep/sdk';
3
+ import { type ExitCode } from './exitCodes.js';
4
+ /**
5
+ * `lockstep sync-models` (M3).
6
+ *
7
+ * Design decision (locked): the platform never executes client code, and journey files are
8
+ * TypeScript, so the API cannot evaluate them from a GitHub webhook. Model sync therefore runs
9
+ * here, in the client's CI on every push to the default branch: load the config, journeys and
10
+ * scenarios with the same loaders `reconcile` uses (same ordering, same validation), build a
11
+ * `ModelSyncPayload`, and post it to `POST /api/ingest/models`. The webhook only tells the
12
+ * platform that a newer commit exists; this command is what delivers the models.
13
+ */
14
+ export interface SyncModelsOptions {
15
+ /** Commit SHA; defaults to GITHUB_SHA, then `git rev-parse HEAD`. */
16
+ ref?: string;
17
+ /** Branch name; defaults to GITHUB_REF_NAME (branch pushes only), then the checked-out branch. */
18
+ branch?: string;
19
+ /** Commit time (ISO 8601); defaults to the commit's committer date, then now. */
20
+ pushedAt?: string;
21
+ configPath?: string;
22
+ outDir?: string;
23
+ /** Load, validate and write the payload locally; no API call. */
24
+ dryRun?: boolean;
25
+ cwd?: string;
26
+ log?: (line: string) => void;
27
+ env?: NodeJS.ProcessEnv;
28
+ /** Test seams. */
29
+ deps?: {
30
+ client?: IngestClient;
31
+ fetch?: typeof fetch;
32
+ now?: () => Date;
33
+ };
34
+ }
35
+ export interface SyncModelsResult {
36
+ payload: ModelSyncPayload;
37
+ response: ModelSyncResponse | null;
38
+ exitCode: ExitCode;
39
+ outDir: string;
40
+ }
41
+ /** Branch under sync: explicit, then GitHub's `GITHUB_REF_NAME` for branch refs, then git HEAD. */
42
+ export declare function resolveBranch(branch: string | undefined, cwd: string, env?: NodeJS.ProcessEnv): string;
43
+ /** Commit time as ISO 8601 UTC: explicit, then the committer date from git, then `now`. */
44
+ export declare function resolvePushedAt(pushedAt: string | undefined, cwd: string, sha: string, now?: () => Date): string;
45
+ export declare function syncModels(opts: SyncModelsOptions): Promise<SyncModelsResult>;
46
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACpF,OAAO,EAAwC,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AASjG,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAKrD;;;;;;;;;GASG;AACH,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kGAAkG;IAClG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,kBAAkB;IAClB,IAAI,CAAC,EAAE;QACL,MAAM,CAAC,EAAE,YAAY,CAAC;QACtB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;QACrB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACnC,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mGAAmG;AACnG,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAWR;AAED,2FAA2F;AAC3F,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,MAAM,IAAuB,GACjC,MAAM,CAWR;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmFnF"}
package/dist/sync.js ADDED
@@ -0,0 +1,110 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ModelSyncPayload } from '@junejuly-lockstep/model';
4
+ import { createIngestClient, LockstepApiError } from '@junejuly-lockstep/sdk';
5
+ import { API_KEY_ENV, assertContract, checkContract, loadConfig, } from './config.js';
6
+ import { ContractViolation, InfrastructureError } from './errors.js';
7
+ import { EXIT } from './exitCodes.js';
8
+ import { loadJourneys, loadScenarios } from './journeys.js';
9
+ import { gitExec, resolveCommitSha } from './reconcile.js';
10
+ import { ensureDir, reconcilerVersion } from './util.js';
11
+ /** Branch under sync: explicit, then GitHub's `GITHUB_REF_NAME` for branch refs, then git HEAD. */
12
+ export function resolveBranch(branch, cwd, env = process.env) {
13
+ const explicit = branch?.trim();
14
+ if (explicit)
15
+ return explicit.replace(/^refs\/heads\//, '');
16
+ const ref = env['GITHUB_REF'];
17
+ if (ref?.startsWith('refs/heads/'))
18
+ return ref.slice('refs/heads/'.length);
19
+ if (env['GITHUB_REF_TYPE'] === 'branch' && env['GITHUB_REF_NAME'])
20
+ return env['GITHUB_REF_NAME'];
21
+ const head = gitExec(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
22
+ if (head && head !== 'HEAD')
23
+ return head;
24
+ throw new InfrastructureError('could not determine the branch (detached HEAD or not a git checkout); pass --branch <name>');
25
+ }
26
+ /** Commit time as ISO 8601 UTC: explicit, then the committer date from git, then `now`. */
27
+ export function resolvePushedAt(pushedAt, cwd, sha, now = () => new Date()) {
28
+ const candidate = pushedAt ?? gitExec(cwd, ['show', '-s', '--format=%cI', sha]) ?? undefined;
29
+ if (candidate !== undefined) {
30
+ const d = new Date(candidate);
31
+ if (Number.isNaN(d.getTime()))
32
+ throw new InfrastructureError(`--pushed-at must be an ISO 8601 timestamp, got "${candidate}"`);
33
+ return d.toISOString();
34
+ }
35
+ return now().toISOString();
36
+ }
37
+ export async function syncModels(opts) {
38
+ const log = opts.log ?? ((line) => console.log(line));
39
+ const now = opts.deps?.now ?? (() => new Date());
40
+ const env = opts.env ?? process.env;
41
+ const cwd = opts.cwd ?? process.cwd();
42
+ const dryRun = opts.dryRun ?? false;
43
+ // 1. Contract: same checks as reconcile, so a repo that cannot reconcile cannot sync either.
44
+ const loaded = await loadConfig(opts.configPath, cwd);
45
+ const { config, rootDir } = loaded;
46
+ const report = await checkContract(loaded, { env, requireApiKey: !dryRun });
47
+ for (const w of report.warnings)
48
+ log(`warning: ${w}`);
49
+ assertContract(report);
50
+ const outDir = path.resolve(rootDir, opts.outDir ?? '.lockstep');
51
+ const commitSha = resolveCommitSha(opts.ref, rootDir, env);
52
+ const branch = resolveBranch(opts.branch, rootDir, env);
53
+ const pushedAt = resolvePushedAt(opts.pushedAt, rootDir, commitSha, now);
54
+ log(`lockstep sync-models ${reconcilerVersion()} ${config.projectKey} ${branch} ${commitSha.slice(0, 12)}${dryRun ? ' (dry run)' : ''}`);
55
+ // 2. Journeys and scenarios, deterministically ordered by the loaders (files by path, scenarios by name).
56
+ const journeys = (await loadJourneys(rootDir, config.journeysGlob)).map((j) => j.model);
57
+ const scenarios = await loadScenarios(rootDir, config.scenariosPath);
58
+ log(`journeys: ${journeys.map((j) => j.key).join(', ') || '(none)'}; scenarios: ${scenarios.map((s) => s.name).join(', ')}`);
59
+ // 3. Payload, validated locally before anything leaves the machine.
60
+ const payload = ModelSyncPayload.parse({
61
+ schemaVersion: 1,
62
+ projectKey: config.projectKey,
63
+ commitSha,
64
+ ref: branch,
65
+ pushedAt,
66
+ journeys,
67
+ scenarios,
68
+ reconcilerVersion: reconcilerVersion(),
69
+ });
70
+ ensureDir(outDir);
71
+ writeFileSync(path.join(outDir, 'model-sync-payload.json'), JSON.stringify(payload, null, 2) + '\n');
72
+ // 4. Post.
73
+ let response = null;
74
+ const client = dryRun
75
+ ? (opts.deps?.client ?? null)
76
+ : (opts.deps?.client ??
77
+ createIngestClient({
78
+ baseUrl: config.apiBaseUrl,
79
+ apiKey: env[API_KEY_ENV],
80
+ fetch: opts.deps?.fetch,
81
+ }));
82
+ if (client && !dryRun) {
83
+ try {
84
+ response = await client.syncModels(payload);
85
+ }
86
+ catch (err) {
87
+ // 422 means the platform refused the models themselves: that is the repo's problem (exit 1).
88
+ if (err instanceof LockstepApiError && err.status === 422) {
89
+ const details = detailsOf(err.body);
90
+ throw ContractViolation.fromList(details.length
91
+ ? details.map((message) => ({ item: 'models', message }))
92
+ : [{ item: 'models', message: err.message }]);
93
+ }
94
+ throw new InfrastructureError('posting the models to /api/ingest/models failed', err);
95
+ }
96
+ writeFileSync(path.join(outDir, 'model-sync-response.json'), JSON.stringify(response, null, 2) + '\n');
97
+ log(`synced: ${response.journeysUpserted} journey(s) upserted, ${response.versionsCreated} version(s) created, ${response.archived} archived`);
98
+ }
99
+ else {
100
+ log(`dry run: ${journeys.length} journey(s), ${scenarios.length} scenario(s) written to ${path.relative(rootDir, path.join(outDir, 'model-sync-payload.json')) || '.'}; nothing posted`);
101
+ }
102
+ return { payload, response, exitCode: EXIT.CLEAN, outDir };
103
+ }
104
+ function detailsOf(body) {
105
+ if (!body || typeof body !== 'object')
106
+ return [];
107
+ const details = body.details;
108
+ return Array.isArray(details) ? details.filter((d) => typeof d === 'string') : [];
109
+ }
110
+ //# sourceMappingURL=sync.js.map