@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/index.js CHANGED
@@ -262,6 +262,9 @@ function validateFeatureSpec(spec, module) {
262
262
  if (!spec.name.trim() || spec.states.length === 0 || spec.states.some((state) => !state.trim()) || new Set(spec.states).size !== spec.states.length) {
263
263
  error("invalid-feature-states", "Feature name and states must be non-empty; states must be unique.");
264
264
  }
265
+ if (spec.tenantField !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.tenantField)) {
266
+ error("feature-tenant-boundary-invalid", `Feature ${spec.name} declares an unsafe tenant field.`);
267
+ }
265
268
  const names = new Set;
266
269
  for (const transition of spec.transitions) {
267
270
  if (!transition.name.trim() || names.has(transition.name)) {
@@ -274,8 +277,41 @@ function validateFeatureSpec(spec, module) {
274
277
  if (transition.permission !== undefined && !transition.permission.trim()) {
275
278
  error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
276
279
  }
280
+ if (transition.persistence) {
281
+ const persistence = transition.persistence;
282
+ 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")) {
283
+ error("feature-persistence-invalid", `Database transition ${transition.name} requires distinct safe tenant/version/state fields, optimistic locking, and a receipt, without reconciliation.`);
284
+ }
285
+ if (persistence.boundary === "database" && spec.tenantField !== persistence.tenantField) {
286
+ error("feature-tenant-boundary-drift", `Database transition ${transition.name} tenant field must match Feature ${spec.name} tenantField.`);
287
+ }
288
+ if (persistence.boundary === "external" && (persistence.optimisticLock !== "none" || persistence.versionField !== undefined || persistence.tenantField !== undefined || persistence.stateField !== undefined || persistence.receipt !== "required" || persistence.reconciliation !== "required")) {
289
+ error("feature-persistence-invalid", `External transition ${transition.name} requires a receipt and reconciliation, without database optimistic locking.`);
290
+ }
291
+ if (persistence.boundary === "external" && !transition.job) {
292
+ error("feature-job-unresolved", `External transition ${transition.name} must bind a reconciliation Job.`);
293
+ }
294
+ if (!transition.command) {
295
+ error("feature-command-unresolved", `Persistent transition ${transition.name} must reference a command.`);
296
+ }
297
+ if (!transition.permission?.trim()) {
298
+ error("feature-governance-drift", `Persistent transition ${transition.name} must declare a permission.`);
299
+ }
300
+ }
277
301
  if (!module)
278
302
  continue;
303
+ if (transition.persistence?.boundary === "database") {
304
+ for (const query of module.queries) {
305
+ if (query.tenantScope !== "required") {
306
+ error("feature-query-tenant-boundary", `Query ${query.className} in Feature ${spec.name} must declare tenantScope: "required".`);
307
+ }
308
+ }
309
+ for (const job of module.jobs ?? []) {
310
+ if (job.tenantScope !== "required") {
311
+ error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
312
+ }
313
+ }
314
+ }
279
315
  const commands = module.commands.filter((command) => command.className === transition.command || command.name === transition.command);
280
316
  const command = commands[0];
281
317
  if (transition.command && commands.length !== 1) {
@@ -290,23 +326,59 @@ function validateFeatureSpec(spec, module) {
290
326
  error("feature-governance-drift", `Transition ${transition.name} ${key} differs from command ${command.className}.`);
291
327
  }
292
328
  }
329
+ if (transition.persistence) {
330
+ if (transition.persistence.boundary === "database" && command.transaction !== "required") {
331
+ error("feature-persistence-drift", `Database transition ${transition.name} requires command ${command.className} to declare transaction: "required".`);
332
+ }
333
+ if (command.tenantScope !== "required") {
334
+ error("feature-command-tenant-boundary", `Command ${command.className} in Feature ${spec.name} must declare tenantScope: "required".`);
335
+ }
336
+ if (transition.persistence.boundary === "external" && command.transaction !== "none") {
337
+ error("feature-persistence-drift", `External transition ${transition.name} requires command ${command.className} to declare transaction: "none".`);
338
+ }
339
+ if (command.idempotency !== "required" || !command.audit) {
340
+ error("feature-persistence-drift", `Persistent transition ${transition.name} requires command ${command.className} to declare idempotency and audit.`);
341
+ }
342
+ }
343
+ }
344
+ if (transition.job) {
345
+ const jobs = (module.jobs ?? []).filter((job) => job.className === transition.job || job.name === transition.job);
346
+ if (jobs.length !== 1) {
347
+ error("feature-job-unresolved", `Transition ${transition.name} must reference exactly one Job in module ${module.name}.`);
348
+ } else {
349
+ const job = jobs[0];
350
+ if (job.tenantScope !== "required") {
351
+ error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
352
+ }
353
+ if (job.idempotency !== "required") {
354
+ error("feature-job-idempotency", `Job ${job.className} in Feature ${spec.name} must declare idempotency: "required".`);
355
+ }
356
+ if (job.mode === undefined) {
357
+ error("feature-job-mode", `Job ${job.className} in Feature ${spec.name} must declare mode: "task" or "workflow".`);
358
+ }
359
+ if (job.input === undefined || job.schemaKinds?.input !== "declared") {
360
+ error("feature-job-contract", `Job ${job.className} in Feature ${spec.name} must declare a non-opaque input schema.`);
361
+ }
362
+ const provider = module.providers.find((candidate) => candidate.token === job.className || candidate.useClass === job.className);
363
+ if (job.tenantScope === "required" && provider && !provider.deps.some((token) => isJobContextToken(token))) {
364
+ error("feature-job-context", `Job ${job.className} in Feature ${spec.name} must inject JOB_CONTEXT for trusted tenant execution.`);
365
+ }
366
+ }
293
367
  }
294
368
  if (transition.route) {
295
369
  const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
296
370
  if (routes.length !== 1) {
297
371
  error("feature-route-unresolved", `Transition ${transition.name} must reference exactly one route '${transition.route}' in module ${module.name}.`);
298
- } else if (command && routes[0]?.command !== command.className) {
372
+ } else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className) {
299
373
  error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
374
+ } else if (transition.routeBinding === "controller" && routes[0]?.command !== undefined) {
375
+ error("feature-route-drift", `Controller-bound route ${transition.route} must not also declare a framework command binding.`);
300
376
  }
301
377
  }
302
378
  }
303
379
  return diagnostics;
304
380
  }
305
381
  function generateFeatureSource(spec) {
306
- const errors = validateFeatureSpec(spec);
307
- if (errors.length)
308
- throw new Error(errors.map((error) => error.message).join(`
309
- `));
310
382
  if (spec.transitions.some((transition) => !transition.permission)) {
311
383
  throw new Error("Spec-to-Code requires an explicit permission for every transition.");
312
384
  }
@@ -314,13 +386,34 @@ function generateFeatureSource(spec) {
314
386
  ...transition,
315
387
  command: `Transition${index + 1}Command`
316
388
  }));
389
+ const errors = validateFeatureSpec({ ...spec, transitions });
390
+ if (errors.length)
391
+ throw new Error(errors.map((error) => error.message).join(`
392
+ `));
317
393
  if (transitions.some((transition) => transition.route)) {
318
394
  throw new Error("Generate the command slice first; existing HTTP routes require explicit schema and handler implementations.");
319
395
  }
320
- const sourceSpec = { name: spec.name, states: spec.states, transitions: Object.fromEntries(transitions.map((transition) => {
321
- const { name, ...options } = transition;
322
- return [name, options];
323
- })) };
396
+ const sourceSpec = {
397
+ name: spec.name,
398
+ states: spec.states,
399
+ tenantField: spec.tenantField ?? "tenant_id",
400
+ transitions: Object.fromEntries(transitions.map((transition) => {
401
+ const { name, ...options } = transition;
402
+ const persistence = transition.persistence ?? {
403
+ boundary: "database",
404
+ optimisticLock: "required",
405
+ versionField: "version",
406
+ tenantField: "tenant_id",
407
+ stateField: "state",
408
+ receipt: "required",
409
+ reconciliation: "none"
410
+ };
411
+ return [name, {
412
+ ...options,
413
+ persistence
414
+ }];
415
+ }))
416
+ };
324
417
  return [
325
418
  'import { Command, defineFeatureSlice, defineFeatureSpec } from "@supacloud/app";',
326
419
  "",
@@ -331,9 +424,10 @@ function generateFeatureSource(spec) {
331
424
  `@Command(${JSON.stringify({
332
425
  name: `${spec.name}.${transition.name}`,
333
426
  permission: transition.permission,
334
- transaction: transition.transaction ?? "none",
335
- idempotency: transition.idempotency ?? "none",
336
- audit: transition.audit
427
+ transaction: transition.transaction ?? (transition.persistence?.boundary === "external" ? "none" : "required"),
428
+ idempotency: transition.idempotency ?? "required",
429
+ audit: transition.audit ?? `${spec.name}.${transition.name}`,
430
+ tenantScope: "required"
337
431
  })})`,
338
432
  `export class ${transition.command} {`,
339
433
  ` execute(state: FeatureState): never {`,
@@ -801,13 +895,40 @@ function validateGraph(graph, options = false) {
801
895
  permission: `${module.name}.${command.name}`
802
896
  });
803
897
  }
898
+ if (command.rpc && command.branches) {
899
+ error("command-rpc-branches-conflict", `Command ${command.name} cannot declare both rpc and branches.`, module.file, module.line);
900
+ }
901
+ const branchAdapters = command.branches ?? [];
902
+ for (const branch of branchAdapters) {
903
+ if (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, branch.rpc)) {
904
+ error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${branch.rpc}'.`, module.file, module.line);
905
+ }
906
+ }
804
907
  if (command.rpc && (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, command.rpc))) {
805
908
  error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${command.rpc}'.`, module.file, module.line);
806
909
  }
807
910
  if (typeof options === "object" && options.commandCapabilities) {
808
911
  const hostCaps = options.commandCapabilities;
809
912
  const rpcCaps = command.rpc && Object.hasOwn(hostCaps.rpc ?? {}, command.rpc) ? hostCaps.rpc?.[command.rpc] : undefined;
810
- 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)) {
913
+ const branchCaps = branchAdapters.map((branch) => ({
914
+ branch,
915
+ capabilities: Object.hasOwn(hostCaps.rpc ?? {}, branch.rpc) ? hostCaps.rpc?.[branch.rpc] : undefined
916
+ }));
917
+ for (const { branch, capabilities } of branchCaps) {
918
+ if (capabilities?.boundary === "external" && (branch.transaction === "required" || capabilities.transaction === true)) {
919
+ 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.");
920
+ }
921
+ if (branch.audit && capabilities?.audit !== true) {
922
+ error("command-audit-unsupported", `Command ${command.name} branch '${branch.when}' declares audit, but runtime audit persistence is unavailable.`, module.file, module.line);
923
+ }
924
+ if (branch.idempotency === "required" && capabilities?.idempotency !== true) {
925
+ error("command-idempotency-unsupported", `Command ${command.name} branch '${branch.when}' declares idempotency, but receipt persistence is unavailable.`, module.file, module.line);
926
+ }
927
+ if (branch.transaction === "required" && capabilities?.transaction !== true) {
928
+ error("command-transaction-unsupported", `Command ${command.name} branch '${branch.when}' declares transaction, but transaction support is unavailable.`, module.file, module.line);
929
+ }
930
+ }
931
+ 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)) {
811
932
  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.");
812
933
  }
813
934
  if (rpcCaps?.boundary === "external" && (command.transaction === "required" || rpcCaps.transaction === true)) {
@@ -1283,6 +1404,18 @@ var init_validate = __esm(() => {
1283
1404
  "invalid-feature-transition": { code: "SC6103", docsUrl: "https://supacloud.dev/errors/SC6103" },
1284
1405
  "feature-command-unresolved": { code: "SC6104", docsUrl: "https://supacloud.dev/errors/SC6104" },
1285
1406
  "feature-governance-drift": { code: "SC6105", docsUrl: "https://supacloud.dev/errors/SC6105" },
1407
+ "feature-persistence-invalid": { code: "SC6110", docsUrl: "https://supacloud.dev/errors/SC6110" },
1408
+ "feature-persistence-drift": { code: "SC6111", docsUrl: "https://supacloud.dev/errors/SC6111" },
1409
+ "feature-tenant-boundary-invalid": { code: "SC6112", docsUrl: "https://supacloud.dev/errors/SC6112" },
1410
+ "feature-tenant-boundary-drift": { code: "SC6113", docsUrl: "https://supacloud.dev/errors/SC6113" },
1411
+ "feature-query-tenant-boundary": { code: "SC6114", docsUrl: "https://supacloud.dev/errors/SC6114" },
1412
+ "feature-job-tenant-boundary": { code: "SC6115", docsUrl: "https://supacloud.dev/errors/SC6115" },
1413
+ "feature-command-tenant-boundary": { code: "SC6116", docsUrl: "https://supacloud.dev/errors/SC6116" },
1414
+ "feature-job-unresolved": { code: "SC6117", docsUrl: "https://supacloud.dev/errors/SC6117" },
1415
+ "feature-job-idempotency": { code: "SC6118", docsUrl: "https://supacloud.dev/errors/SC6118" },
1416
+ "feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
1417
+ "feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
1418
+ "feature-job-context": { code: "SC6121", docsUrl: "https://supacloud.dev/errors/SC6121" },
1286
1419
  "feature-route-unresolved": { code: "SC6106", docsUrl: "https://supacloud.dev/errors/SC6106" },
1287
1420
  "feature-route-drift": { code: "SC6107", docsUrl: "https://supacloud.dev/errors/SC6107" },
1288
1421
  "invalid-feature-spec": { code: "SC6108", docsUrl: "https://supacloud.dev/errors/SC6108" }
@@ -1293,6 +1426,8 @@ var init_validate = __esm(() => {
1293
1426
  function buildContractManifest(graph, artifacts) {
1294
1427
  const commands = [];
1295
1428
  const queries = [];
1429
+ const jobs = [];
1430
+ const features = [];
1296
1431
  const routes = [];
1297
1432
  const permissions = new Set;
1298
1433
  const rpc = [];
@@ -1304,21 +1439,95 @@ function buildContractManifest(graph, artifacts) {
1304
1439
  permissions.add(command.permission);
1305
1440
  if (command.rpc)
1306
1441
  rpc.push({ command: command.name, adapter: command.rpc });
1442
+ for (const branch of command.branches ?? []) {
1443
+ rpc.push({ command: `${command.name} [${branch.when}]`, adapter: branch.rpc });
1444
+ }
1307
1445
  if (command.audit)
1308
1446
  events.push({ name: command.audit, source: "command.audit", command: command.name });
1447
+ for (const branch of command.branches ?? []) {
1448
+ if (branch.audit)
1449
+ events.push({ name: branch.audit, source: "command.branch.audit", command: command.name });
1450
+ }
1309
1451
  commands.push({
1310
1452
  name: command.name,
1311
1453
  className: command.className,
1312
1454
  module: module.name,
1313
1455
  ...command.permission === undefined ? {} : { permission: command.permission },
1314
1456
  ...command.rpc === undefined ? {} : { rpc: command.rpc },
1457
+ ...command.branches === undefined ? {} : {
1458
+ branches: command.branches.map((branch) => ({
1459
+ when: branch.when,
1460
+ adapter: branch.rpc,
1461
+ transaction: branch.transaction,
1462
+ idempotency: branch.idempotency,
1463
+ ...branch.audit === undefined ? {} : { auditEvent: branch.audit },
1464
+ ...branch.boundary === undefined ? {} : { boundary: branch.boundary }
1465
+ }))
1466
+ },
1315
1467
  transaction: command.transaction,
1316
1468
  idempotency: command.idempotency,
1469
+ ...command.tenantScope === undefined ? {} : { tenantScope: command.tenantScope },
1317
1470
  ...command.audit === undefined ? {} : { auditEvent: command.audit }
1318
1471
  });
1319
1472
  }
1320
1473
  for (const query of module.queries) {
1321
- queries.push({ name: query.name, className: query.className, module: module.name });
1474
+ queries.push({
1475
+ name: query.name,
1476
+ className: query.className,
1477
+ module: module.name,
1478
+ ...query.tenantScope === undefined ? {} : { tenantScope: query.tenantScope }
1479
+ });
1480
+ }
1481
+ for (const job of module.jobs ?? []) {
1482
+ jobs.push({
1483
+ name: job.name,
1484
+ className: job.className,
1485
+ module: module.name,
1486
+ ...job.input === undefined ? {} : { input: job.input },
1487
+ ...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
1488
+ ...job.output === undefined ? {} : { output: job.output },
1489
+ ...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
1490
+ ...job.mode === undefined ? {} : { mode: job.mode },
1491
+ ...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
1492
+ ...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
1493
+ });
1494
+ }
1495
+ if (module.featureSpec) {
1496
+ features.push({
1497
+ name: module.featureSpec.name,
1498
+ states: [...module.featureSpec.states],
1499
+ ...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
1500
+ transitions: module.featureSpec.transitions.map((transition) => {
1501
+ const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
1502
+ return {
1503
+ name: transition.name,
1504
+ from: transition.from,
1505
+ to: transition.to,
1506
+ ...transition.permission === undefined ? {} : { permission: transition.permission },
1507
+ ...transition.command === undefined ? {} : { command: transition.command },
1508
+ ...transition.route === undefined ? {} : { route: transition.route },
1509
+ ...transition.routeBinding === undefined ? {} : { routeBinding: transition.routeBinding },
1510
+ ...transition.transaction === undefined ? {} : { transaction: transition.transaction },
1511
+ ...transition.idempotency === undefined ? {} : { idempotency: transition.idempotency },
1512
+ ...transition.audit === undefined ? {} : { audit: transition.audit },
1513
+ ...transition.job === undefined ? {} : { job: transition.job },
1514
+ ...job === undefined ? {} : {
1515
+ jobDetails: {
1516
+ name: job.name,
1517
+ className: job.className,
1518
+ ...job.input === undefined ? {} : { input: job.input },
1519
+ ...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
1520
+ ...job.output === undefined ? {} : { output: job.output },
1521
+ ...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
1522
+ ...job.mode === undefined ? {} : { mode: job.mode },
1523
+ ...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
1524
+ ...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
1525
+ }
1526
+ },
1527
+ ...transition.persistence === undefined ? {} : { persistence: transition.persistence }
1528
+ };
1529
+ })
1530
+ });
1322
1531
  }
1323
1532
  for (const controller of module.controllers) {
1324
1533
  for (const route of controller.routes) {
@@ -1349,6 +1558,8 @@ function buildContractManifest(graph, artifacts) {
1349
1558
  }
1350
1559
  commands.sort((left, right) => left.name.localeCompare(right.name));
1351
1560
  queries.sort((left, right) => left.name.localeCompare(right.name));
1561
+ jobs.sort((left, right) => left.name.localeCompare(right.name));
1562
+ features.sort((left, right) => left.name.localeCompare(right.name));
1352
1563
  routes.sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
1353
1564
  rpc.sort((left, right) => left.command.localeCompare(right.command));
1354
1565
  events.sort((left, right) => left.name.localeCompare(right.name));
@@ -1356,6 +1567,8 @@ function buildContractManifest(graph, artifacts) {
1356
1567
  version: 1,
1357
1568
  commands,
1358
1569
  queries,
1570
+ jobs,
1571
+ features,
1359
1572
  routes,
1360
1573
  permissions: [...permissions].sort(),
1361
1574
  rpc,
@@ -1503,6 +1716,7 @@ function renderApplication(graph, options) {
1503
1716
  name: command.name,
1504
1717
  permission: command.permission ?? null,
1505
1718
  rpc: command.rpc ?? null,
1719
+ branches: command.branches ?? [],
1506
1720
  transaction: command.transaction ?? null,
1507
1721
  audit: command.audit ?? null,
1508
1722
  idempotency: command.idempotency ?? null
@@ -1929,7 +2143,9 @@ ${indent(item, 2)}`).join(",")}
1929
2143
  `transaction: ${JSON.stringify(command.transaction)}`,
1930
2144
  ...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
1931
2145
  `idempotency: ${JSON.stringify(command.idempotency)}`,
2146
+ ...command.tenantScope ? [`tenantScope: ${JSON.stringify(command.tenantScope)}`] : [],
1932
2147
  ...command.rpc ? [`rpc: ${JSON.stringify(command.rpc)}`] : [],
2148
+ ...command.branches && command.branches.length > 0 ? [`branches: ${JSON.stringify(command.branches)}`] : [],
1933
2149
  ...command.standalone ? ["standalone: true"] : [],
1934
2150
  ...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`, `aspectPipeline: ${this.renderAspectPipeline(command.aspects)}`] : []
1935
2151
  ];
@@ -1962,6 +2178,8 @@ ${indent(item, 2)}`).join(",")}
1962
2178
  fields.push(`maxAttempts: ${job.maxAttempts}`);
1963
2179
  if (job.idempotency !== undefined)
1964
2180
  fields.push(`idempotency: ${JSON.stringify(job.idempotency)}`);
2181
+ if (job.tenantScope !== undefined)
2182
+ fields.push(`tenantScope: ${JSON.stringify(job.tenantScope)}`);
1965
2183
  if (job.aspects && job.aspects.length > 0) {
1966
2184
  fields.push(`aspects: ${this.renderAspects(job.aspects)}`);
1967
2185
  fields.push(`aspectPipeline: ${this.renderAspectPipeline(job.aspects)}`);
@@ -3665,17 +3883,28 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
3665
3883
 
3666
3884
  export interface CompiledCommand {
3667
3885
  rpc?: string;
3886
+ branches?: readonly CompiledCommandBranch[];
3668
3887
  className: string;
3669
3888
  name: string;
3670
3889
  permission: string;
3671
3890
  transaction: "required" | "none";
3672
3891
  audit?: string;
3673
3892
  idempotency: "required" | "none";
3893
+ tenantScope?: "required" | "none";
3674
3894
  standalone?: boolean;
3675
3895
  aspects?: CompiledAspect[];
3676
3896
  aspectPipeline?: CompiledAspectPipeline;
3677
3897
  }
3678
3898
 
3899
+ export interface CompiledCommandBranch {
3900
+ when: string;
3901
+ rpc: string;
3902
+ transaction?: "required" | "none" | string;
3903
+ audit?: string;
3904
+ idempotency?: "required" | "none" | string;
3905
+ boundary?: "database" | "external";
3906
+ }
3907
+
3679
3908
  export interface CompiledJob {
3680
3909
  className: string;
3681
3910
  name: string;
@@ -3687,6 +3916,7 @@ export interface CompiledJob {
3687
3916
  timeoutSec?: number;
3688
3917
  maxAttempts?: number;
3689
3918
  idempotency?: "required" | "none";
3919
+ tenantScope?: "required" | "none";
3690
3920
  aspects?: CompiledAspect[];
3691
3921
  aspectPipeline?: CompiledAspectPipeline;
3692
3922
  }
@@ -15088,6 +15318,7 @@ function createContextPack(graph, subject) {
15088
15318
  } : undefined;
15089
15319
  const files = [...new Set(modules.flatMap((module) => [
15090
15320
  module.file,
15321
+ ...module.featureSpec?.file ? [module.featureSpec.file] : [],
15091
15322
  ...module.providers.map((provider) => provider.file),
15092
15323
  ...module.controllers.map((controller) => controller.file),
15093
15324
  ...graph.moduleHandlerFiles?.[module.name] ?? [],
@@ -15942,7 +16173,7 @@ async function readDeliverySelection(manifestPath, target) {
15942
16173
  return { root, object, planned };
15943
16174
  }
15944
16175
  async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
15945
- 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) {
16176
+ 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) {
15946
16177
  throw new Error("Artifact inventory exceeds limits.");
15947
16178
  }
15948
16179
  const objectRoot = join8(root, "objects", object.objectId);
@@ -15981,10 +16212,12 @@ async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
15981
16212
  }
15982
16213
  return retained;
15983
16214
  }
15984
- var JSON_LIMIT = 1048576;
16215
+ var JSON_LIMIT = 1048576, DELIVERY_ARTIFACT_MAX_FILE_BYTES, DELIVERY_ARTIFACT_MAX_TOTAL_BYTES;
15985
16216
  var init_delivery_artifact = __esm(() => {
15986
16217
  init_delivery_build_schema();
15987
16218
  init_delivery_files();
16219
+ DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
16220
+ DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
15988
16221
  });
15989
16222
 
15990
16223
  // node_modules/@supacloud/delivery/src/application-release.ts
@@ -16843,6 +17076,7 @@ var init_delivery_migration_plan = () => {};
16843
17076
 
16844
17077
  // node_modules/@supacloud/delivery/src/index.ts
16845
17078
  var init_src = __esm(() => {
17079
+ init_delivery_artifact();
16846
17080
  init_delivery_artifact();
16847
17081
  init_worker_execution();
16848
17082
  init_worker_execution();
@@ -18765,6 +18999,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
18765
18999
  if (meta && booleanProp(meta, "standalone")) {
18766
19000
  const permission = stringLiteralProp(meta, "permission");
18767
19001
  const rpc = checkedRpc(meta, ctx);
19002
+ const branches = checkedBranches(meta, ctx);
18768
19003
  const audit = stringLiteralProp(meta, "audit");
18769
19004
  const standaloneName = stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name;
18770
19005
  const standaloneUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${standaloneName}`, sourcePath(ctx.rootDir, classInfo.file), lineOf(classInfo.decl));
@@ -18774,6 +19009,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
18774
19009
  name: standaloneName,
18775
19010
  ...permission === undefined ? {} : { permission },
18776
19011
  ...rpc === undefined ? {} : { rpc },
19012
+ ...branches === undefined ? {} : { branches },
18777
19013
  transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
18778
19014
  ...audit === undefined ? {} : { audit },
18779
19015
  idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
@@ -19105,18 +19341,22 @@ function parseModule(candidate, nameByNode, ctx) {
19105
19341
  const aspects = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
19106
19342
  const permission = stringLiteralProp(meta, "permission");
19107
19343
  const rpc = checkedRpc(meta, ctx);
19344
+ const branches = checkedBranches(meta, ctx);
19108
19345
  const audit = stringLiteralProp(meta, "audit");
19109
19346
  const commandName = stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>";
19110
19347
  const uses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19111
19348
  rejectUndeclaredResourceUses(uses, moduleResourceNames, ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19349
+ const tenantScope = commandModeProp(meta, "tenantScope");
19112
19350
  commands.push({
19113
19351
  className: cls.name?.text ?? "<anonymous>",
19114
19352
  name: commandName,
19115
19353
  ...permission === undefined ? {} : { permission },
19116
19354
  ...rpc === undefined ? {} : { rpc },
19355
+ ...branches === undefined ? {} : { branches },
19117
19356
  transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
19118
19357
  ...audit === undefined ? {} : { audit },
19119
19358
  idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
19359
+ ...tenantScope === undefined ? {} : { tenantScope },
19120
19360
  ...booleanProp(meta, "standalone") ? { standalone: true } : {},
19121
19361
  ...uses.length > 0 ? { uses } : {},
19122
19362
  ...aspects.length > 0 ? { aspects } : {}
@@ -19147,12 +19387,14 @@ function parseModule(candidate, nameByNode, ctx) {
19147
19387
  const contract = parseJobOptions(meta, className, ctx);
19148
19388
  const jobUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19149
19389
  rejectUndeclaredResourceUses(jobUses, moduleResourceNames, ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19390
+ const tenantScope = commandModeProp(meta, "tenantScope");
19150
19391
  jobs.push({
19151
19392
  className,
19152
19393
  name: stringLiteralProp(meta, "name") ?? className,
19153
19394
  serviceKey: camelName(provider?.token ?? className),
19154
19395
  scope,
19155
19396
  ...contract,
19397
+ ...tenantScope === undefined ? {} : { tenantScope },
19156
19398
  ...jobUses.length > 0 ? { uses: jobUses } : {},
19157
19399
  ...aspects.length > 0 ? { aspects } : {}
19158
19400
  });
@@ -19162,9 +19404,11 @@ function parseModule(candidate, nameByNode, ctx) {
19162
19404
  if (queryDec) {
19163
19405
  const meta = decoratorObjectArg(queryDec);
19164
19406
  if (meta) {
19407
+ const tenantScope = commandModeProp(meta, "tenantScope");
19165
19408
  queries.push({
19166
19409
  className: cls.name?.text ?? "<anonymous>",
19167
- name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
19410
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
19411
+ ...tenantScope === undefined ? {} : { tenantScope }
19168
19412
  });
19169
19413
  }
19170
19414
  }
@@ -19219,6 +19463,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19219
19463
  if (!ts7.isObjectLiteralExpression(input))
19220
19464
  return invalid();
19221
19465
  const name = stringLiteralProp(input, "name");
19466
+ const tenantField = stringLiteralProp(input, "tenantField");
19222
19467
  const statesExpr = getProp(input, "states");
19223
19468
  const transitionObject = getProp(input, "transitions");
19224
19469
  if (!name || !statesExpr || !ts7.isArrayLiteralExpression(statesExpr) || statesExpr.elements.some((state) => !ts7.isStringLiteral(state)) || !transitionObject || !ts7.isObjectLiteralExpression(transitionObject) || input.properties.some((property) => !ts7.isPropertyAssignment(property))) {
@@ -19237,15 +19482,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19237
19482
  const options = property.initializer;
19238
19483
  const from = stringLiteralProp(options, "from");
19239
19484
  const to = stringLiteralProp(options, "to");
19240
- 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))) {
19485
+ 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))) {
19241
19486
  return invalid();
19242
19487
  }
19243
19488
  const permission = stringLiteralProp(options, "permission");
19244
19489
  const command = stringLiteralProp(options, "command");
19245
19490
  const route = stringLiteralProp(options, "route");
19491
+ const routeBinding = stringLiteralProp(options, "routeBinding");
19492
+ if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
19493
+ return invalid();
19246
19494
  const transaction = commandModeProp(options, "transaction");
19247
19495
  const idempotency = commandModeProp(options, "idempotency");
19248
19496
  const audit = stringLiteralProp(options, "audit");
19497
+ const job = stringLiteralProp(options, "job");
19498
+ const persistenceExpr = getProp(options, "persistence");
19499
+ const persistence = persistenceExpr && ts7.isObjectLiteralExpression(persistenceExpr) ? parseFeaturePersistence(persistenceExpr) : undefined;
19500
+ if (persistenceExpr && !persistence)
19501
+ return invalid();
19249
19502
  transitions.push({
19250
19503
  name: propertyName(property.name),
19251
19504
  from,
@@ -19253,12 +19506,44 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19253
19506
  ...permission === undefined ? {} : { permission },
19254
19507
  ...command === undefined ? {} : { command },
19255
19508
  ...route === undefined ? {} : { route },
19509
+ ...routeBinding === undefined ? {} : { routeBinding },
19256
19510
  ...transaction === undefined ? {} : { transaction },
19257
19511
  ...idempotency === undefined ? {} : { idempotency },
19258
- ...audit === undefined ? {} : { audit }
19512
+ ...audit === undefined ? {} : { audit },
19513
+ ...job === undefined ? {} : { job },
19514
+ ...persistence === undefined ? {} : { persistence }
19259
19515
  });
19260
19516
  }
19261
- return { name, states, transitions, file: sourcePath(ctx.rootDir, input.getSourceFile().fileName), line: lineOf(input) };
19517
+ return {
19518
+ name,
19519
+ states,
19520
+ ...tenantField === undefined ? {} : { tenantField },
19521
+ transitions,
19522
+ file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
19523
+ line: lineOf(input)
19524
+ };
19525
+ }
19526
+ function parseFeaturePersistence(input) {
19527
+ if (input.properties.some((property) => !ts7.isPropertyAssignment(property)))
19528
+ return;
19529
+ const boundary = stringLiteralProp(input, "boundary");
19530
+ const optimisticLock = commandModeProp(input, "optimisticLock");
19531
+ const versionField = stringLiteralProp(input, "versionField");
19532
+ const tenantField = stringLiteralProp(input, "tenantField");
19533
+ const stateField = stringLiteralProp(input, "stateField");
19534
+ const receipt = commandModeProp(input, "receipt");
19535
+ const reconciliation = commandModeProp(input, "reconciliation");
19536
+ if (boundary !== "database" && boundary !== "external" || optimisticLock !== "required" && optimisticLock !== "none" || receipt !== "required" && receipt !== "none" || reconciliation !== "required" && reconciliation !== "none")
19537
+ return;
19538
+ return {
19539
+ boundary,
19540
+ optimisticLock,
19541
+ ...versionField === undefined ? {} : { versionField },
19542
+ ...tenantField === undefined ? {} : { tenantField },
19543
+ ...stateField === undefined ? {} : { stateField },
19544
+ receipt,
19545
+ reconciliation
19546
+ };
19262
19547
  }
19263
19548
  function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
19264
19549
  if (!input || seen.has(input))
@@ -20685,6 +20970,75 @@ function checkedRpc(meta, ctx) {
20685
20970
  });
20686
20971
  return;
20687
20972
  }
20973
+ function checkedBranches(meta, ctx) {
20974
+ const expression = getProp(meta, "branches");
20975
+ if (!expression)
20976
+ return;
20977
+ if (!ts7.isArrayLiteralExpression(expression)) {
20978
+ ctx.diagnostics.push({
20979
+ severity: "error",
20980
+ code: "invalid-command-branches",
20981
+ file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
20982
+ line: lineOf(expression),
20983
+ message: "Command branches must be an explicit object array.",
20984
+ suggestion: "Declare each branch as { when, rpc, transaction, audit, idempotency, boundary }."
20985
+ });
20986
+ return;
20987
+ }
20988
+ const branches = [];
20989
+ for (const item of expression.elements) {
20990
+ if (!ts7.isObjectLiteralExpression(item)) {
20991
+ ctx.diagnostics.push({
20992
+ severity: "error",
20993
+ code: "invalid-command-branches",
20994
+ file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
20995
+ line: lineOf(item),
20996
+ message: "Command branches entries must be object literals."
20997
+ });
20998
+ continue;
20999
+ }
21000
+ const when = stringLiteralProp(item, "when");
21001
+ const rpc = stringLiteralProp(item, "rpc");
21002
+ const transaction = checkedCommandMode(item, "transaction", ctx, "command branch") ?? "none";
21003
+ const audit = stringLiteralProp(item, "audit");
21004
+ const idempotency = checkedCommandMode(item, "idempotency", ctx, "command branch") ?? "none";
21005
+ const boundary = stringLiteralProp(item, "boundary");
21006
+ const validBoundary = boundary === undefined || boundary === "database" || boundary === "external";
21007
+ const validWhen = when !== undefined && /^(?:[A-Za-z_$][\w$]*)(?:\.[A-Za-z_$][\w$]*)*\s*===\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(when);
21008
+ if (!when || !validWhen || !rpc?.trim() || !validBoundary) {
21009
+ ctx.diagnostics.push({
21010
+ severity: "error",
21011
+ code: "invalid-command-branches",
21012
+ file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
21013
+ line: lineOf(item),
21014
+ message: "Command branches require a restricted equality selector, a non-empty rpc, and a valid boundary.",
21015
+ suggestion: `Use when: 'resource === "value"' (or body.resource/params.resource/query.resource), plus a named RPC adapter.`
21016
+ });
21017
+ continue;
21018
+ }
21019
+ branches.push({
21020
+ when,
21021
+ rpc,
21022
+ transaction,
21023
+ ...audit === undefined ? {} : { audit },
21024
+ idempotency,
21025
+ ...boundary === undefined ? {} : { boundary }
21026
+ });
21027
+ }
21028
+ if (branches.length === 0)
21029
+ return [];
21030
+ if (new Set(branches.map((branch) => branch.when)).size !== branches.length) {
21031
+ ctx.diagnostics.push({
21032
+ severity: "error",
21033
+ code: "duplicate-command-branch",
21034
+ file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
21035
+ line: lineOf(expression),
21036
+ message: "Command branches must use distinct selectors.",
21037
+ suggestion: "Make each branch selector mutually distinct."
21038
+ });
21039
+ }
21040
+ return branches;
21041
+ }
20688
21042
  function classDeps(cls, ctx) {
20689
21043
  const injectable = parseInjectableOptions(cls, ctx);
20690
21044
  const ctor = cls.members.find(ts7.isConstructorDeclaration);
@@ -24096,10 +24450,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
24096
24450
  import { resolve as resolve17 } from "node:path";
24097
24451
 
24098
24452
  // src/migration-app-version.ts
24099
- var MIGRATION_APP_VERSION = "0.28.2";
24453
+ var MIGRATION_APP_VERSION = "0.29.0";
24100
24454
 
24101
24455
  // src/migration-elysia-version.ts
24102
- var MIGRATION_ELYSIA_VERSION = "0.33.4";
24456
+ var MIGRATION_ELYSIA_VERSION = "0.34.0";
24103
24457
 
24104
24458
  // src/migration-policy.ts
24105
24459
  var cachedCompilerVersion;
@@ -25068,6 +25422,163 @@ function isRecord2(value) {
25068
25422
  function arrayLength(value) {
25069
25423
  return Array.isArray(value) ? value.length : undefined;
25070
25424
  }
25425
+ function safeField(value) {
25426
+ return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
25427
+ }
25428
+ function validFeaturePersistence(value) {
25429
+ if (!isRecord2(value))
25430
+ return false;
25431
+ const boundary = value["boundary"];
25432
+ const receipt = value["receipt"];
25433
+ const reconciliation = value["reconciliation"];
25434
+ if (boundary === "database") {
25435
+ const fields = [value["tenantField"], value["versionField"], value["stateField"]];
25436
+ return value["optimisticLock"] === "required" && fields.every(safeField) && new Set(fields).size === 3 && receipt === "required" && reconciliation === "none";
25437
+ }
25438
+ if (boundary === "external") {
25439
+ return value["optimisticLock"] === "none" && value["versionField"] === undefined && value["tenantField"] === undefined && value["stateField"] === undefined && receipt === "required" && reconciliation === "required";
25440
+ }
25441
+ return false;
25442
+ }
25443
+ function validCommandInventory(value) {
25444
+ const names = new Set;
25445
+ const classes = new Set;
25446
+ return Array.isArray(value) && value.every((command) => {
25447
+ 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")
25448
+ return false;
25449
+ if (names.has(command["name"]) || classes.has(command["className"]))
25450
+ return false;
25451
+ if (command["rpc"] !== undefined && (typeof command["rpc"] !== "string" || command["rpc"].trim() === ""))
25452
+ return false;
25453
+ if (command["auditEvent"] !== undefined && (typeof command["auditEvent"] !== "string" || command["auditEvent"].trim() === ""))
25454
+ return false;
25455
+ names.add(command["name"]);
25456
+ classes.add(command["className"]);
25457
+ return true;
25458
+ });
25459
+ }
25460
+ function validFeatureInventory(value, jobs, commands, routes, rpc) {
25461
+ const featureNames = new Set;
25462
+ return Array.isArray(value) && value.every((feature) => {
25463
+ if (!isRecord2(feature) || typeof feature["name"] !== "string" || feature["name"].trim() === "")
25464
+ return false;
25465
+ if (featureNames.has(feature["name"]))
25466
+ return false;
25467
+ featureNames.add(feature["name"]);
25468
+ if (feature["tenantField"] !== undefined && !safeField(feature["tenantField"]))
25469
+ return false;
25470
+ const states = feature["states"];
25471
+ const transitions = feature["transitions"];
25472
+ if (!Array.isArray(states) || states.length === 0 || states.some((state) => typeof state !== "string" || state.trim() === "") || new Set(states).size !== states.length || !Array.isArray(transitions))
25473
+ return false;
25474
+ const transitionNames = new Set;
25475
+ return transitions.every((transition) => {
25476
+ 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"]))
25477
+ return false;
25478
+ if (transitionNames.has(transition["name"]))
25479
+ return false;
25480
+ transitionNames.add(transition["name"]);
25481
+ const persistence = transition["persistence"];
25482
+ if (persistence !== undefined) {
25483
+ if (!isRecord2(persistence) || !validFeaturePersistence(persistence))
25484
+ return false;
25485
+ if (typeof transition["command"] !== "string" || transition["command"].trim() === "" || typeof transition["permission"] !== "string" || transition["permission"].trim() === "")
25486
+ return false;
25487
+ if (persistence["boundary"] === "database" && feature["tenantField"] !== persistence["tenantField"])
25488
+ return false;
25489
+ }
25490
+ const commandName = transition["command"];
25491
+ if (commandName !== undefined) {
25492
+ const command = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
25493
+ if (!isRecord2(command))
25494
+ return false;
25495
+ if (transition["permission"] !== undefined && command["permission"] !== transition["permission"])
25496
+ return false;
25497
+ if (transition["transaction"] !== undefined && command["transaction"] !== transition["transaction"])
25498
+ return false;
25499
+ if (transition["idempotency"] !== undefined && command["idempotency"] !== transition["idempotency"])
25500
+ return false;
25501
+ if (transition["audit"] !== undefined && command["auditEvent"] !== transition["audit"])
25502
+ return false;
25503
+ if (persistence !== undefined) {
25504
+ const adapter = command["rpc"];
25505
+ 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() === "")
25506
+ return false;
25507
+ const expectedTransaction = persistence["boundary"] === "database" ? "required" : "none";
25508
+ if (command["transaction"] !== expectedTransaction)
25509
+ return false;
25510
+ }
25511
+ }
25512
+ const routeName = transition["route"];
25513
+ const routeBinding = transition["routeBinding"];
25514
+ if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
25515
+ return false;
25516
+ if (routeName !== undefined) {
25517
+ const route = routes.find((candidate) => isRecord2(candidate) && `${candidate["method"]} ${candidate["path"]}` === routeName);
25518
+ if (!isRecord2(route))
25519
+ return false;
25520
+ if (routeBinding === "controller") {
25521
+ if (route["command"] !== undefined)
25522
+ return false;
25523
+ } else if (route["command"] !== commandName) {
25524
+ return false;
25525
+ }
25526
+ }
25527
+ const job = transition["job"];
25528
+ if (job === undefined)
25529
+ return true;
25530
+ const details = transition["jobDetails"];
25531
+ const inventory = jobs.find((candidate) => isRecord2(candidate) && (candidate["name"] === job || candidate["className"] === job));
25532
+ const contractFields = ["input", "inputKind", "output", "outputKind", "mode", "idempotency", "tenantScope"];
25533
+ 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]);
25534
+ });
25535
+ });
25536
+ }
25537
+ function validJobInventory(value) {
25538
+ const names = new Set;
25539
+ const classes = new Set;
25540
+ return Array.isArray(value) && value.every((job) => {
25541
+ if (!isRecord2(job) || typeof job["name"] !== "string" || job["name"].trim() === "" || typeof job["className"] !== "string" || job["className"].trim() === "" || typeof job["module"] !== "string" || job["module"].trim() === "")
25542
+ return false;
25543
+ if (names.has(job["name"]) || classes.has(job["className"]))
25544
+ return false;
25545
+ names.add(job["name"]);
25546
+ classes.add(job["className"]);
25547
+ if (job["input"] !== undefined && (typeof job["input"] !== "string" || job["input"].trim() === ""))
25548
+ return false;
25549
+ if (job["inputKind"] !== undefined && job["inputKind"] !== "opaque" && job["inputKind"] !== "declared")
25550
+ return false;
25551
+ if (job["output"] !== undefined && (typeof job["output"] !== "string" || job["output"].trim() === ""))
25552
+ return false;
25553
+ if (job["outputKind"] !== undefined && job["outputKind"] !== "opaque" && job["outputKind"] !== "declared")
25554
+ return false;
25555
+ if (job["mode"] !== undefined && job["mode"] !== "task" && job["mode"] !== "workflow")
25556
+ return false;
25557
+ if (job["idempotency"] !== undefined && job["idempotency"] !== "required" && job["idempotency"] !== "none")
25558
+ return false;
25559
+ return job["tenantScope"] === undefined || job["tenantScope"] === "required" || job["tenantScope"] === "none";
25560
+ });
25561
+ }
25562
+ function validRpcInventory(value, commandInventory) {
25563
+ const commandNames = new Set;
25564
+ const adapters = new Set;
25565
+ return Array.isArray(value) && value.every((rpc) => {
25566
+ if (!isRecord2(rpc) || typeof rpc["command"] !== "string" || rpc["command"].trim() === "" || typeof rpc["adapter"] !== "string" || rpc["adapter"].trim() === "")
25567
+ return false;
25568
+ if (commandNames.has(rpc["command"]) || adapters.has(rpc["adapter"]))
25569
+ return false;
25570
+ const command = commandInventory.find((candidate) => isRecord2(candidate) && candidate["name"] === rpc["command"] && candidate["rpc"] === rpc["adapter"]);
25571
+ if (!isRecord2(command))
25572
+ return false;
25573
+ commandNames.add(rpc["command"]);
25574
+ adapters.add(rpc["adapter"]);
25575
+ return true;
25576
+ }) && commandInventory.every((command) => {
25577
+ if (!isRecord2(command) || command["rpc"] === undefined)
25578
+ return true;
25579
+ return value.some((rpc) => isRecord2(rpc) && rpc["command"] === command["name"] && rpc["adapter"] === command["rpc"]);
25580
+ });
25581
+ }
25071
25582
  async function sha2566(path) {
25072
25583
  return createHash11("sha256").update(await readFile11(path)).digest("hex");
25073
25584
  }
@@ -25137,19 +25648,24 @@ async function assessMigration(options) {
25137
25648
  };
25138
25649
  try {
25139
25650
  const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
25140
- if (!isRecord2(parsed) || parsed["version"] !== 1)
25651
+ 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"])) {
25141
25652
  throw new Error("invalid manifest");
25653
+ }
25142
25654
  const commands = arrayLength(parsed["commands"]);
25655
+ const jobs = arrayLength(parsed["jobs"]);
25143
25656
  const routes = arrayLength(parsed["routes"]);
25144
25657
  const permissions = arrayLength(parsed["permissions"]);
25658
+ const features = arrayLength(parsed["features"]);
25145
25659
  contractsManifest = {
25146
25660
  path: contractsManifestPath,
25147
25661
  present: true,
25148
25662
  sha256: await sha2566(contractsManifestPath),
25149
25663
  version: 1,
25150
25664
  ...commands === undefined ? {} : { commands },
25665
+ ...jobs === undefined ? {} : { jobs },
25151
25666
  ...routes === undefined ? {} : { routes },
25152
- ...permissions === undefined ? {} : { permissions }
25667
+ ...permissions === undefined ? {} : { permissions },
25668
+ ...features === undefined ? {} : { features }
25153
25669
  };
25154
25670
  } catch {
25155
25671
  findings.push({