@specverse/engines 6.33.0 → 6.34.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 (57) hide show
  1. package/dist/libs/instance-factories/communication/rabbitmq-events.yaml +11 -0
  2. package/dist/libs/instance-factories/controllers/fastify.yaml +11 -0
  3. package/dist/libs/instance-factories/managed/stripe-rest-sdk.yaml +78 -0
  4. package/dist/libs/instance-factories/managed/templates/stripe/stripe-client-generator.js +8 -0
  5. package/dist/libs/instance-factories/orms/prisma.yaml +11 -0
  6. package/dist/realize/index.d.ts.map +1 -1
  7. package/dist/realize/index.js +261 -4
  8. package/dist/realize/index.js.map +1 -1
  9. package/dist/realize/library/loader.d.ts.map +1 -1
  10. package/dist/realize/library/loader.js +81 -1
  11. package/dist/realize/library/loader.js.map +1 -1
  12. package/dist/realize/per-action-emitter.d.ts +9 -0
  13. package/dist/realize/per-action-emitter.d.ts.map +1 -1
  14. package/dist/realize/per-action-emitter.js.map +1 -1
  15. package/dist/realize/per-action-llm-emit.d.ts.map +1 -1
  16. package/dist/realize/per-action-llm-emit.js +5 -1
  17. package/dist/realize/per-action-llm-emit.js.map +1 -1
  18. package/dist/realize/per-action-runner.d.ts +45 -0
  19. package/dist/realize/per-action-runner.d.ts.map +1 -1
  20. package/dist/realize/per-action-runner.js +132 -0
  21. package/dist/realize/per-action-runner.js.map +1 -1
  22. package/dist/realize/resolver/index.d.ts +157 -0
  23. package/dist/realize/resolver/index.d.ts.map +1 -0
  24. package/dist/realize/resolver/index.js +307 -0
  25. package/dist/realize/resolver/index.js.map +1 -0
  26. package/dist/realize/runtime-emitters/dispatcher.d.ts +176 -0
  27. package/dist/realize/runtime-emitters/dispatcher.d.ts.map +1 -0
  28. package/dist/realize/runtime-emitters/dispatcher.js +76 -0
  29. package/dist/realize/runtime-emitters/dispatcher.js.map +1 -0
  30. package/dist/realize/runtime-emitters/executable.d.ts +57 -0
  31. package/dist/realize/runtime-emitters/executable.d.ts.map +1 -0
  32. package/dist/realize/runtime-emitters/executable.js +316 -0
  33. package/dist/realize/runtime-emitters/executable.js.map +1 -0
  34. package/dist/realize/runtime-emitters/library.d.ts +52 -0
  35. package/dist/realize/runtime-emitters/library.d.ts.map +1 -0
  36. package/dist/realize/runtime-emitters/library.js +349 -0
  37. package/dist/realize/runtime-emitters/library.js.map +1 -0
  38. package/dist/realize/runtime-emitters/managed.d.ts +44 -0
  39. package/dist/realize/runtime-emitters/managed.d.ts.map +1 -0
  40. package/dist/realize/runtime-emitters/managed.js +283 -0
  41. package/dist/realize/runtime-emitters/managed.js.map +1 -0
  42. package/dist/realize/runtime-emitters/messaging.d.ts +77 -0
  43. package/dist/realize/runtime-emitters/messaging.d.ts.map +1 -0
  44. package/dist/realize/runtime-emitters/messaging.js +423 -0
  45. package/dist/realize/runtime-emitters/messaging.js.map +1 -0
  46. package/dist/realize/runtime-emitters/service.d.ts +42 -0
  47. package/dist/realize/runtime-emitters/service.d.ts.map +1 -0
  48. package/dist/realize/runtime-emitters/service.js +355 -0
  49. package/dist/realize/runtime-emitters/service.js.map +1 -0
  50. package/dist/realize/types/instance-factory.d.ts +1 -1
  51. package/dist/realize/types/instance-factory.d.ts.map +1 -1
  52. package/libs/instance-factories/communication/rabbitmq-events.yaml +11 -0
  53. package/libs/instance-factories/controllers/fastify.yaml +11 -0
  54. package/libs/instance-factories/managed/stripe-rest-sdk.yaml +78 -0
  55. package/libs/instance-factories/managed/templates/stripe/stripe-client-generator.ts +26 -0
  56. package/libs/instance-factories/orms/prisma.yaml +11 -0
  57. package/package.json +1 -1
@@ -40,6 +40,17 @@ dependencies:
40
40
  - name: "typescript"
41
41
  version: "^5.2.0"
42
42
 
43
+ # V2 factory metadata — Component Dependencies V2 proposal Section 4
44
+ v2metadata:
45
+ topology: messaging # AMQP broker pub/sub; deploys as communications: instance
46
+ protocol: amqp # AMQP 0-9-1 (RabbitMQ)
47
+ authentication: { kind: bearer, mechanism: PLAIN } # RabbitMQ credential pair (user/password)
48
+ supportedTopologies: [messaging]
49
+ serverTemplate: templates/rabbitmq/connection-generator.ts # broker connection setup
50
+ clientTemplate: templates/rabbitmq/publisher-generator.ts # publisher client for producers
51
+ emitsDeploymentInstance: true # generates a communications: instance entry
52
+ deploymentCategory: communications # lands in communications: category
53
+
43
54
  codeTemplates:
44
55
  publisher:
45
56
  engine: typescript
@@ -52,6 +52,17 @@ dependencies:
52
52
  - name: "tsx"
53
53
  version: "^4.0.0"
54
54
 
55
+ # V2 factory metadata — Component Dependencies V2 proposal Section 4
56
+ v2metadata:
57
+ topology: service # generates a Fastify HTTP server; deploys as services: instance
58
+ protocol: rest # HTTP/REST transport
59
+ authentication: { kind: bearer } # typical JWT bearer auth; overridable per deployment
60
+ supportedTopologies: [service]
61
+ serverTemplate: templates/fastify/server-generator.ts # server bind code (main.ts / server.ts)
62
+ clientTemplate: null # clients generated from TypeScript SDK factory (separate concern)
63
+ emitsDeploymentInstance: true # generates a services: instance entry in deployment
64
+ deploymentCategory: services # lands in services: category
65
+
55
66
  codeTemplates:
56
67
  routes:
57
68
  engine: typescript
@@ -0,0 +1,78 @@
1
+ name: StripeRestSDK
2
+ version: "1.0.0"
3
+ category: infrastructure
4
+ description: "Stripe payments SDK adapter — wraps the Stripe Node.js SDK with a typed SpecVerse client for managed payment processing"
5
+
6
+ metadata:
7
+ author: "SpecVerse Team"
8
+ license: "MIT"
9
+ tags: [stripe, payments, managed, rest, sdk, infrastructure]
10
+
11
+ compatibility:
12
+ specverse: ">=6.33.0"
13
+ node: ">=18.0.0"
14
+
15
+ capabilities:
16
+ provides:
17
+ - "managed.payments"
18
+ - "managed.payments.stripe"
19
+ requires: [] # external SaaS — no transitive SpecVerse deps
20
+
21
+ technology:
22
+ runtime: "node"
23
+ language: "typescript"
24
+ framework: "stripe-sdk"
25
+ version: "^14.0.0"
26
+
27
+ # V2 factory metadata — Component Dependencies V2 proposal Section 4
28
+ # Forward-looking stub: Round 2 realize emitters will consume these fields.
29
+ v2metadata:
30
+ topology: managed # external SaaS via SDK; deploys as infrastructure: instance
31
+ protocol: rest # Stripe API uses HTTPS/REST under the hood
32
+ sdk: "stripe" # npm package name: https://www.npmjs.com/package/stripe
33
+ authentication: { kind: api-key, envVar: STRIPE_SECRET_KEY } # Stripe uses API keys
34
+ supportedTopologies: [managed]
35
+ clientTemplate: templates/stripe/stripe-client-generator.ts # typed wrapper exposing Charge.create etc.
36
+ serverTemplate: null # external SaaS — no server bind code emitted
37
+ emitsDeploymentInstance: true # generates an infrastructure: instance entry in deployment
38
+ deploymentCategory: infrastructure # lands in infrastructure: category (external SaaS)
39
+
40
+ dependencies:
41
+ runtime:
42
+ - name: "stripe"
43
+ version: "^14.0.0"
44
+
45
+ dev:
46
+ - name: "@types/node"
47
+ version: "^20.8.0"
48
+ - name: "typescript"
49
+ version: "^5.2.0"
50
+
51
+ # Minimal stub: codeTemplates to be fleshed out in Round 2 (per-runtime emitters)
52
+ codeTemplates:
53
+ client:
54
+ engine: typescript
55
+ generator: "libs/instance-factories/managed/templates/stripe/stripe-client-generator.ts"
56
+ outputPattern: "{backendDir}/src/clients/StripeClient.ts"
57
+
58
+ configuration:
59
+ envVar: "STRIPE_SECRET_KEY"
60
+ apiVersion: "2024-06-20"
61
+ webhookSecret: null # optional: STRIPE_WEBHOOK_SECRET for signature verification
62
+
63
+ requirements:
64
+ dependencies:
65
+ npm:
66
+ dependencies:
67
+ "stripe": "^14.0.0"
68
+ environment:
69
+ - name: "STRIPE_SECRET_KEY"
70
+ description: "Stripe secret API key"
71
+ example: "sk_live_..."
72
+ required: true
73
+ category: "Payments"
74
+ - name: "STRIPE_WEBHOOK_SECRET"
75
+ description: "Stripe webhook signing secret (required only if handling webhooks)"
76
+ example: "whsec_..."
77
+ required: false
78
+ category: "Payments"
@@ -0,0 +1,8 @@
1
+ async function generate(_context) {
2
+ throw new Error(
3
+ "StripeRestSDK client generator is a V2 Round 2 stub. The managed-runtime realize emitter is not yet implemented. See Component Dependencies V2 proposal Phase 3."
4
+ );
5
+ }
6
+ export {
7
+ generate
8
+ };
@@ -40,6 +40,17 @@ dependencies:
40
40
  - name: "typescript"
41
41
  version: "^5.2.0"
42
42
 
43
+ # V2 factory metadata — Component Dependencies V2 proposal Section 4
44
+ v2metadata:
45
+ topology: library # in-process ORM client; no deployment instance emitted
46
+ # protocol: omitted — not applicable for library topology
47
+ authentication: { kind: none } # auth is the DB connection string, not a protocol-level scheme
48
+ supportedTopologies: [library]
49
+ clientTemplate: null # client is the generated Prisma client itself
50
+ serverTemplate: null # no server component
51
+ emitsDeploymentInstance: false # library-shaped; consumer manages DB config via storage: instance
52
+ deploymentCategory: storage # informational: if a storage: instance existed, it would land here
53
+
43
54
  codeTemplates:
44
55
  schema:
45
56
  engine: typescript
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realize/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AAGjC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,uBAAuB,CAAC;AAItC,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAMlE,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA0DnF,cAAM,sBAAuB,YAAW,aAAa;IACnD,IAAI,SAAa;IACjB,OAAO,SAAW;IAClB,YAAY,WAA+E;IAE3F,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,WAAW,CAAS;IAEtB,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwDjB,OAAO,IAAI,UAAU;IAIrB,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG;IAK1B,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC;IAKvF;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAw0B9F;;;OAGG;YACW,UAAU;IA8DxB,OAAO,CAAC,oBAAoB;CAuB7B;AAED,eAAO,MAAM,MAAM,wBAA+B,CAAC;AACnD,eAAe,MAAM,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realize/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AAGjC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,uBAAuB,CAAC;AAItC,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAMlE,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA2DnF,cAAM,sBAAuB,YAAW,aAAa;IACnD,IAAI,SAAa;IACjB,OAAO,SAAW;IAClB,YAAY,WAA+E;IAE3F,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,WAAW,CAAS;IAEtB,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwDjB,OAAO,IAAI,UAAU;IAIrB,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG;IAK1B,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC;IAKvF;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAomC9F;;;OAGG;YACW,UAAU;IA8DxB,OAAO,CAAC,oBAAoB;CAuB7B;AAED,eAAO,MAAM,MAAM,wBAA+B,CAAC;AACnD,eAAe,MAAM,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
@@ -17,6 +17,7 @@ export { createCodeGenerator } from './engines/code-generator.js';
17
17
  import { writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync } from 'fs';
18
18
  import { dirname, join, basename } from 'path';
19
19
  import { fileURLToPath } from 'url';
20
+ import yaml from 'js-yaml';
20
21
  import { createDefaultLibrary } from './library/library.js';
21
22
  import { createResolver } from './library/resolver.js';
22
23
  import { createCodeGenerator } from './engines/code-generator.js';
@@ -354,20 +355,39 @@ class SpecVerseRealizeEngine {
354
355
  }
355
356
  }
356
357
  // ─── Per-action path (default and only path since Phase 3b) ───
357
- const { runPerActionEmission } = await import('./per-action-runner.js');
358
+ const { runPerActionEmission, buildAvailableClientsByOwner } = await import('./per-action-runner.js');
358
359
  const { createRealizeActionLlmEmit } = await import('./per-action-llm-emit.js');
359
360
  // Prefer the ORM-specific shared matcher when available; fall back to
360
361
  // the prisma matchAgainstConventions with the loaded convention array.
361
- const sharedMatcher = matcher ?? (conventions
362
- ? (await import(join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', '_shared', 'step-matching.ts').replace(/\.ts$/, '.js'))).matchAgainstConventions
363
- : undefined);
362
+ // Use resolveGenPath (defined above) which handles published-package
363
+ // structure correctly: it checks dist/libs/ first (compiled .js for
364
+ // installed packages) before falling back to libs/ (source .ts for
365
+ // dev). The previous inline `.replace(/\.ts$/, '.js')` only checked
366
+ // libs/.../step-matching.js which doesn't exist in either location —
367
+ // published packages have it at dist/libs/.../step-matching.js.
368
+ let sharedMatcher = matcher;
369
+ if (!sharedMatcher && conventions) {
370
+ const sharedMatcherTsPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', '_shared', 'step-matching.ts');
371
+ const sharedMatcherSrc = resolveGenPath(sharedMatcherTsPath);
372
+ if (sharedMatcherSrc) {
373
+ const sharedMod = await import(sharedMatcherSrc);
374
+ sharedMatcher = sharedMod.matchAgainstConventions;
375
+ }
376
+ }
364
377
  // LLM emit hook — only fires when an AI provider is configured.
365
378
  const llmEmit = createRealizeActionLlmEmit({});
379
+ // Round 3 follow-up: thread V2 imports → LLM prompt's
380
+ // `availableClients` slot. Built from spec.components[].import,
381
+ // keyed by owner so the runner can drop the per-component string
382
+ // into RealizeContext as it walks each owner. Empty map when no
383
+ // component declares V2 imports — runner is a no-op then.
384
+ const availableClientsByOwner = buildAvailableClientsByOwner(spec);
366
385
  const result = await runPerActionEmission({
367
386
  spec, outputDir,
368
387
  matcher: sharedMatcher,
369
388
  conventions,
370
389
  llmEmit,
390
+ availableClientsByOwner,
371
391
  silent: false,
372
392
  });
373
393
  if (result.totals.actionCount > 0) {
@@ -378,6 +398,243 @@ class SpecVerseRealizeEngine {
378
398
  errors.push(`AI Behaviors: ${e.message}`);
379
399
  }
380
400
  }
401
+ // 2.6 V2 imports — per-component runtime client emission
402
+ //
403
+ // For each component's `import:` block, resolve the `from:` reference via
404
+ // the V2 dependency resolver (4-step chain: local → npm → registry library
405
+ // → registry factory) and dispatch to the right per-runtime emitter
406
+ // (library / service / managed / executable / messaging) keyed by factory
407
+ // v2metadata.topology. This is the wire-up that makes V2 P3's framework
408
+ // actually drive code emission end-to-end.
409
+ //
410
+ // Factory binding (R3 follow-up): for each import the provider's deployment
411
+ // instance is looked up in the spec by `instance.component === resolved.resolvedName`
412
+ // across all 8 instance categories. The first `advertises:` capability is
413
+ // then resolved via manifest.capabilityMappings to an instanceFactory name,
414
+ // which is fetched from the InstanceFactoryLibrary. If the factory has
415
+ // v2metadata, it drives dispatch; otherwise we fall back to a synthetic
416
+ // library-topology factory (in-process default) so consumers without
417
+ // explicit topology bindings still get a typed wrapper.
418
+ //
419
+ // Output: clients/<provider>.ts (and servers/<provider>-server.ts for
420
+ // service topology when serverTemplate is set) under <outputDir>/backend/src/<ConsumerComponent>/.
421
+ //
422
+ // Resolver registry steps (3+4) are stubbed in Round 1; offline mode and
423
+ // local + npm sources work fully.
424
+ //
425
+ // Deliberately no-op when a component has no `import:` entries — V2 is
426
+ // additive; existing specs are unaffected.
427
+ // Helper: walk all deployments in the spec, find the first instance whose
428
+ // `component:` matches the resolved provider name. Searches across the 8
429
+ // instance categories (controllers / services / views / communications /
430
+ // storage / security / infrastructure / monitoring).
431
+ const findDeploymentInstanceForProvider = (providerName) => {
432
+ const deployments = Array.isArray(spec.deployments)
433
+ ? spec.deployments
434
+ : Object.values(spec.deployments ?? {});
435
+ for (const dep of deployments) {
436
+ const instances = dep.instances ?? {};
437
+ for (const categoryKey of Object.keys(instances)) {
438
+ const category = instances[categoryKey];
439
+ if (!category || typeof category !== 'object')
440
+ continue;
441
+ for (const instanceKey of Object.keys(category)) {
442
+ const inst = category[instanceKey];
443
+ if (inst && typeof inst === 'object' && inst.component === providerName) {
444
+ return inst;
445
+ }
446
+ }
447
+ }
448
+ }
449
+ return null;
450
+ };
451
+ // Helper: pick a factory for an import. Priority chain:
452
+ // 1. Resolved provider's deployment instance → first `advertises:` capability
453
+ // → manifest.capabilityMappings → library.get(instanceFactory).
454
+ // 2. Synthetic library-topology default (the safe in-process fallback).
455
+ const syntheticLibraryFactory = {
456
+ name: 'LibraryRuntimeDefault',
457
+ version: '1.0.0',
458
+ category: 'service',
459
+ capabilities: { provides: [], requires: [] },
460
+ technology: { runtime: 'node', framework: 'typescript', language: 'typescript' },
461
+ v2metadata: { topology: 'library', emitsDeploymentInstance: false },
462
+ };
463
+ // Helper: strip a trailing 'Client' suffix from an emitter-returned
464
+ // specifier so the v2Clients aggregator keys are clean (e.g. specifier
465
+ // 'profitCalculatorClient' → key 'profitCalculator').
466
+ const stripTrailingClient = (specifier) => specifier.endsWith('Client') ? specifier.slice(0, -'Client'.length) : specifier;
467
+ const pickFactoryForImport = (resolved) => {
468
+ const providerName = resolved.resolvedName ?? resolved.fromName;
469
+ if (!providerName)
470
+ return syntheticLibraryFactory;
471
+ const deployInstance = findDeploymentInstanceForProvider(providerName);
472
+ const advertises = deployInstance?.advertises;
473
+ const firstCap = Array.isArray(advertises) ? advertises[0] : undefined;
474
+ if (typeof firstCap !== 'string' || firstCap.length === 0) {
475
+ return syntheticLibraryFactory;
476
+ }
477
+ const mappings = this.manifest?.capabilityMappings ?? this.manifest?.manifests?.[0]?.capabilityMappings ?? [];
478
+ const mapping = (Array.isArray(mappings) ? mappings : []).find((m) => m?.capability === firstCap);
479
+ const factoryName = mapping?.instanceFactory;
480
+ if (typeof factoryName !== 'string' || factoryName.length === 0) {
481
+ return syntheticLibraryFactory;
482
+ }
483
+ // Look up the factory in the library. If it has v2metadata, use it;
484
+ // otherwise fall back to the synthetic default. Pre-V2 factories without
485
+ // v2metadata can't drive topology-specific emitters; the loader's
486
+ // applyV2MetadataDefaults fills v2metadata for canonical factories, so
487
+ // this fallback is the safety net for malformed manifests.
488
+ const factory = this.library?.get?.(factoryName);
489
+ if (factory && factory.v2metadata) {
490
+ return factory;
491
+ }
492
+ return syntheticLibraryFactory;
493
+ };
494
+ try {
495
+ const { createDependencyResolver } = await import('./resolver/index.js');
496
+ const { dispatchRuntimeEmitter, buildDefaultEmitters } = await import('./runtime-emitters/dispatcher.js');
497
+ const dependencyResolver = createDependencyResolver();
498
+ const runtimeEmitters = await buildDefaultEmitters();
499
+ let importsEmitted = 0;
500
+ // V2 P3 R3 follow-up: collect structured deployment-instance
501
+ // suggestions across every consumer component so the run produces
502
+ // a single aggregated v2-deployment-suggestions.yaml at the
503
+ // top-level outputDir. Library-topology emissions don't contribute.
504
+ const allDeploymentSuggestions = [];
505
+ const componentsList = Array.isArray(spec.components)
506
+ ? spec.components
507
+ : Object.values(spec.components ?? {});
508
+ for (const consumerComp of componentsList) {
509
+ const imports = consumerComp.imports ?? [];
510
+ if (!Array.isArray(imports) || imports.length === 0)
511
+ continue;
512
+ // Track per-component imports for the aggregator file (clients/index.ts).
513
+ // Each entry: { from: file path (without .js), specifier: client const name }
514
+ const aggregatorEntries = [];
515
+ const componentName = consumerComp.name ?? 'unknown';
516
+ const componentOutputDir = join(outputDir, 'backend', 'src', componentName);
517
+ for (const importEntry of imports) {
518
+ try {
519
+ const fromName = importEntry.from;
520
+ if (typeof fromName !== 'string' || fromName.length === 0)
521
+ continue;
522
+ const resolved = await dependencyResolver.resolve(fromName, importEntry.version, { spec, offline: !!process.env.SPECVERSE_OFFLINE });
523
+ if (!resolved)
524
+ continue;
525
+ const factory = pickFactoryForImport(resolved);
526
+ const emitter = dispatchRuntimeEmitter(factory, runtimeEmitters);
527
+ const result = await emitter.emit({
528
+ consumerComponent: consumerComp,
529
+ importEntry: {
530
+ from: fromName,
531
+ version: importEntry.version,
532
+ select: importEntry.select ?? [],
533
+ },
534
+ resolved,
535
+ factory,
536
+ outputDir: componentOutputDir,
537
+ spec: spec,
538
+ });
539
+ for (const file of result.files) {
540
+ const filePath = join(componentOutputDir, file.path);
541
+ writeOutput({ filePath, code: file.content });
542
+ importsEmitted += 1;
543
+ }
544
+ // Collect the consumer-import declarations the emitter returned so
545
+ // we can write the per-component clients/index.ts aggregator below.
546
+ for (const consumerImport of (result.imports ?? [])) {
547
+ aggregatorEntries.push(consumerImport);
548
+ }
549
+ // Collect structured deployment-instance suggestion (V2 P3 R3
550
+ // follow-up). Library emitters omit this field; only service /
551
+ // managed / executable / messaging contribute.
552
+ if (result.deploymentSuggestion) {
553
+ allDeploymentSuggestions.push(result.deploymentSuggestion);
554
+ }
555
+ }
556
+ catch (importErr) {
557
+ const compName = consumerComp.name ?? '<unknown>';
558
+ const fromName = importEntry.from ?? '<unknown>';
559
+ errors.push(`V2 import ${compName}.${fromName}: ${importErr.message}`);
560
+ }
561
+ }
562
+ // Per-component aggregator: clients/index.ts re-exports every V2
563
+ // client emitted for this consumer + a combined `v2Clients` constant
564
+ // so consumers can `import { v2Clients } from './clients'` and spread
565
+ // it into their deps object. This is the cleanest seam between the
566
+ // V2 emission phase and the per-action emitter (L3 P3) without
567
+ // requiring controller-template surgery — the consumer's existing
568
+ // code just imports + spreads.
569
+ if (aggregatorEntries.length > 0) {
570
+ const lines = [];
571
+ lines.push('// FACTORY-EMITTED — DO NOT EDIT (R8)');
572
+ lines.push('// V2 P3 imports aggregator. Re-exports every emitted client + a combined map.');
573
+ lines.push('// Consumer usage:');
574
+ lines.push('// import { v2Clients } from \'./clients\';');
575
+ lines.push('// const deps = { ...existing, ...v2Clients };');
576
+ lines.push('');
577
+ // Static imports.
578
+ for (const entry of aggregatorEntries) {
579
+ const localPath = entry.from.replace(/^\.\/clients\//, './');
580
+ lines.push(`import { ${entry.specifier} } from '${localPath}.js';`);
581
+ }
582
+ lines.push('');
583
+ // Re-exports.
584
+ for (const entry of aggregatorEntries) {
585
+ lines.push(`export { ${entry.specifier} };`);
586
+ }
587
+ lines.push('');
588
+ // Aggregated map keyed by client name with trailing 'Client' stripped
589
+ // (v2Clients.profitCalculator rather than v2Clients.profitCalculatorClient).
590
+ lines.push('export const v2Clients = {');
591
+ for (const entry of aggregatorEntries) {
592
+ lines.push(` ${stripTrailingClient(entry.specifier)}: ${entry.specifier},`);
593
+ }
594
+ lines.push('} as const;');
595
+ lines.push('');
596
+ lines.push('export type V2Clients = typeof v2Clients;');
597
+ lines.push('');
598
+ const aggregatorPath = join(componentOutputDir, 'clients', 'index.ts');
599
+ writeOutput({ filePath: aggregatorPath, code: lines.join('\n') });
600
+ }
601
+ }
602
+ if (importsEmitted > 0) {
603
+ console.log(` ✅ V2 imports: ${importsEmitted} client file(s) emitted`);
604
+ }
605
+ // V2 P3 R3 follow-up: write v2-deployment-suggestions.yaml when any
606
+ // emitter returned a structured suggestion. Auto-merge into the
607
+ // user's deployments block isn't safe (env-specific values, secrets,
608
+ // scaling parameters), so this is review-then-paste material.
609
+ if (allDeploymentSuggestions.length > 0) {
610
+ const yamlBody = yaml.dump({ suggestions: allDeploymentSuggestions }, { indent: 2, lineWidth: 100, noRefs: true, sortKeys: false });
611
+ const header = [
612
+ '# FACTORY-EMITTED — DO NOT EDIT (R8)',
613
+ '# V2 P3 R3 follow-up: deployment-instance suggestions extracted from runtime emitters.',
614
+ '#',
615
+ '# Each entry corresponds to one V2 import that produced a non-library client.',
616
+ '# Merge these into your deployments.<env>.instances.<category> block manually:',
617
+ '#',
618
+ '# deployments:',
619
+ '# production:',
620
+ '# instances:',
621
+ '# <category>:',
622
+ '# <key>:',
623
+ '# <config keys from this file>',
624
+ '#',
625
+ '# Auto-merge is intentionally NOT performed: deployment shape is user-controlled',
626
+ '# (env-specific values, secrets, scaling parameters). Review each suggestion before',
627
+ '# pasting; placeholder markers like <url> indicate values you must supply.',
628
+ '',
629
+ ].join('\n');
630
+ const suggestionsPath = join(outputDir, 'v2-deployment-suggestions.yaml');
631
+ writeOutput({ filePath: suggestionsPath, code: header + yamlBody });
632
+ console.log(` ✅ V2 deployment suggestions: ${allDeploymentSuggestions.length} entry(ies) → v2-deployment-suggestions.yaml`);
633
+ }
634
+ }
635
+ catch (e) {
636
+ errors.push(`V2 imports phase: ${e.message}`);
637
+ }
381
638
  // 3. Services
382
639
  const servicesList = Array.isArray(spec.services) ? spec.services : Object.values(spec.services || {});
383
640
  if (ctrlResolved?.instanceFactory?.codeTemplates?.services) {