@supacloud/compiler 0.47.2 → 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
  }
@@ -4609,6 +4839,7 @@ function createContextPack(graph, subject) {
4609
4839
  } : undefined;
4610
4840
  const files = [...new Set(modules.flatMap((module) => [
4611
4841
  module.file,
4842
+ ...module.featureSpec?.file ? [module.featureSpec.file] : [],
4612
4843
  ...module.providers.map((provider) => provider.file),
4613
4844
  ...module.controllers.map((controller) => controller.file),
4614
4845
  ...graph.moduleHandlerFiles?.[module.name] ?? [],
@@ -16218,7 +16449,7 @@ async function readDeliverySelection(manifestPath, target) {
16218
16449
  return { root, object, planned };
16219
16450
  }
16220
16451
  async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
16221
- 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) {
16222
16453
  throw new Error("Artifact inventory exceeds limits.");
16223
16454
  }
16224
16455
  const objectRoot = join7(root, "objects", object.objectId);
@@ -16257,10 +16488,12 @@ async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
16257
16488
  }
16258
16489
  return retained;
16259
16490
  }
16260
- var JSON_LIMIT = 1048576;
16491
+ var JSON_LIMIT = 1048576, DELIVERY_ARTIFACT_MAX_FILE_BYTES, DELIVERY_ARTIFACT_MAX_TOTAL_BYTES;
16261
16492
  var init_delivery_artifact = __esm(() => {
16262
16493
  init_delivery_build_schema();
16263
16494
  init_delivery_files();
16495
+ DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
16496
+ DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
16264
16497
  });
16265
16498
 
16266
16499
  // src/delivery-artifact.ts
@@ -17425,6 +17658,7 @@ var init_delivery_migration_plan = () => {};
17425
17658
 
17426
17659
  // node_modules/@supacloud/delivery/src/index.ts
17427
17660
  var init_src = __esm(() => {
17661
+ init_delivery_artifact();
17428
17662
  init_delivery_artifact();
17429
17663
  init_worker_execution();
17430
17664
  init_worker_execution();
@@ -18806,6 +19040,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
18806
19040
  if (meta && booleanProp(meta, "standalone")) {
18807
19041
  const permission = stringLiteralProp(meta, "permission");
18808
19042
  const rpc = checkedRpc(meta, ctx);
19043
+ const branches = checkedBranches(meta, ctx);
18809
19044
  const audit = stringLiteralProp(meta, "audit");
18810
19045
  const standaloneName = stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name;
18811
19046
  const standaloneUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${standaloneName}`, sourcePath(ctx.rootDir, classInfo.file), lineOf(classInfo.decl));
@@ -18815,6 +19050,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
18815
19050
  name: standaloneName,
18816
19051
  ...permission === undefined ? {} : { permission },
18817
19052
  ...rpc === undefined ? {} : { rpc },
19053
+ ...branches === undefined ? {} : { branches },
18818
19054
  transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
18819
19055
  ...audit === undefined ? {} : { audit },
18820
19056
  idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
@@ -19146,18 +19382,22 @@ function parseModule(candidate, nameByNode, ctx) {
19146
19382
  const aspects = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
19147
19383
  const permission = stringLiteralProp(meta, "permission");
19148
19384
  const rpc = checkedRpc(meta, ctx);
19385
+ const branches = checkedBranches(meta, ctx);
19149
19386
  const audit = stringLiteralProp(meta, "audit");
19150
19387
  const commandName = stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>";
19151
19388
  const uses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19152
19389
  rejectUndeclaredResourceUses(uses, moduleResourceNames, ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19390
+ const tenantScope = commandModeProp(meta, "tenantScope");
19153
19391
  commands.push({
19154
19392
  className: cls.name?.text ?? "<anonymous>",
19155
19393
  name: commandName,
19156
19394
  ...permission === undefined ? {} : { permission },
19157
19395
  ...rpc === undefined ? {} : { rpc },
19396
+ ...branches === undefined ? {} : { branches },
19158
19397
  transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
19159
19398
  ...audit === undefined ? {} : { audit },
19160
19399
  idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
19400
+ ...tenantScope === undefined ? {} : { tenantScope },
19161
19401
  ...booleanProp(meta, "standalone") ? { standalone: true } : {},
19162
19402
  ...uses.length > 0 ? { uses } : {},
19163
19403
  ...aspects.length > 0 ? { aspects } : {}
@@ -19188,12 +19428,14 @@ function parseModule(candidate, nameByNode, ctx) {
19188
19428
  const contract = parseJobOptions(meta, className, ctx);
19189
19429
  const jobUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19190
19430
  rejectUndeclaredResourceUses(jobUses, moduleResourceNames, ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19431
+ const tenantScope = commandModeProp(meta, "tenantScope");
19191
19432
  jobs.push({
19192
19433
  className,
19193
19434
  name: stringLiteralProp(meta, "name") ?? className,
19194
19435
  serviceKey: camelName(provider?.token ?? className),
19195
19436
  scope,
19196
19437
  ...contract,
19438
+ ...tenantScope === undefined ? {} : { tenantScope },
19197
19439
  ...jobUses.length > 0 ? { uses: jobUses } : {},
19198
19440
  ...aspects.length > 0 ? { aspects } : {}
19199
19441
  });
@@ -19203,9 +19445,11 @@ function parseModule(candidate, nameByNode, ctx) {
19203
19445
  if (queryDec) {
19204
19446
  const meta = decoratorObjectArg(queryDec);
19205
19447
  if (meta) {
19448
+ const tenantScope = commandModeProp(meta, "tenantScope");
19206
19449
  queries.push({
19207
19450
  className: cls.name?.text ?? "<anonymous>",
19208
- name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
19451
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
19452
+ ...tenantScope === undefined ? {} : { tenantScope }
19209
19453
  });
19210
19454
  }
19211
19455
  }
@@ -19260,6 +19504,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19260
19504
  if (!ts7.isObjectLiteralExpression(input))
19261
19505
  return invalid();
19262
19506
  const name = stringLiteralProp(input, "name");
19507
+ const tenantField = stringLiteralProp(input, "tenantField");
19263
19508
  const statesExpr = getProp(input, "states");
19264
19509
  const transitionObject = getProp(input, "transitions");
19265
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))) {
@@ -19278,15 +19523,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19278
19523
  const options = property.initializer;
19279
19524
  const from = stringLiteralProp(options, "from");
19280
19525
  const to = stringLiteralProp(options, "to");
19281
- 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))) {
19282
19527
  return invalid();
19283
19528
  }
19284
19529
  const permission = stringLiteralProp(options, "permission");
19285
19530
  const command = stringLiteralProp(options, "command");
19286
19531
  const route = stringLiteralProp(options, "route");
19532
+ const routeBinding = stringLiteralProp(options, "routeBinding");
19533
+ if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
19534
+ return invalid();
19287
19535
  const transaction = commandModeProp(options, "transaction");
19288
19536
  const idempotency = commandModeProp(options, "idempotency");
19289
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();
19290
19543
  transitions.push({
19291
19544
  name: propertyName(property.name),
19292
19545
  from,
@@ -19294,12 +19547,44 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19294
19547
  ...permission === undefined ? {} : { permission },
19295
19548
  ...command === undefined ? {} : { command },
19296
19549
  ...route === undefined ? {} : { route },
19550
+ ...routeBinding === undefined ? {} : { routeBinding },
19297
19551
  ...transaction === undefined ? {} : { transaction },
19298
19552
  ...idempotency === undefined ? {} : { idempotency },
19299
- ...audit === undefined ? {} : { audit }
19553
+ ...audit === undefined ? {} : { audit },
19554
+ ...job === undefined ? {} : { job },
19555
+ ...persistence === undefined ? {} : { persistence }
19300
19556
  });
19301
19557
  }
19302
- 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
+ };
19303
19588
  }
19304
19589
  function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
19305
19590
  if (!input || seen.has(input))
@@ -20726,6 +21011,75 @@ function checkedRpc(meta, ctx) {
20726
21011
  });
20727
21012
  return;
20728
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
+ }
20729
21083
  function classDeps(cls, ctx) {
20730
21084
  const injectable = parseInjectableOptions(cls, ctx);
20731
21085
  const ctor = cls.members.find(ts7.isConstructorDeclaration);
@@ -24414,10 +24768,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
24414
24768
  import { resolve as resolve18 } from "node:path";
24415
24769
 
24416
24770
  // src/migration-app-version.ts
24417
- var MIGRATION_APP_VERSION = "0.28.2";
24771
+ var MIGRATION_APP_VERSION = "0.29.0";
24418
24772
 
24419
24773
  // src/migration-elysia-version.ts
24420
- var MIGRATION_ELYSIA_VERSION = "0.33.4";
24774
+ var MIGRATION_ELYSIA_VERSION = "0.34.0";
24421
24775
 
24422
24776
  // src/migration-policy.ts
24423
24777
  var cachedCompilerVersion;
@@ -25387,6 +25741,163 @@ function isRecord2(value) {
25387
25741
  function arrayLength(value) {
25388
25742
  return Array.isArray(value) ? value.length : undefined;
25389
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
+ }
25390
25901
  async function sha2566(path) {
25391
25902
  return createHash11("sha256").update(await readFile11(path)).digest("hex");
25392
25903
  }
@@ -25456,19 +25967,24 @@ async function assessMigration(options) {
25456
25967
  };
25457
25968
  try {
25458
25969
  const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
25459
- 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"])) {
25460
25971
  throw new Error("invalid manifest");
25972
+ }
25461
25973
  const commands = arrayLength(parsed["commands"]);
25974
+ const jobs = arrayLength(parsed["jobs"]);
25462
25975
  const routes = arrayLength(parsed["routes"]);
25463
25976
  const permissions = arrayLength(parsed["permissions"]);
25977
+ const features = arrayLength(parsed["features"]);
25464
25978
  contractsManifest = {
25465
25979
  path: contractsManifestPath,
25466
25980
  present: true,
25467
25981
  sha256: await sha2566(contractsManifestPath),
25468
25982
  version: 1,
25469
25983
  ...commands === undefined ? {} : { commands },
25984
+ ...jobs === undefined ? {} : { jobs },
25470
25985
  ...routes === undefined ? {} : { routes },
25471
- ...permissions === undefined ? {} : { permissions }
25986
+ ...permissions === undefined ? {} : { permissions },
25987
+ ...features === undefined ? {} : { features }
25472
25988
  };
25473
25989
  } catch {
25474
25990
  findings.push({