@supacloud/compiler 0.47.3 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
  }
@@ -15943,7 +16173,7 @@ async function readDeliverySelection(manifestPath, target) {
15943
16173
  return { root, object, planned };
15944
16174
  }
15945
16175
  async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
15946
- 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) {
15947
16177
  throw new Error("Artifact inventory exceeds limits.");
15948
16178
  }
15949
16179
  const objectRoot = join8(root, "objects", object.objectId);
@@ -15982,10 +16212,12 @@ async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
15982
16212
  }
15983
16213
  return retained;
15984
16214
  }
15985
- var JSON_LIMIT = 1048576;
16215
+ var JSON_LIMIT = 1048576, DELIVERY_ARTIFACT_MAX_FILE_BYTES, DELIVERY_ARTIFACT_MAX_TOTAL_BYTES;
15986
16216
  var init_delivery_artifact = __esm(() => {
15987
16217
  init_delivery_build_schema();
15988
16218
  init_delivery_files();
16219
+ DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
16220
+ DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
15989
16221
  });
15990
16222
 
15991
16223
  // node_modules/@supacloud/delivery/src/application-release.ts
@@ -16844,6 +17076,7 @@ var init_delivery_migration_plan = () => {};
16844
17076
 
16845
17077
  // node_modules/@supacloud/delivery/src/index.ts
16846
17078
  var init_src = __esm(() => {
17079
+ init_delivery_artifact();
16847
17080
  init_delivery_artifact();
16848
17081
  init_worker_execution();
16849
17082
  init_worker_execution();
@@ -18766,6 +18999,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
18766
18999
  if (meta && booleanProp(meta, "standalone")) {
18767
19000
  const permission = stringLiteralProp(meta, "permission");
18768
19001
  const rpc = checkedRpc(meta, ctx);
19002
+ const branches = checkedBranches(meta, ctx);
18769
19003
  const audit = stringLiteralProp(meta, "audit");
18770
19004
  const standaloneName = stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name;
18771
19005
  const standaloneUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${standaloneName}`, sourcePath(ctx.rootDir, classInfo.file), lineOf(classInfo.decl));
@@ -18775,6 +19009,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
18775
19009
  name: standaloneName,
18776
19010
  ...permission === undefined ? {} : { permission },
18777
19011
  ...rpc === undefined ? {} : { rpc },
19012
+ ...branches === undefined ? {} : { branches },
18778
19013
  transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
18779
19014
  ...audit === undefined ? {} : { audit },
18780
19015
  idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
@@ -19106,18 +19341,22 @@ function parseModule(candidate, nameByNode, ctx) {
19106
19341
  const aspects = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
19107
19342
  const permission = stringLiteralProp(meta, "permission");
19108
19343
  const rpc = checkedRpc(meta, ctx);
19344
+ const branches = checkedBranches(meta, ctx);
19109
19345
  const audit = stringLiteralProp(meta, "audit");
19110
19346
  const commandName = stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>";
19111
19347
  const uses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19112
19348
  rejectUndeclaredResourceUses(uses, moduleResourceNames, ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19349
+ const tenantScope = commandModeProp(meta, "tenantScope");
19113
19350
  commands.push({
19114
19351
  className: cls.name?.text ?? "<anonymous>",
19115
19352
  name: commandName,
19116
19353
  ...permission === undefined ? {} : { permission },
19117
19354
  ...rpc === undefined ? {} : { rpc },
19355
+ ...branches === undefined ? {} : { branches },
19118
19356
  transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
19119
19357
  ...audit === undefined ? {} : { audit },
19120
19358
  idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
19359
+ ...tenantScope === undefined ? {} : { tenantScope },
19121
19360
  ...booleanProp(meta, "standalone") ? { standalone: true } : {},
19122
19361
  ...uses.length > 0 ? { uses } : {},
19123
19362
  ...aspects.length > 0 ? { aspects } : {}
@@ -19148,12 +19387,14 @@ function parseModule(candidate, nameByNode, ctx) {
19148
19387
  const contract = parseJobOptions(meta, className, ctx);
19149
19388
  const jobUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19150
19389
  rejectUndeclaredResourceUses(jobUses, moduleResourceNames, ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
19390
+ const tenantScope = commandModeProp(meta, "tenantScope");
19151
19391
  jobs.push({
19152
19392
  className,
19153
19393
  name: stringLiteralProp(meta, "name") ?? className,
19154
19394
  serviceKey: camelName(provider?.token ?? className),
19155
19395
  scope,
19156
19396
  ...contract,
19397
+ ...tenantScope === undefined ? {} : { tenantScope },
19157
19398
  ...jobUses.length > 0 ? { uses: jobUses } : {},
19158
19399
  ...aspects.length > 0 ? { aspects } : {}
19159
19400
  });
@@ -19163,9 +19404,11 @@ function parseModule(candidate, nameByNode, ctx) {
19163
19404
  if (queryDec) {
19164
19405
  const meta = decoratorObjectArg(queryDec);
19165
19406
  if (meta) {
19407
+ const tenantScope = commandModeProp(meta, "tenantScope");
19166
19408
  queries.push({
19167
19409
  className: cls.name?.text ?? "<anonymous>",
19168
- name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
19410
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
19411
+ ...tenantScope === undefined ? {} : { tenantScope }
19169
19412
  });
19170
19413
  }
19171
19414
  }
@@ -19220,6 +19463,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19220
19463
  if (!ts7.isObjectLiteralExpression(input))
19221
19464
  return invalid();
19222
19465
  const name = stringLiteralProp(input, "name");
19466
+ const tenantField = stringLiteralProp(input, "tenantField");
19223
19467
  const statesExpr = getProp(input, "states");
19224
19468
  const transitionObject = getProp(input, "transitions");
19225
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))) {
@@ -19238,15 +19482,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19238
19482
  const options = property.initializer;
19239
19483
  const from = stringLiteralProp(options, "from");
19240
19484
  const to = stringLiteralProp(options, "to");
19241
- 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))) {
19242
19486
  return invalid();
19243
19487
  }
19244
19488
  const permission = stringLiteralProp(options, "permission");
19245
19489
  const command = stringLiteralProp(options, "command");
19246
19490
  const route = stringLiteralProp(options, "route");
19491
+ const routeBinding = stringLiteralProp(options, "routeBinding");
19492
+ if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
19493
+ return invalid();
19247
19494
  const transaction = commandModeProp(options, "transaction");
19248
19495
  const idempotency = commandModeProp(options, "idempotency");
19249
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();
19250
19502
  transitions.push({
19251
19503
  name: propertyName(property.name),
19252
19504
  from,
@@ -19254,12 +19506,44 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19254
19506
  ...permission === undefined ? {} : { permission },
19255
19507
  ...command === undefined ? {} : { command },
19256
19508
  ...route === undefined ? {} : { route },
19509
+ ...routeBinding === undefined ? {} : { routeBinding },
19257
19510
  ...transaction === undefined ? {} : { transaction },
19258
19511
  ...idempotency === undefined ? {} : { idempotency },
19259
- ...audit === undefined ? {} : { audit }
19512
+ ...audit === undefined ? {} : { audit },
19513
+ ...job === undefined ? {} : { job },
19514
+ ...persistence === undefined ? {} : { persistence }
19260
19515
  });
19261
19516
  }
19262
- 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
+ };
19263
19547
  }
19264
19548
  function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
19265
19549
  if (!input || seen.has(input))
@@ -20686,6 +20970,75 @@ function checkedRpc(meta, ctx) {
20686
20970
  });
20687
20971
  return;
20688
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
+ }
20689
21042
  function classDeps(cls, ctx) {
20690
21043
  const injectable = parseInjectableOptions(cls, ctx);
20691
21044
  const ctor = cls.members.find(ts7.isConstructorDeclaration);
@@ -24097,10 +24450,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
24097
24450
  import { resolve as resolve17 } from "node:path";
24098
24451
 
24099
24452
  // src/migration-app-version.ts
24100
- var MIGRATION_APP_VERSION = "0.28.3";
24453
+ var MIGRATION_APP_VERSION = "0.29.0";
24101
24454
 
24102
24455
  // src/migration-elysia-version.ts
24103
- var MIGRATION_ELYSIA_VERSION = "0.33.5";
24456
+ var MIGRATION_ELYSIA_VERSION = "0.34.0";
24104
24457
 
24105
24458
  // src/migration-policy.ts
24106
24459
  var cachedCompilerVersion;
@@ -25069,6 +25422,163 @@ function isRecord2(value) {
25069
25422
  function arrayLength(value) {
25070
25423
  return Array.isArray(value) ? value.length : undefined;
25071
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
+ }
25072
25582
  async function sha2566(path) {
25073
25583
  return createHash11("sha256").update(await readFile11(path)).digest("hex");
25074
25584
  }
@@ -25138,19 +25648,24 @@ async function assessMigration(options) {
25138
25648
  };
25139
25649
  try {
25140
25650
  const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
25141
- 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"])) {
25142
25652
  throw new Error("invalid manifest");
25653
+ }
25143
25654
  const commands = arrayLength(parsed["commands"]);
25655
+ const jobs = arrayLength(parsed["jobs"]);
25144
25656
  const routes = arrayLength(parsed["routes"]);
25145
25657
  const permissions = arrayLength(parsed["permissions"]);
25658
+ const features = arrayLength(parsed["features"]);
25146
25659
  contractsManifest = {
25147
25660
  path: contractsManifestPath,
25148
25661
  present: true,
25149
25662
  sha256: await sha2566(contractsManifestPath),
25150
25663
  version: 1,
25151
25664
  ...commands === undefined ? {} : { commands },
25665
+ ...jobs === undefined ? {} : { jobs },
25152
25666
  ...routes === undefined ? {} : { routes },
25153
- ...permissions === undefined ? {} : { permissions }
25667
+ ...permissions === undefined ? {} : { permissions },
25668
+ ...features === undefined ? {} : { features }
25154
25669
  };
25155
25670
  } catch {
25156
25671
  findings.push({