@supacloud/compiler 0.47.3 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -263,6 +263,9 @@ function validateFeatureSpec(spec, module) {
263
263
  if (!spec.name.trim() || spec.states.length === 0 || spec.states.some((state) => !state.trim()) || new Set(spec.states).size !== spec.states.length) {
264
264
  error("invalid-feature-states", "Feature name and states must be non-empty; states must be unique.");
265
265
  }
266
+ if (spec.tenantField !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.tenantField)) {
267
+ error("feature-tenant-boundary-invalid", `Feature ${spec.name} declares an unsafe tenant field.`);
268
+ }
266
269
  const names = new Set;
267
270
  for (const transition of spec.transitions) {
268
271
  if (!transition.name.trim() || names.has(transition.name)) {
@@ -275,8 +278,41 @@ function validateFeatureSpec(spec, module) {
275
278
  if (transition.permission !== undefined && !transition.permission.trim()) {
276
279
  error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
277
280
  }
281
+ if (transition.persistence) {
282
+ const persistence = transition.persistence;
283
+ if (persistence.boundary === "database" && (persistence.optimisticLock !== "required" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.versionField ?? "") || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.tenantField ?? "") || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.stateField ?? "") || new Set([persistence.tenantField, persistence.versionField, persistence.stateField]).size !== 3 || persistence.receipt !== "required" || persistence.reconciliation !== "none")) {
284
+ error("feature-persistence-invalid", `Database transition ${transition.name} requires distinct safe tenant/version/state fields, optimistic locking, and a receipt, without reconciliation.`);
285
+ }
286
+ if (persistence.boundary === "database" && spec.tenantField !== persistence.tenantField) {
287
+ error("feature-tenant-boundary-drift", `Database transition ${transition.name} tenant field must match Feature ${spec.name} tenantField.`);
288
+ }
289
+ if (persistence.boundary === "external" && (persistence.optimisticLock !== "none" || persistence.versionField !== undefined || persistence.tenantField !== undefined || persistence.stateField !== undefined || persistence.receipt !== "required" || persistence.reconciliation !== "required")) {
290
+ error("feature-persistence-invalid", `External transition ${transition.name} requires a receipt and reconciliation, without database optimistic locking.`);
291
+ }
292
+ if (persistence.boundary === "external" && !transition.job) {
293
+ error("feature-job-unresolved", `External transition ${transition.name} must bind a reconciliation Job.`);
294
+ }
295
+ if (!transition.command) {
296
+ error("feature-command-unresolved", `Persistent transition ${transition.name} must reference a command.`);
297
+ }
298
+ if (!transition.permission?.trim()) {
299
+ error("feature-governance-drift", `Persistent transition ${transition.name} must declare a permission.`);
300
+ }
301
+ }
278
302
  if (!module)
279
303
  continue;
304
+ if (transition.persistence?.boundary === "database") {
305
+ for (const query of module.queries) {
306
+ if (query.tenantScope !== "required") {
307
+ error("feature-query-tenant-boundary", `Query ${query.className} in Feature ${spec.name} must declare tenantScope: "required".`);
308
+ }
309
+ }
310
+ for (const job of module.jobs ?? []) {
311
+ if (job.tenantScope !== "required") {
312
+ error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
313
+ }
314
+ }
315
+ }
280
316
  const commands = module.commands.filter((command) => command.className === transition.command || command.name === transition.command);
281
317
  const command = commands[0];
282
318
  if (transition.command && commands.length !== 1) {
@@ -291,23 +327,59 @@ function validateFeatureSpec(spec, module) {
291
327
  error("feature-governance-drift", `Transition ${transition.name} ${key} differs from command ${command.className}.`);
292
328
  }
293
329
  }
330
+ if (transition.persistence) {
331
+ if (transition.persistence.boundary === "database" && command.transaction !== "required") {
332
+ error("feature-persistence-drift", `Database transition ${transition.name} requires command ${command.className} to declare transaction: "required".`);
333
+ }
334
+ if (command.tenantScope !== "required") {
335
+ error("feature-command-tenant-boundary", `Command ${command.className} in Feature ${spec.name} must declare tenantScope: "required".`);
336
+ }
337
+ if (transition.persistence.boundary === "external" && command.transaction !== "none") {
338
+ error("feature-persistence-drift", `External transition ${transition.name} requires command ${command.className} to declare transaction: "none".`);
339
+ }
340
+ if (command.idempotency !== "required" || !command.audit) {
341
+ error("feature-persistence-drift", `Persistent transition ${transition.name} requires command ${command.className} to declare idempotency and audit.`);
342
+ }
343
+ }
344
+ }
345
+ if (transition.job) {
346
+ const jobs = (module.jobs ?? []).filter((job) => job.className === transition.job || job.name === transition.job);
347
+ if (jobs.length !== 1) {
348
+ error("feature-job-unresolved", `Transition ${transition.name} must reference exactly one Job in module ${module.name}.`);
349
+ } else {
350
+ const job = jobs[0];
351
+ if (job.tenantScope !== "required") {
352
+ error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
353
+ }
354
+ if (job.idempotency !== "required") {
355
+ error("feature-job-idempotency", `Job ${job.className} in Feature ${spec.name} must declare idempotency: "required".`);
356
+ }
357
+ if (job.mode === undefined) {
358
+ error("feature-job-mode", `Job ${job.className} in Feature ${spec.name} must declare mode: "task" or "workflow".`);
359
+ }
360
+ if (job.input === undefined || job.schemaKinds?.input !== "declared") {
361
+ error("feature-job-contract", `Job ${job.className} in Feature ${spec.name} must declare a non-opaque input schema.`);
362
+ }
363
+ const provider = module.providers.find((candidate) => candidate.token === job.className || candidate.useClass === job.className);
364
+ if (job.tenantScope === "required" && provider && !provider.deps.some((token) => isJobContextToken(token))) {
365
+ error("feature-job-context", `Job ${job.className} in Feature ${spec.name} must inject JOB_CONTEXT for trusted tenant execution.`);
366
+ }
367
+ }
294
368
  }
295
369
  if (transition.route) {
296
370
  const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
297
371
  if (routes.length !== 1) {
298
372
  error("feature-route-unresolved", `Transition ${transition.name} must reference exactly one route '${transition.route}' in module ${module.name}.`);
299
- } else if (command && routes[0]?.command !== command.className) {
373
+ } else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className) {
300
374
  error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
375
+ } else if (transition.routeBinding === "controller" && routes[0]?.command !== undefined) {
376
+ error("feature-route-drift", `Controller-bound route ${transition.route} must not also declare a framework command binding.`);
301
377
  }
302
378
  }
303
379
  }
304
380
  return diagnostics;
305
381
  }
306
382
  function generateFeatureSource(spec) {
307
- const errors = validateFeatureSpec(spec);
308
- if (errors.length)
309
- throw new Error(errors.map((error) => error.message).join(`
310
- `));
311
383
  if (spec.transitions.some((transition) => !transition.permission)) {
312
384
  throw new Error("Spec-to-Code requires an explicit permission for every transition.");
313
385
  }
@@ -315,13 +387,34 @@ function generateFeatureSource(spec) {
315
387
  ...transition,
316
388
  command: `Transition${index + 1}Command`
317
389
  }));
390
+ const errors = validateFeatureSpec({ ...spec, transitions });
391
+ if (errors.length)
392
+ throw new Error(errors.map((error) => error.message).join(`
393
+ `));
318
394
  if (transitions.some((transition) => transition.route)) {
319
395
  throw new Error("Generate the command slice first; existing HTTP routes require explicit schema and handler implementations.");
320
396
  }
321
- const sourceSpec = { name: spec.name, states: spec.states, transitions: Object.fromEntries(transitions.map((transition) => {
322
- const { name, ...options } = transition;
323
- return [name, options];
324
- })) };
397
+ const sourceSpec = {
398
+ name: spec.name,
399
+ states: spec.states,
400
+ tenantField: spec.tenantField ?? "tenant_id",
401
+ transitions: Object.fromEntries(transitions.map((transition) => {
402
+ const { name, ...options } = transition;
403
+ const persistence = transition.persistence ?? {
404
+ boundary: "database",
405
+ optimisticLock: "required",
406
+ versionField: "version",
407
+ tenantField: "tenant_id",
408
+ stateField: "state",
409
+ receipt: "required",
410
+ reconciliation: "none"
411
+ };
412
+ return [name, {
413
+ ...options,
414
+ persistence
415
+ }];
416
+ }))
417
+ };
325
418
  return [
326
419
  'import { Command, defineFeatureSlice, defineFeatureSpec } from "@supacloud/app";',
327
420
  "",
@@ -332,9 +425,10 @@ function generateFeatureSource(spec) {
332
425
  `@Command(${JSON.stringify({
333
426
  name: `${spec.name}.${transition.name}`,
334
427
  permission: transition.permission,
335
- transaction: transition.transaction ?? "none",
336
- idempotency: transition.idempotency ?? "none",
337
- audit: transition.audit
428
+ transaction: transition.transaction ?? (transition.persistence?.boundary === "external" ? "none" : "required"),
429
+ idempotency: transition.idempotency ?? "required",
430
+ audit: transition.audit ?? `${spec.name}.${transition.name}`,
431
+ tenantScope: "required"
338
432
  })})`,
339
433
  `export class ${transition.command} {`,
340
434
  ` execute(state: FeatureState): never {`,
@@ -802,13 +896,40 @@ function validateGraph(graph, options = false) {
802
896
  permission: `${module.name}.${command.name}`
803
897
  });
804
898
  }
899
+ if (command.rpc && command.branches) {
900
+ error("command-rpc-branches-conflict", `Command ${command.name} cannot declare both rpc and branches.`, module.file, module.line);
901
+ }
902
+ const branchAdapters = command.branches ?? [];
903
+ for (const branch of branchAdapters) {
904
+ if (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, branch.rpc)) {
905
+ error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${branch.rpc}'.`, module.file, module.line);
906
+ }
907
+ }
805
908
  if (command.rpc && (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, command.rpc))) {
806
909
  error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${command.rpc}'.`, module.file, module.line);
807
910
  }
808
911
  if (typeof options === "object" && options.commandCapabilities) {
809
912
  const hostCaps = options.commandCapabilities;
810
913
  const rpcCaps = command.rpc && Object.hasOwn(hostCaps.rpc ?? {}, command.rpc) ? hostCaps.rpc?.[command.rpc] : undefined;
811
- if (hostCaps.requirePersistentAdapters && (command.rpc !== undefined && (!rpcCaps?.boundary || rpcCaps.audit !== true || rpcCaps.idempotency !== true) || hostCaps.permission !== true || !command.permission || !command.audit || command.idempotency !== "required" || command.rpc !== undefined && rpcCaps?.boundary === "database" && (rpcCaps.transaction !== true || command.transaction !== "required") || command.rpc === undefined && (hostCaps.audit !== true || hostCaps.idempotency !== true || hostCaps.transaction !== true || command.transaction !== "required") || command.rpc !== undefined && rpcCaps?.boundary === "external" && rpcCaps.reconcile !== true)) {
914
+ const branchCaps = branchAdapters.map((branch) => ({
915
+ branch,
916
+ capabilities: Object.hasOwn(hostCaps.rpc ?? {}, branch.rpc) ? hostCaps.rpc?.[branch.rpc] : undefined
917
+ }));
918
+ for (const { branch, capabilities } of branchCaps) {
919
+ if (capabilities?.boundary === "external" && (branch.transaction === "required" || capabilities.transaction === true)) {
920
+ error("command-external-transaction", `Command ${command.name} branch '${branch.when}' calls an external service; use transaction: 'none'.`, module.file, module.line, "Use transaction: 'none' with durable intent and reconciliation.");
921
+ }
922
+ if (branch.audit && capabilities?.audit !== true) {
923
+ error("command-audit-unsupported", `Command ${command.name} branch '${branch.when}' declares audit, but runtime audit persistence is unavailable.`, module.file, module.line);
924
+ }
925
+ if (branch.idempotency === "required" && capabilities?.idempotency !== true) {
926
+ error("command-idempotency-unsupported", `Command ${command.name} branch '${branch.when}' declares idempotency, but receipt persistence is unavailable.`, module.file, module.line);
927
+ }
928
+ if (branch.transaction === "required" && capabilities?.transaction !== true) {
929
+ error("command-transaction-unsupported", `Command ${command.name} branch '${branch.when}' declares transaction, but transaction support is unavailable.`, module.file, module.line);
930
+ }
931
+ }
932
+ if (hostCaps.requirePersistentAdapters && (command.rpc !== undefined && (!rpcCaps?.boundary || rpcCaps.audit !== true || rpcCaps.idempotency !== true) || branchCaps.length > 0 && branchCaps.some(({ branch, capabilities }) => !capabilities?.boundary || capabilities.audit !== true || capabilities.idempotency !== true || capabilities.boundary === "database" && (capabilities.transaction !== true || branch.transaction !== "required")) || hostCaps.permission !== true || !command.permission || branchCaps.length === 0 && (!command.audit || command.idempotency !== "required") || branchCaps.length > 0 && branchCaps.some(({ branch }) => !branch.audit || branch.idempotency !== "required") || branchCaps.length > 0 && branchCaps.some(({ capabilities }) => capabilities?.boundary === "external" && capabilities.reconcile !== true) || command.rpc !== undefined && rpcCaps?.boundary === "database" && (rpcCaps.transaction !== true || command.transaction !== "required") || command.rpc === undefined && branchCaps.length === 0 && (hostCaps.audit !== true || hostCaps.idempotency !== true || hostCaps.transaction !== true || command.transaction !== "required") || command.rpc !== undefined && rpcCaps?.boundary === "external" && rpcCaps.reconcile !== true)) {
812
933
  error("command-persistence-required", `Command ${command.name} requires an explicit persistent adapter, permission, audit and idempotency policy.`, module.file, module.line, "Register a named database/external adapter, enable permission checks and declare permission, audit and required idempotency on the command.");
813
934
  }
814
935
  if (rpcCaps?.boundary === "external" && (command.transaction === "required" || rpcCaps.transaction === true)) {
@@ -1284,6 +1405,18 @@ var init_validate = __esm(() => {
1284
1405
  "invalid-feature-transition": { code: "SC6103", docsUrl: "https://supacloud.dev/errors/SC6103" },
1285
1406
  "feature-command-unresolved": { code: "SC6104", docsUrl: "https://supacloud.dev/errors/SC6104" },
1286
1407
  "feature-governance-drift": { code: "SC6105", docsUrl: "https://supacloud.dev/errors/SC6105" },
1408
+ "feature-persistence-invalid": { code: "SC6110", docsUrl: "https://supacloud.dev/errors/SC6110" },
1409
+ "feature-persistence-drift": { code: "SC6111", docsUrl: "https://supacloud.dev/errors/SC6111" },
1410
+ "feature-tenant-boundary-invalid": { code: "SC6112", docsUrl: "https://supacloud.dev/errors/SC6112" },
1411
+ "feature-tenant-boundary-drift": { code: "SC6113", docsUrl: "https://supacloud.dev/errors/SC6113" },
1412
+ "feature-query-tenant-boundary": { code: "SC6114", docsUrl: "https://supacloud.dev/errors/SC6114" },
1413
+ "feature-job-tenant-boundary": { code: "SC6115", docsUrl: "https://supacloud.dev/errors/SC6115" },
1414
+ "feature-command-tenant-boundary": { code: "SC6116", docsUrl: "https://supacloud.dev/errors/SC6116" },
1415
+ "feature-job-unresolved": { code: "SC6117", docsUrl: "https://supacloud.dev/errors/SC6117" },
1416
+ "feature-job-idempotency": { code: "SC6118", docsUrl: "https://supacloud.dev/errors/SC6118" },
1417
+ "feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
1418
+ "feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
1419
+ "feature-job-context": { code: "SC6121", docsUrl: "https://supacloud.dev/errors/SC6121" },
1287
1420
  "feature-route-unresolved": { code: "SC6106", docsUrl: "https://supacloud.dev/errors/SC6106" },
1288
1421
  "feature-route-drift": { code: "SC6107", docsUrl: "https://supacloud.dev/errors/SC6107" },
1289
1422
  "invalid-feature-spec": { code: "SC6108", docsUrl: "https://supacloud.dev/errors/SC6108" }
@@ -1294,6 +1427,8 @@ var init_validate = __esm(() => {
1294
1427
  function buildContractManifest(graph, artifacts) {
1295
1428
  const commands = [];
1296
1429
  const queries = [];
1430
+ const jobs = [];
1431
+ const features = [];
1297
1432
  const routes = [];
1298
1433
  const permissions = new Set;
1299
1434
  const rpc = [];
@@ -1305,21 +1440,95 @@ function buildContractManifest(graph, artifacts) {
1305
1440
  permissions.add(command.permission);
1306
1441
  if (command.rpc)
1307
1442
  rpc.push({ command: command.name, adapter: command.rpc });
1443
+ for (const branch of command.branches ?? []) {
1444
+ rpc.push({ command: `${command.name} [${branch.when}]`, adapter: branch.rpc });
1445
+ }
1308
1446
  if (command.audit)
1309
1447
  events.push({ name: command.audit, source: "command.audit", command: command.name });
1448
+ for (const branch of command.branches ?? []) {
1449
+ if (branch.audit)
1450
+ events.push({ name: branch.audit, source: "command.branch.audit", command: command.name });
1451
+ }
1310
1452
  commands.push({
1311
1453
  name: command.name,
1312
1454
  className: command.className,
1313
1455
  module: module.name,
1314
1456
  ...command.permission === undefined ? {} : { permission: command.permission },
1315
1457
  ...command.rpc === undefined ? {} : { rpc: command.rpc },
1458
+ ...command.branches === undefined ? {} : {
1459
+ branches: command.branches.map((branch) => ({
1460
+ when: branch.when,
1461
+ adapter: branch.rpc,
1462
+ transaction: branch.transaction,
1463
+ idempotency: branch.idempotency,
1464
+ ...branch.audit === undefined ? {} : { auditEvent: branch.audit },
1465
+ ...branch.boundary === undefined ? {} : { boundary: branch.boundary }
1466
+ }))
1467
+ },
1316
1468
  transaction: command.transaction,
1317
1469
  idempotency: command.idempotency,
1470
+ ...command.tenantScope === undefined ? {} : { tenantScope: command.tenantScope },
1318
1471
  ...command.audit === undefined ? {} : { auditEvent: command.audit }
1319
1472
  });
1320
1473
  }
1321
1474
  for (const query of module.queries) {
1322
- queries.push({ name: query.name, className: query.className, module: module.name });
1475
+ queries.push({
1476
+ name: query.name,
1477
+ className: query.className,
1478
+ module: module.name,
1479
+ ...query.tenantScope === undefined ? {} : { tenantScope: query.tenantScope }
1480
+ });
1481
+ }
1482
+ for (const job of module.jobs ?? []) {
1483
+ jobs.push({
1484
+ name: job.name,
1485
+ className: job.className,
1486
+ module: module.name,
1487
+ ...job.input === undefined ? {} : { input: job.input },
1488
+ ...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
1489
+ ...job.output === undefined ? {} : { output: job.output },
1490
+ ...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
1491
+ ...job.mode === undefined ? {} : { mode: job.mode },
1492
+ ...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
1493
+ ...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
1494
+ });
1495
+ }
1496
+ if (module.featureSpec) {
1497
+ features.push({
1498
+ name: module.featureSpec.name,
1499
+ states: [...module.featureSpec.states],
1500
+ ...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
1501
+ transitions: module.featureSpec.transitions.map((transition) => {
1502
+ const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
1503
+ return {
1504
+ name: transition.name,
1505
+ from: transition.from,
1506
+ to: transition.to,
1507
+ ...transition.permission === undefined ? {} : { permission: transition.permission },
1508
+ ...transition.command === undefined ? {} : { command: transition.command },
1509
+ ...transition.route === undefined ? {} : { route: transition.route },
1510
+ ...transition.routeBinding === undefined ? {} : { routeBinding: transition.routeBinding },
1511
+ ...transition.transaction === undefined ? {} : { transaction: transition.transaction },
1512
+ ...transition.idempotency === undefined ? {} : { idempotency: transition.idempotency },
1513
+ ...transition.audit === undefined ? {} : { audit: transition.audit },
1514
+ ...transition.job === undefined ? {} : { job: transition.job },
1515
+ ...job === undefined ? {} : {
1516
+ jobDetails: {
1517
+ name: job.name,
1518
+ className: job.className,
1519
+ ...job.input === undefined ? {} : { input: job.input },
1520
+ ...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
1521
+ ...job.output === undefined ? {} : { output: job.output },
1522
+ ...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
1523
+ ...job.mode === undefined ? {} : { mode: job.mode },
1524
+ ...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
1525
+ ...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
1526
+ }
1527
+ },
1528
+ ...transition.persistence === undefined ? {} : { persistence: transition.persistence }
1529
+ };
1530
+ })
1531
+ });
1323
1532
  }
1324
1533
  for (const controller of module.controllers) {
1325
1534
  for (const route of controller.routes) {
@@ -1350,6 +1559,8 @@ function buildContractManifest(graph, artifacts) {
1350
1559
  }
1351
1560
  commands.sort((left, right) => left.name.localeCompare(right.name));
1352
1561
  queries.sort((left, right) => left.name.localeCompare(right.name));
1562
+ jobs.sort((left, right) => left.name.localeCompare(right.name));
1563
+ features.sort((left, right) => left.name.localeCompare(right.name));
1353
1564
  routes.sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
1354
1565
  rpc.sort((left, right) => left.command.localeCompare(right.command));
1355
1566
  events.sort((left, right) => left.name.localeCompare(right.name));
@@ -1357,6 +1568,8 @@ function buildContractManifest(graph, artifacts) {
1357
1568
  version: 1,
1358
1569
  commands,
1359
1570
  queries,
1571
+ jobs,
1572
+ features,
1360
1573
  routes,
1361
1574
  permissions: [...permissions].sort(),
1362
1575
  rpc,
@@ -1504,6 +1717,7 @@ function renderApplication(graph, options) {
1504
1717
  name: command.name,
1505
1718
  permission: command.permission ?? null,
1506
1719
  rpc: command.rpc ?? null,
1720
+ branches: command.branches ?? [],
1507
1721
  transaction: command.transaction ?? null,
1508
1722
  audit: command.audit ?? null,
1509
1723
  idempotency: command.idempotency ?? null
@@ -1930,7 +2144,9 @@ ${indent(item, 2)}`).join(",")}
1930
2144
  `transaction: ${JSON.stringify(command.transaction)}`,
1931
2145
  ...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
1932
2146
  `idempotency: ${JSON.stringify(command.idempotency)}`,
2147
+ ...command.tenantScope ? [`tenantScope: ${JSON.stringify(command.tenantScope)}`] : [],
1933
2148
  ...command.rpc ? [`rpc: ${JSON.stringify(command.rpc)}`] : [],
2149
+ ...command.branches && command.branches.length > 0 ? [`branches: ${JSON.stringify(command.branches)}`] : [],
1934
2150
  ...command.standalone ? ["standalone: true"] : [],
1935
2151
  ...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`, `aspectPipeline: ${this.renderAspectPipeline(command.aspects)}`] : []
1936
2152
  ];
@@ -1963,6 +2179,8 @@ ${indent(item, 2)}`).join(",")}
1963
2179
  fields.push(`maxAttempts: ${job.maxAttempts}`);
1964
2180
  if (job.idempotency !== undefined)
1965
2181
  fields.push(`idempotency: ${JSON.stringify(job.idempotency)}`);
2182
+ if (job.tenantScope !== undefined)
2183
+ fields.push(`tenantScope: ${JSON.stringify(job.tenantScope)}`);
1966
2184
  if (job.aspects && job.aspects.length > 0) {
1967
2185
  fields.push(`aspects: ${this.renderAspects(job.aspects)}`);
1968
2186
  fields.push(`aspectPipeline: ${this.renderAspectPipeline(job.aspects)}`);
@@ -3666,17 +3884,28 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
3666
3884
 
3667
3885
  export interface CompiledCommand {
3668
3886
  rpc?: string;
3887
+ branches?: readonly CompiledCommandBranch[];
3669
3888
  className: string;
3670
3889
  name: string;
3671
3890
  permission: string;
3672
3891
  transaction: "required" | "none";
3673
3892
  audit?: string;
3674
3893
  idempotency: "required" | "none";
3894
+ tenantScope?: "required" | "none";
3675
3895
  standalone?: boolean;
3676
3896
  aspects?: CompiledAspect[];
3677
3897
  aspectPipeline?: CompiledAspectPipeline;
3678
3898
  }
3679
3899
 
3900
+ export interface CompiledCommandBranch {
3901
+ when: string;
3902
+ rpc: string;
3903
+ transaction?: "required" | "none" | string;
3904
+ audit?: string;
3905
+ idempotency?: "required" | "none" | string;
3906
+ boundary?: "database" | "external";
3907
+ }
3908
+
3680
3909
  export interface CompiledJob {
3681
3910
  className: string;
3682
3911
  name: string;
@@ -3688,6 +3917,7 @@ export interface CompiledJob {
3688
3917
  timeoutSec?: number;
3689
3918
  maxAttempts?: number;
3690
3919
  idempotency?: "required" | "none";
3920
+ tenantScope?: "required" | "none";
3691
3921
  aspects?: CompiledAspect[];
3692
3922
  aspectPipeline?: CompiledAspectPipeline;
3693
3923
  }
@@ -16219,7 +16449,7 @@ async function readDeliverySelection(manifestPath, target) {
16219
16449
  return { root, object, planned };
16220
16450
  }
16221
16451
  async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
16222
- if (object.files.length > 1024 || object.files.some((file) => file.bytes > 64 * 1024 * 1024) || object.files.reduce((bytes, file) => bytes + file.bytes, 0) > 128 * 1024 * 1024) {
16452
+ if (object.files.length > 1024 || object.files.some((file) => file.bytes > DELIVERY_ARTIFACT_MAX_FILE_BYTES) || object.files.reduce((bytes, file) => bytes + file.bytes, 0) > DELIVERY_ARTIFACT_MAX_TOTAL_BYTES) {
16223
16453
  throw new Error("Artifact inventory exceeds limits.");
16224
16454
  }
16225
16455
  const objectRoot = join7(root, "objects", object.objectId);
@@ -16258,10 +16488,12 @@ async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
16258
16488
  }
16259
16489
  return retained;
16260
16490
  }
16261
- var JSON_LIMIT = 1048576;
16491
+ var JSON_LIMIT = 1048576, DELIVERY_ARTIFACT_MAX_FILE_BYTES, DELIVERY_ARTIFACT_MAX_TOTAL_BYTES;
16262
16492
  var init_delivery_artifact = __esm(() => {
16263
16493
  init_delivery_build_schema();
16264
16494
  init_delivery_files();
16495
+ DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
16496
+ DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
16265
16497
  });
16266
16498
 
16267
16499
  // src/delivery-artifact.ts
@@ -17426,6 +17658,7 @@ var init_delivery_migration_plan = () => {};
17426
17658
 
17427
17659
  // node_modules/@supacloud/delivery/src/index.ts
17428
17660
  var init_src = __esm(() => {
17661
+ init_delivery_artifact();
17429
17662
  init_delivery_artifact();
17430
17663
  init_worker_execution();
17431
17664
  init_worker_execution();
@@ -18807,6 +19040,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
18807
19040
  if (meta && booleanProp(meta, "standalone")) {
18808
19041
  const permission = stringLiteralProp(meta, "permission");
18809
19042
  const rpc = checkedRpc(meta, ctx);
19043
+ const branches = checkedBranches(meta, ctx);
18810
19044
  const audit = stringLiteralProp(meta, "audit");
18811
19045
  const standaloneName = stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name;
18812
19046
  const standaloneUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${standaloneName}`, sourcePath(ctx.rootDir, classInfo.file), lineOf(classInfo.decl));
@@ -18816,6 +19050,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
18816
19050
  name: standaloneName,
18817
19051
  ...permission === undefined ? {} : { permission },
18818
19052
  ...rpc === undefined ? {} : { rpc },
19053
+ ...branches === undefined ? {} : { branches },
18819
19054
  transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
18820
19055
  ...audit === undefined ? {} : { audit },
18821
19056
  idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
@@ -19147,18 +19382,22 @@ function parseModule(candidate, nameByNode, ctx) {
19147
19382
  const aspects = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
19148
19383
  const permission = stringLiteralProp(meta, "permission");
19149
19384
  const rpc = checkedRpc(meta, ctx);
19385
+ const branches = checkedBranches(meta, ctx);
19150
19386
  const audit = stringLiteralProp(meta, "audit");
19151
19387
  const commandName = stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>";
19152
19388
  const uses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19153
19389
  rejectUndeclaredResourceUses(uses, moduleResourceNames, ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19390
+ const tenantScope = commandModeProp(meta, "tenantScope");
19154
19391
  commands.push({
19155
19392
  className: cls.name?.text ?? "<anonymous>",
19156
19393
  name: commandName,
19157
19394
  ...permission === undefined ? {} : { permission },
19158
19395
  ...rpc === undefined ? {} : { rpc },
19396
+ ...branches === undefined ? {} : { branches },
19159
19397
  transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
19160
19398
  ...audit === undefined ? {} : { audit },
19161
19399
  idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
19400
+ ...tenantScope === undefined ? {} : { tenantScope },
19162
19401
  ...booleanProp(meta, "standalone") ? { standalone: true } : {},
19163
19402
  ...uses.length > 0 ? { uses } : {},
19164
19403
  ...aspects.length > 0 ? { aspects } : {}
@@ -19189,12 +19428,14 @@ function parseModule(candidate, nameByNode, ctx) {
19189
19428
  const contract = parseJobOptions(meta, className, ctx);
19190
19429
  const jobUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19191
19430
  rejectUndeclaredResourceUses(jobUses, moduleResourceNames, ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19431
+ const tenantScope = commandModeProp(meta, "tenantScope");
19192
19432
  jobs.push({
19193
19433
  className,
19194
19434
  name: stringLiteralProp(meta, "name") ?? className,
19195
19435
  serviceKey: camelName(provider?.token ?? className),
19196
19436
  scope,
19197
19437
  ...contract,
19438
+ ...tenantScope === undefined ? {} : { tenantScope },
19198
19439
  ...jobUses.length > 0 ? { uses: jobUses } : {},
19199
19440
  ...aspects.length > 0 ? { aspects } : {}
19200
19441
  });
@@ -19204,9 +19445,11 @@ function parseModule(candidate, nameByNode, ctx) {
19204
19445
  if (queryDec) {
19205
19446
  const meta = decoratorObjectArg(queryDec);
19206
19447
  if (meta) {
19448
+ const tenantScope = commandModeProp(meta, "tenantScope");
19207
19449
  queries.push({
19208
19450
  className: cls.name?.text ?? "<anonymous>",
19209
- name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
19451
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
19452
+ ...tenantScope === undefined ? {} : { tenantScope }
19210
19453
  });
19211
19454
  }
19212
19455
  }
@@ -19261,6 +19504,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19261
19504
  if (!ts7.isObjectLiteralExpression(input))
19262
19505
  return invalid();
19263
19506
  const name = stringLiteralProp(input, "name");
19507
+ const tenantField = stringLiteralProp(input, "tenantField");
19264
19508
  const statesExpr = getProp(input, "states");
19265
19509
  const transitionObject = getProp(input, "transitions");
19266
19510
  if (!name || !statesExpr || !ts7.isArrayLiteralExpression(statesExpr) || statesExpr.elements.some((state) => !ts7.isStringLiteral(state)) || !transitionObject || !ts7.isObjectLiteralExpression(transitionObject) || input.properties.some((property) => !ts7.isPropertyAssignment(property))) {
@@ -19279,15 +19523,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19279
19523
  const options = property.initializer;
19280
19524
  const from = stringLiteralProp(options, "from");
19281
19525
  const to = stringLiteralProp(options, "to");
19282
- if (!from || !to || options.properties.some((prop) => !ts7.isPropertyAssignment(prop)) || ["permission", "command", "route", "audit"].some((key) => getProp(options, key) && !stringLiteralProp(options, key)) || ["transaction", "idempotency"].some((key) => getProp(options, key) && !commandModeProp(options, key))) {
19526
+ if (!from || !to || options.properties.some((prop) => !ts7.isPropertyAssignment(prop)) || ["permission", "command", "route", "routeBinding", "audit", "job"].some((key) => getProp(options, key) && !stringLiteralProp(options, key)) || ["transaction", "idempotency"].some((key) => getProp(options, key) && !commandModeProp(options, key))) {
19283
19527
  return invalid();
19284
19528
  }
19285
19529
  const permission = stringLiteralProp(options, "permission");
19286
19530
  const command = stringLiteralProp(options, "command");
19287
19531
  const route = stringLiteralProp(options, "route");
19532
+ const routeBinding = stringLiteralProp(options, "routeBinding");
19533
+ if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
19534
+ return invalid();
19288
19535
  const transaction = commandModeProp(options, "transaction");
19289
19536
  const idempotency = commandModeProp(options, "idempotency");
19290
19537
  const audit = stringLiteralProp(options, "audit");
19538
+ const job = stringLiteralProp(options, "job");
19539
+ const persistenceExpr = getProp(options, "persistence");
19540
+ const persistence = persistenceExpr && ts7.isObjectLiteralExpression(persistenceExpr) ? parseFeaturePersistence(persistenceExpr) : undefined;
19541
+ if (persistenceExpr && !persistence)
19542
+ return invalid();
19291
19543
  transitions.push({
19292
19544
  name: propertyName(property.name),
19293
19545
  from,
@@ -19295,12 +19547,44 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19295
19547
  ...permission === undefined ? {} : { permission },
19296
19548
  ...command === undefined ? {} : { command },
19297
19549
  ...route === undefined ? {} : { route },
19550
+ ...routeBinding === undefined ? {} : { routeBinding },
19298
19551
  ...transaction === undefined ? {} : { transaction },
19299
19552
  ...idempotency === undefined ? {} : { idempotency },
19300
- ...audit === undefined ? {} : { audit }
19553
+ ...audit === undefined ? {} : { audit },
19554
+ ...job === undefined ? {} : { job },
19555
+ ...persistence === undefined ? {} : { persistence }
19301
19556
  });
19302
19557
  }
19303
- return { name, states, transitions, file: sourcePath(ctx.rootDir, input.getSourceFile().fileName), line: lineOf(input) };
19558
+ return {
19559
+ name,
19560
+ states,
19561
+ ...tenantField === undefined ? {} : { tenantField },
19562
+ transitions,
19563
+ file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
19564
+ line: lineOf(input)
19565
+ };
19566
+ }
19567
+ function parseFeaturePersistence(input) {
19568
+ if (input.properties.some((property) => !ts7.isPropertyAssignment(property)))
19569
+ return;
19570
+ const boundary = stringLiteralProp(input, "boundary");
19571
+ const optimisticLock = commandModeProp(input, "optimisticLock");
19572
+ const versionField = stringLiteralProp(input, "versionField");
19573
+ const tenantField = stringLiteralProp(input, "tenantField");
19574
+ const stateField = stringLiteralProp(input, "stateField");
19575
+ const receipt = commandModeProp(input, "receipt");
19576
+ const reconciliation = commandModeProp(input, "reconciliation");
19577
+ if (boundary !== "database" && boundary !== "external" || optimisticLock !== "required" && optimisticLock !== "none" || receipt !== "required" && receipt !== "none" || reconciliation !== "required" && reconciliation !== "none")
19578
+ return;
19579
+ return {
19580
+ boundary,
19581
+ optimisticLock,
19582
+ ...versionField === undefined ? {} : { versionField },
19583
+ ...tenantField === undefined ? {} : { tenantField },
19584
+ ...stateField === undefined ? {} : { stateField },
19585
+ receipt,
19586
+ reconciliation
19587
+ };
19304
19588
  }
19305
19589
  function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
19306
19590
  if (!input || seen.has(input))
@@ -20727,6 +21011,75 @@ function checkedRpc(meta, ctx) {
20727
21011
  });
20728
21012
  return;
20729
21013
  }
21014
+ function checkedBranches(meta, ctx) {
21015
+ const expression = getProp(meta, "branches");
21016
+ if (!expression)
21017
+ return;
21018
+ if (!ts7.isArrayLiteralExpression(expression)) {
21019
+ ctx.diagnostics.push({
21020
+ severity: "error",
21021
+ code: "invalid-command-branches",
21022
+ file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
21023
+ line: lineOf(expression),
21024
+ message: "Command branches must be an explicit object array.",
21025
+ suggestion: "Declare each branch as { when, rpc, transaction, audit, idempotency, boundary }."
21026
+ });
21027
+ return;
21028
+ }
21029
+ const branches = [];
21030
+ for (const item of expression.elements) {
21031
+ if (!ts7.isObjectLiteralExpression(item)) {
21032
+ ctx.diagnostics.push({
21033
+ severity: "error",
21034
+ code: "invalid-command-branches",
21035
+ file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
21036
+ line: lineOf(item),
21037
+ message: "Command branches entries must be object literals."
21038
+ });
21039
+ continue;
21040
+ }
21041
+ const when = stringLiteralProp(item, "when");
21042
+ const rpc = stringLiteralProp(item, "rpc");
21043
+ const transaction = checkedCommandMode(item, "transaction", ctx, "command branch") ?? "none";
21044
+ const audit = stringLiteralProp(item, "audit");
21045
+ const idempotency = checkedCommandMode(item, "idempotency", ctx, "command branch") ?? "none";
21046
+ const boundary = stringLiteralProp(item, "boundary");
21047
+ const validBoundary = boundary === undefined || boundary === "database" || boundary === "external";
21048
+ const validWhen = when !== undefined && /^(?:[A-Za-z_$][\w$]*)(?:\.[A-Za-z_$][\w$]*)*\s*===\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(when);
21049
+ if (!when || !validWhen || !rpc?.trim() || !validBoundary) {
21050
+ ctx.diagnostics.push({
21051
+ severity: "error",
21052
+ code: "invalid-command-branches",
21053
+ file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
21054
+ line: lineOf(item),
21055
+ message: "Command branches require a restricted equality selector, a non-empty rpc, and a valid boundary.",
21056
+ suggestion: `Use when: 'resource === "value"' (or body.resource/params.resource/query.resource), plus a named RPC adapter.`
21057
+ });
21058
+ continue;
21059
+ }
21060
+ branches.push({
21061
+ when,
21062
+ rpc,
21063
+ transaction,
21064
+ ...audit === undefined ? {} : { audit },
21065
+ idempotency,
21066
+ ...boundary === undefined ? {} : { boundary }
21067
+ });
21068
+ }
21069
+ if (branches.length === 0)
21070
+ return [];
21071
+ if (new Set(branches.map((branch) => branch.when)).size !== branches.length) {
21072
+ ctx.diagnostics.push({
21073
+ severity: "error",
21074
+ code: "duplicate-command-branch",
21075
+ file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
21076
+ line: lineOf(expression),
21077
+ message: "Command branches must use distinct selectors.",
21078
+ suggestion: "Make each branch selector mutually distinct."
21079
+ });
21080
+ }
21081
+ return branches;
21082
+ }
20730
21083
  function classDeps(cls, ctx) {
20731
21084
  const injectable = parseInjectableOptions(cls, ctx);
20732
21085
  const ctor = cls.members.find(ts7.isConstructorDeclaration);
@@ -24415,10 +24768,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
24415
24768
  import { resolve as resolve18 } from "node:path";
24416
24769
 
24417
24770
  // src/migration-app-version.ts
24418
- var MIGRATION_APP_VERSION = "0.28.3";
24771
+ var MIGRATION_APP_VERSION = "0.29.0";
24419
24772
 
24420
24773
  // src/migration-elysia-version.ts
24421
- var MIGRATION_ELYSIA_VERSION = "0.33.5";
24774
+ var MIGRATION_ELYSIA_VERSION = "0.34.0";
24422
24775
 
24423
24776
  // src/migration-policy.ts
24424
24777
  var cachedCompilerVersion;
@@ -25388,6 +25741,163 @@ function isRecord2(value) {
25388
25741
  function arrayLength(value) {
25389
25742
  return Array.isArray(value) ? value.length : undefined;
25390
25743
  }
25744
+ function safeField(value) {
25745
+ return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
25746
+ }
25747
+ function validFeaturePersistence(value) {
25748
+ if (!isRecord2(value))
25749
+ return false;
25750
+ const boundary = value["boundary"];
25751
+ const receipt = value["receipt"];
25752
+ const reconciliation = value["reconciliation"];
25753
+ if (boundary === "database") {
25754
+ const fields = [value["tenantField"], value["versionField"], value["stateField"]];
25755
+ return value["optimisticLock"] === "required" && fields.every(safeField) && new Set(fields).size === 3 && receipt === "required" && reconciliation === "none";
25756
+ }
25757
+ if (boundary === "external") {
25758
+ return value["optimisticLock"] === "none" && value["versionField"] === undefined && value["tenantField"] === undefined && value["stateField"] === undefined && receipt === "required" && reconciliation === "required";
25759
+ }
25760
+ return false;
25761
+ }
25762
+ function validCommandInventory(value) {
25763
+ const names = new Set;
25764
+ const classes = new Set;
25765
+ return Array.isArray(value) && value.every((command) => {
25766
+ if (!isRecord2(command) || typeof command["name"] !== "string" || command["name"].trim() === "" || typeof command["className"] !== "string" || command["className"].trim() === "" || typeof command["module"] !== "string" || command["module"].trim() === "" || typeof command["permission"] !== "string" || command["permission"].trim() === "" || command["transaction"] !== "required" && command["transaction"] !== "none" || command["idempotency"] !== "required" && command["idempotency"] !== "none" || command["tenantScope"] !== undefined && command["tenantScope"] !== "required" && command["tenantScope"] !== "none")
25767
+ return false;
25768
+ if (names.has(command["name"]) || classes.has(command["className"]))
25769
+ return false;
25770
+ if (command["rpc"] !== undefined && (typeof command["rpc"] !== "string" || command["rpc"].trim() === ""))
25771
+ return false;
25772
+ if (command["auditEvent"] !== undefined && (typeof command["auditEvent"] !== "string" || command["auditEvent"].trim() === ""))
25773
+ return false;
25774
+ names.add(command["name"]);
25775
+ classes.add(command["className"]);
25776
+ return true;
25777
+ });
25778
+ }
25779
+ function validFeatureInventory(value, jobs, commands, routes, rpc) {
25780
+ const featureNames = new Set;
25781
+ return Array.isArray(value) && value.every((feature) => {
25782
+ if (!isRecord2(feature) || typeof feature["name"] !== "string" || feature["name"].trim() === "")
25783
+ return false;
25784
+ if (featureNames.has(feature["name"]))
25785
+ return false;
25786
+ featureNames.add(feature["name"]);
25787
+ if (feature["tenantField"] !== undefined && !safeField(feature["tenantField"]))
25788
+ return false;
25789
+ const states = feature["states"];
25790
+ const transitions = feature["transitions"];
25791
+ if (!Array.isArray(states) || states.length === 0 || states.some((state) => typeof state !== "string" || state.trim() === "") || new Set(states).size !== states.length || !Array.isArray(transitions))
25792
+ return false;
25793
+ const transitionNames = new Set;
25794
+ return transitions.every((transition) => {
25795
+ if (!isRecord2(transition) || typeof transition["name"] !== "string" || transition["name"].trim() === "" || typeof transition["from"] !== "string" || !states.includes(transition["from"]) || typeof transition["to"] !== "string" || !states.includes(transition["to"]))
25796
+ return false;
25797
+ if (transitionNames.has(transition["name"]))
25798
+ return false;
25799
+ transitionNames.add(transition["name"]);
25800
+ const persistence = transition["persistence"];
25801
+ if (persistence !== undefined) {
25802
+ if (!isRecord2(persistence) || !validFeaturePersistence(persistence))
25803
+ return false;
25804
+ if (typeof transition["command"] !== "string" || transition["command"].trim() === "" || typeof transition["permission"] !== "string" || transition["permission"].trim() === "")
25805
+ return false;
25806
+ if (persistence["boundary"] === "database" && feature["tenantField"] !== persistence["tenantField"])
25807
+ return false;
25808
+ }
25809
+ const commandName = transition["command"];
25810
+ if (commandName !== undefined) {
25811
+ const command = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
25812
+ if (!isRecord2(command))
25813
+ return false;
25814
+ if (transition["permission"] !== undefined && command["permission"] !== transition["permission"])
25815
+ return false;
25816
+ if (transition["transaction"] !== undefined && command["transaction"] !== transition["transaction"])
25817
+ return false;
25818
+ if (transition["idempotency"] !== undefined && command["idempotency"] !== transition["idempotency"])
25819
+ return false;
25820
+ if (transition["audit"] !== undefined && command["auditEvent"] !== transition["audit"])
25821
+ return false;
25822
+ if (persistence !== undefined) {
25823
+ const adapter = command["rpc"];
25824
+ if (typeof adapter !== "string" || adapter.trim() === "" || !rpc.some((candidate) => isRecord2(candidate) && candidate["command"] === command["name"] && candidate["adapter"] === adapter) || command["idempotency"] !== "required" || command["tenantScope"] !== "required" || typeof command["auditEvent"] !== "string" || command["auditEvent"].trim() === "")
25825
+ return false;
25826
+ const expectedTransaction = persistence["boundary"] === "database" ? "required" : "none";
25827
+ if (command["transaction"] !== expectedTransaction)
25828
+ return false;
25829
+ }
25830
+ }
25831
+ const routeName = transition["route"];
25832
+ const routeBinding = transition["routeBinding"];
25833
+ if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
25834
+ return false;
25835
+ if (routeName !== undefined) {
25836
+ const route = routes.find((candidate) => isRecord2(candidate) && `${candidate["method"]} ${candidate["path"]}` === routeName);
25837
+ if (!isRecord2(route))
25838
+ return false;
25839
+ if (routeBinding === "controller") {
25840
+ if (route["command"] !== undefined)
25841
+ return false;
25842
+ } else if (route["command"] !== commandName) {
25843
+ return false;
25844
+ }
25845
+ }
25846
+ const job = transition["job"];
25847
+ if (job === undefined)
25848
+ return true;
25849
+ const details = transition["jobDetails"];
25850
+ const inventory = jobs.find((candidate) => isRecord2(candidate) && (candidate["name"] === job || candidate["className"] === job));
25851
+ const contractFields = ["input", "inputKind", "output", "outputKind", "mode", "idempotency", "tenantScope"];
25852
+ return typeof job === "string" && job.trim() !== "" && isRecord2(details) && isRecord2(inventory) && (details["name"] === job || details["className"] === job) && typeof details["className"] === "string" && details["className"].trim() !== "" && details["inputKind"] === "declared" && (details["mode"] === "task" || details["mode"] === "workflow") && details["idempotency"] === "required" && details["tenantScope"] === "required" && contractFields.every((field) => details[field] === inventory[field]);
25853
+ });
25854
+ });
25855
+ }
25856
+ function validJobInventory(value) {
25857
+ const names = new Set;
25858
+ const classes = new Set;
25859
+ return Array.isArray(value) && value.every((job) => {
25860
+ if (!isRecord2(job) || typeof job["name"] !== "string" || job["name"].trim() === "" || typeof job["className"] !== "string" || job["className"].trim() === "" || typeof job["module"] !== "string" || job["module"].trim() === "")
25861
+ return false;
25862
+ if (names.has(job["name"]) || classes.has(job["className"]))
25863
+ return false;
25864
+ names.add(job["name"]);
25865
+ classes.add(job["className"]);
25866
+ if (job["input"] !== undefined && (typeof job["input"] !== "string" || job["input"].trim() === ""))
25867
+ return false;
25868
+ if (job["inputKind"] !== undefined && job["inputKind"] !== "opaque" && job["inputKind"] !== "declared")
25869
+ return false;
25870
+ if (job["output"] !== undefined && (typeof job["output"] !== "string" || job["output"].trim() === ""))
25871
+ return false;
25872
+ if (job["outputKind"] !== undefined && job["outputKind"] !== "opaque" && job["outputKind"] !== "declared")
25873
+ return false;
25874
+ if (job["mode"] !== undefined && job["mode"] !== "task" && job["mode"] !== "workflow")
25875
+ return false;
25876
+ if (job["idempotency"] !== undefined && job["idempotency"] !== "required" && job["idempotency"] !== "none")
25877
+ return false;
25878
+ return job["tenantScope"] === undefined || job["tenantScope"] === "required" || job["tenantScope"] === "none";
25879
+ });
25880
+ }
25881
+ function validRpcInventory(value, commandInventory) {
25882
+ const commandNames = new Set;
25883
+ const adapters = new Set;
25884
+ return Array.isArray(value) && value.every((rpc) => {
25885
+ if (!isRecord2(rpc) || typeof rpc["command"] !== "string" || rpc["command"].trim() === "" || typeof rpc["adapter"] !== "string" || rpc["adapter"].trim() === "")
25886
+ return false;
25887
+ if (commandNames.has(rpc["command"]) || adapters.has(rpc["adapter"]))
25888
+ return false;
25889
+ const command = commandInventory.find((candidate) => isRecord2(candidate) && candidate["name"] === rpc["command"] && candidate["rpc"] === rpc["adapter"]);
25890
+ if (!isRecord2(command))
25891
+ return false;
25892
+ commandNames.add(rpc["command"]);
25893
+ adapters.add(rpc["adapter"]);
25894
+ return true;
25895
+ }) && commandInventory.every((command) => {
25896
+ if (!isRecord2(command) || command["rpc"] === undefined)
25897
+ return true;
25898
+ return value.some((rpc) => isRecord2(rpc) && rpc["command"] === command["name"] && rpc["adapter"] === command["rpc"]);
25899
+ });
25900
+ }
25391
25901
  async function sha2566(path) {
25392
25902
  return createHash11("sha256").update(await readFile11(path)).digest("hex");
25393
25903
  }
@@ -25457,19 +25967,24 @@ async function assessMigration(options) {
25457
25967
  };
25458
25968
  try {
25459
25969
  const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
25460
- if (!isRecord2(parsed) || parsed["version"] !== 1)
25970
+ if (!isRecord2(parsed) || parsed["version"] !== 1 || !validJobInventory(parsed["jobs"]) || !validCommandInventory(parsed["commands"]) || !Array.isArray(parsed["routes"]) || !validRpcInventory(parsed["rpc"], parsed["commands"]) || !validFeatureInventory(parsed["features"], parsed["jobs"], parsed["commands"], parsed["routes"], parsed["rpc"])) {
25461
25971
  throw new Error("invalid manifest");
25972
+ }
25462
25973
  const commands = arrayLength(parsed["commands"]);
25974
+ const jobs = arrayLength(parsed["jobs"]);
25463
25975
  const routes = arrayLength(parsed["routes"]);
25464
25976
  const permissions = arrayLength(parsed["permissions"]);
25977
+ const features = arrayLength(parsed["features"]);
25465
25978
  contractsManifest = {
25466
25979
  path: contractsManifestPath,
25467
25980
  present: true,
25468
25981
  sha256: await sha2566(contractsManifestPath),
25469
25982
  version: 1,
25470
25983
  ...commands === undefined ? {} : { commands },
25984
+ ...jobs === undefined ? {} : { jobs },
25471
25985
  ...routes === undefined ? {} : { routes },
25472
- ...permissions === undefined ? {} : { permissions }
25986
+ ...permissions === undefined ? {} : { permissions },
25987
+ ...features === undefined ? {} : { features }
25473
25988
  };
25474
25989
  } catch {
25475
25990
  findings.push({