@stardeck-customer-apps/compose 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2066,23 +2066,23 @@ function isValidModuleName(value) {
2066
2066
  var projectSlugSchema = import_zod.z.string().refine((value) => validateSlug(value).valid, "must be a valid project slug");
2067
2067
  var moduleKindSchema = import_zod.z.enum(["feature", "library", "contract"]);
2068
2068
  var endpointMethodSchema = import_zod.z.enum(ENDPOINT_METHODS);
2069
- function endpointPathValidationError(path3) {
2070
- if (!path3.startsWith("/api/")) {
2069
+ function endpointPathValidationError(path5) {
2070
+ if (!path5.startsWith("/api/")) {
2071
2071
  return 'must begin with "/api/"';
2072
2072
  }
2073
- if (path3.endsWith("/")) {
2073
+ if (path5.endsWith("/")) {
2074
2074
  return "must not end with a trailing slash";
2075
2075
  }
2076
- if (path3.includes("?") || path3.includes("#")) {
2076
+ if (path5.includes("?") || path5.includes("#")) {
2077
2077
  return "must not include query or hash";
2078
2078
  }
2079
- if (path3.includes("\\")) {
2079
+ if (path5.includes("\\")) {
2080
2080
  return "must not include backslashes";
2081
2081
  }
2082
- if (path3.includes("%")) {
2082
+ if (path5.includes("%")) {
2083
2083
  return "must not include percent-encoded segments";
2084
2084
  }
2085
- const parts = path3.split("/");
2085
+ const parts = path5.split("/");
2086
2086
  if (parts.length < 3 || parts[0] !== "" || parts[1] !== "api") {
2087
2087
  return 'must begin with "/api/"';
2088
2088
  }
@@ -2132,12 +2132,12 @@ function endpointPathValidationError(path3) {
2132
2132
  }
2133
2133
  return null;
2134
2134
  }
2135
- function normalizeEndpointPathForCollision(path3) {
2136
- const error = endpointPathValidationError(path3);
2135
+ function normalizeEndpointPathForCollision(path5) {
2136
+ const error = endpointPathValidationError(path5);
2137
2137
  if (error != null) {
2138
2138
  throw new Error(`invalid endpoint path: ${error}`);
2139
2139
  }
2140
- return path3.split("/").map((segment, index) => {
2140
+ return path5.split("/").map((segment, index) => {
2141
2141
  if (index < 2) {
2142
2142
  return segment;
2143
2143
  }
@@ -2150,8 +2150,8 @@ function normalizeEndpointPathForCollision(path3) {
2150
2150
  return segment;
2151
2151
  }).join("/");
2152
2152
  }
2153
- var endpointPathSchema = import_zod.z.string().superRefine((path3, ctx) => {
2154
- const error = endpointPathValidationError(path3);
2153
+ var endpointPathSchema = import_zod.z.string().superRefine((path5, ctx) => {
2154
+ const error = endpointPathValidationError(path5);
2155
2155
  if (error != null) {
2156
2156
  ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: error });
2157
2157
  }
@@ -2398,16 +2398,16 @@ var moduleManifestSchema = import_zod.z.object({
2398
2398
  }
2399
2399
  }
2400
2400
  const seenWorkflows = /* @__PURE__ */ new Map();
2401
- const noteWorkflow = (name, path3) => {
2401
+ const noteWorkflow = (name, path5) => {
2402
2402
  const existing = seenWorkflows.get(name);
2403
2403
  if (existing != null) {
2404
2404
  ctx.addIssue({
2405
2405
  code: import_zod.z.ZodIssueCode.custom,
2406
2406
  message: `duplicate workflow name "${name}" (also at ${existing})`,
2407
- path: path3
2407
+ path: path5
2408
2408
  });
2409
2409
  } else {
2410
- seenWorkflows.set(name, path3.join("."));
2410
+ seenWorkflows.set(name, path5.join("."));
2411
2411
  }
2412
2412
  };
2413
2413
  for (let i = 0; i < manifest.workflows.length; i += 1) {
@@ -2419,7 +2419,7 @@ var moduleManifestSchema = import_zod.z.object({
2419
2419
  }
2420
2420
  }
2421
2421
  const seenEndpointKeys = /* @__PURE__ */ new Map();
2422
- const noteEndpoint = (claim, path3) => {
2422
+ const noteEndpoint = (claim, path5) => {
2423
2423
  let key;
2424
2424
  try {
2425
2425
  key = normalizeEndpointPathForCollision(claim.path);
@@ -2431,10 +2431,10 @@ var moduleManifestSchema = import_zod.z.object({
2431
2431
  ctx.addIssue({
2432
2432
  code: import_zod.z.ZodIssueCode.custom,
2433
2433
  message: `duplicate normalized endpoint path "${key}" (also at ${existing})`,
2434
- path: path3
2434
+ path: path5
2435
2435
  });
2436
2436
  } else {
2437
- seenEndpointKeys.set(key, path3.join("."));
2437
+ seenEndpointKeys.set(key, path5.join("."));
2438
2438
  }
2439
2439
  };
2440
2440
  for (let i = 0; i < manifest.endpoints.length; i += 1) {
@@ -2535,8 +2535,8 @@ var moduleManifestSchema = import_zod.z.object({
2535
2535
  }).transform(({ version: _legacyVersion, ...manifest }) => manifest);
2536
2536
  function formatZodErrors(error) {
2537
2537
  return error.issues.map((issue) => {
2538
- const path3 = issue.path.length > 0 ? issue.path.join(".") : "root";
2539
- return `${path3}: ${issue.message}`;
2538
+ const path5 = issue.path.length > 0 ? issue.path.join(".") : "root";
2539
+ return `${path5}: ${issue.message}`;
2540
2540
  });
2541
2541
  }
2542
2542
  function parseModuleManifest(raw) {
@@ -2936,16 +2936,16 @@ var collectedE2eFactsSchema = import_zod3.z.object({
2936
2936
  function validateRouteExportEvidenceCongruence(input) {
2937
2937
  const entrySet = new Set(input.routeEntries);
2938
2938
  const exportPaths = input.routePageExports.map((e) => e.nextRelativePath);
2939
- for (const path3 of exportPaths) {
2940
- if (!entrySet.has(path3)) {
2941
- input.onIssue(`routePageExports path "${path3}" is not listed in routeEntries`);
2939
+ for (const path5 of exportPaths) {
2940
+ if (!entrySet.has(path5)) {
2941
+ input.onIssue(`routePageExports path "${path5}" is not listed in routeEntries`);
2942
2942
  }
2943
2943
  }
2944
2944
  if (input.routeCollectionErrors.length === 0) {
2945
2945
  const exportSet = new Set(exportPaths);
2946
- for (const path3 of input.routeEntries) {
2947
- if (!exportSet.has(path3)) {
2948
- input.onIssue(`routeEntries path "${path3}" is missing from routePageExports`);
2946
+ for (const path5 of input.routeEntries) {
2947
+ if (!exportSet.has(path5)) {
2948
+ input.onIssue(`routeEntries path "${path5}" is missing from routePageExports`);
2949
2949
  }
2950
2950
  }
2951
2951
  }
@@ -3896,16 +3896,18 @@ var MODULE_CONTRIBUTIONS_GEN_PATH = `${APP_WEB_PREFIX}src/module-contributions.g
3896
3896
  var MODULE_DATASTORES_GEN_PATH = `${APP_WEB_PREFIX}src/module-datastores.gen.ts`;
3897
3897
  var MODULE_I18N_GEN_PATH = `${APP_WEB_PREFIX}src/module-i18n.gen.ts`;
3898
3898
  var MODULE_INIT_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-init.server.gen.ts`;
3899
+ var MODULE_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-server.gen.ts`;
3899
3900
  var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
3900
3901
  var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
3901
3902
 
3902
3903
  // src/cli.ts
3903
- var import_typescript5 = __toESM(require("typescript"));
3904
+ var import_typescript6 = __toESM(require("typescript"));
3905
+ var import_node_path4 = __toESM(require("path"));
3904
3906
 
3905
3907
  // src/index.ts
3906
3908
  var import_node_child_process2 = require("child_process");
3907
- var import_node_fs2 = __toESM(require("fs"));
3908
- var import_node_path2 = __toESM(require("path"));
3909
+ var import_node_fs3 = __toESM(require("fs"));
3910
+ var import_node_path3 = __toESM(require("path"));
3909
3911
 
3910
3912
  // ../../packages/lib/src/server/module-rail/local-fs-sandbox.ts
3911
3913
  var import_node_child_process = require("child_process");
@@ -4141,7 +4143,8 @@ var COMPOSITION_RUNTIME_GEN_PATHS = [
4141
4143
  MODULE_CONTRIBUTIONS_GEN_PATH,
4142
4144
  MODULE_I18N_GEN_PATH,
4143
4145
  MODULE_INIT_SERVER_GEN_PATH,
4144
- MODULE_DATASTORES_GEN_PATH
4146
+ MODULE_DATASTORES_GEN_PATH,
4147
+ MODULE_SERVER_GEN_PATH
4145
4148
  ];
4146
4149
  function compareNames(a, b) {
4147
4150
  return a < b ? -1 : a > b ? 1 : 0;
@@ -4162,6 +4165,9 @@ function formatI18nMarker() {
4162
4165
  function formatInitMarker() {
4163
4166
  return `// ${MODULE_STUB_MARKER} scope=init`;
4164
4167
  }
4168
+ function formatServerMarker() {
4169
+ return `// ${MODULE_STUB_MARKER} scope=server`;
4170
+ }
4165
4171
  function formatDatastoresMarker() {
4166
4172
  return `// ${MODULE_STUB_MARKER} scope=datastores`;
4167
4173
  }
@@ -4257,13 +4263,13 @@ function assertClientSafeConventionalEntryImports(source, pathLabel, kind) {
4257
4263
  function createMessageTree() {
4258
4264
  return /* @__PURE__ */ Object.create(null);
4259
4265
  }
4260
- function assertMessageTree(value, path3) {
4266
+ function assertMessageTree(value, path5) {
4261
4267
  if (typeof value === "string") return;
4262
4268
  if (value === null || Array.isArray(value) || typeof value !== "object") {
4263
- throw new Error(`Invalid message at ${path3}: expected string or nested object`);
4269
+ throw new Error(`Invalid message at ${path5}: expected string or nested object`);
4264
4270
  }
4265
4271
  for (const [key, child] of Object.entries(value)) {
4266
- assertMessageTree(child, `${path3}.${key}`);
4272
+ assertMessageTree(child, `${path5}.${key}`);
4267
4273
  }
4268
4274
  }
4269
4275
  function cloneTree(value) {
@@ -4273,17 +4279,17 @@ function cloneTree(value) {
4273
4279
  }
4274
4280
  return clone;
4275
4281
  }
4276
- function collectLeafPaths(value, path3, out) {
4282
+ function collectLeafPaths(value, path5, out) {
4277
4283
  if (typeof value === "string") {
4278
- out.push(path3);
4284
+ out.push(path5);
4279
4285
  return;
4280
4286
  }
4281
- assertMessageTree(value, path3);
4282
- for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path3}.${key}`, out);
4287
+ assertMessageTree(value, path5);
4288
+ for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path5}.${key}`, out);
4283
4289
  }
4284
- function overrideInto(target, source, owner, path3, missing) {
4290
+ function overrideInto(target, source, owner, path5, missing) {
4285
4291
  for (const [key, value] of Object.entries(source)) {
4286
- const nextPath = path3 ? `${path3}.${key}` : key;
4292
+ const nextPath = path5 ? `${path5}.${key}` : key;
4287
4293
  if (!Object.prototype.hasOwnProperty.call(target, key)) {
4288
4294
  collectLeafPaths(value, nextPath, missing);
4289
4295
  continue;
@@ -4310,9 +4316,9 @@ function overrideInto(target, source, owner, path3, missing) {
4310
4316
  function composeLocaleMessagesForPlan(layers) {
4311
4317
  const result = createMessageTree();
4312
4318
  const leafOwners = /* @__PURE__ */ new Map();
4313
- function mergeInto(target, source, owner, path3) {
4319
+ function mergeInto(target, source, owner, path5) {
4314
4320
  for (const [key, value] of Object.entries(source)) {
4315
- const nextPath = path3 ? `${path3}.${key}` : key;
4321
+ const nextPath = path5 ? `${path5}.${key}` : key;
4316
4322
  const hasExisting = Object.prototype.hasOwnProperty.call(target, key);
4317
4323
  const existing = hasExisting ? target[key] : void 0;
4318
4324
  if (typeof value === "string") {
@@ -4805,6 +4811,42 @@ function emitModuleInitServerGenTs(input) {
4805
4811
  ""
4806
4812
  ].join("\n");
4807
4813
  }
4814
+ function emitModuleServerGenTs(input) {
4815
+ const installed = new Set(input.registry.modules.map((m) => m.name));
4816
+ const moduleNames = input.facts.filter((f) => f.hasServerRegistrations && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames);
4817
+ for (const name of moduleNames) {
4818
+ if (!isValidModuleName(name)) {
4819
+ throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
4820
+ }
4821
+ }
4822
+ const importLines = moduleNames.map(
4823
+ (name, index) => [
4824
+ `@/modules/${name}/server/registrations`,
4825
+ `import * as serverRegistrations${index} from ${JSON.stringify(`@/modules/${name}/server/registrations`)};`
4826
+ ]
4827
+ ).sort((a, b) => compareImportSpecifiers(a[0], b[0])).map(([, line]) => line);
4828
+ const entryLines = moduleNames.map(
4829
+ (name, index) => ` { module: ${JSON.stringify(name)}, registrations: serverRegistrations${index} },`
4830
+ );
4831
+ return [
4832
+ formatServerMarker(),
4833
+ "/** DO NOT EDIT \u2014 owned by the module install rail. */",
4834
+ "",
4835
+ ...importLines,
4836
+ ...importLines.length > 0 ? [""] : [],
4837
+ "export type ModuleServerRegistration = {",
4838
+ " module: string;",
4839
+ " registrations: Readonly<Record<string, unknown>>;",
4840
+ "};",
4841
+ "",
4842
+ ...entryLines.length === 0 ? ["export const MODULE_SERVER_REGISTRATIONS: readonly ModuleServerRegistration[] = [];"] : [
4843
+ "export const MODULE_SERVER_REGISTRATIONS: readonly ModuleServerRegistration[] = [",
4844
+ ...entryLines,
4845
+ "];"
4846
+ ],
4847
+ ""
4848
+ ].join("\n");
4849
+ }
4808
4850
 
4809
4851
  // ../../packages/lib/src/server/module-rail/composition-artifacts.ts
4810
4852
  var COMPOSITION_ARTIFACTS_GENERATED_BY = "module-rail";
@@ -4820,8 +4862,8 @@ var METHOD_ORDER = {
4820
4862
  function compareNames2(a, b) {
4821
4863
  return a < b ? -1 : a > b ? 1 : 0;
4822
4864
  }
4823
- function endpointPairKey(method, path3) {
4824
- return `${method} ${path3}`;
4865
+ function endpointPairKey(method, path5) {
4866
+ return `${method} ${path5}`;
4825
4867
  }
4826
4868
  function apiPathToRouteFile(apiPath) {
4827
4869
  if (!apiPath.startsWith("/api/")) {
@@ -4835,11 +4877,11 @@ function routeEntryToAppPageFile(nextRelativePath) {
4835
4877
  }
4836
4878
  var APP_ROUTE_FILE_PREFIX = `${APP_PACKAGE_DIR}/src/app/`;
4837
4879
  var API_ROUTE_FILE_SUFFIXES = ["/route.ts", "/route.tsx", "/route.jsx", "/route.js"];
4838
- function appRouteFileToNextRelativePath(path3) {
4839
- const pagePath = appPageFileToNextRelativePath(path3, APP_PACKAGE_DIR);
4880
+ function appRouteFileToNextRelativePath(path5) {
4881
+ const pagePath = appPageFileToNextRelativePath(path5, APP_PACKAGE_DIR);
4840
4882
  if (pagePath != null) return pagePath;
4841
- if (!path3.startsWith(APP_ROUTE_FILE_PREFIX)) return null;
4842
- const relative = path3.slice(APP_ROUTE_FILE_PREFIX.length);
4883
+ if (!path5.startsWith(APP_ROUTE_FILE_PREFIX)) return null;
4884
+ const relative = path5.slice(APP_ROUTE_FILE_PREFIX.length);
4843
4885
  if (!relative.startsWith("api/")) return null;
4844
4886
  const suffix = API_ROUTE_FILE_SUFFIXES.find((candidate) => relative.endsWith(candidate));
4845
4887
  if (!suffix) return null;
@@ -4867,6 +4909,14 @@ function formatRouteMarker(owner, nextRelativePath) {
4867
4909
  function formatRegistryMarker() {
4868
4910
  return `// ${MODULE_STUB_MARKER} scope=registry`;
4869
4911
  }
4912
+ var OWNERLESS_MARKER_KINDS = [
4913
+ "registry",
4914
+ "contributions",
4915
+ "datastores",
4916
+ "i18n",
4917
+ "init",
4918
+ "server"
4919
+ ];
4870
4920
  function parseCompositionArtifactMarker(source) {
4871
4921
  const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
4872
4922
  const match = firstLine.match(
@@ -4877,26 +4927,8 @@ function parseCompositionArtifactMarker(source) {
4877
4927
  if (!match) return null;
4878
4928
  const owner = match[1];
4879
4929
  const scope = match[2];
4880
- if (scope === "registry") {
4881
- if (owner) return null;
4882
- return { kind: "registry" };
4883
- }
4884
- if (scope === "contributions") {
4885
- if (owner) return null;
4886
- return { kind: "contributions" };
4887
- }
4888
- if (scope === "datastores") {
4889
- if (owner) return null;
4890
- return { kind: "datastores" };
4891
- }
4892
- if (scope === "i18n") {
4893
- if (owner) return null;
4894
- return { kind: "i18n" };
4895
- }
4896
- if (scope === "init") {
4897
- if (owner) return null;
4898
- return { kind: "init" };
4899
- }
4930
+ const ownerless = OWNERLESS_MARKER_KINDS.find((kind) => kind === scope);
4931
+ if (ownerless) return owner ? null : { kind: ownerless };
4900
4932
  if (!owner || !isValidModuleName(owner)) return null;
4901
4933
  if (scope === "route") {
4902
4934
  const nextRelativePath = match[3]?.trim();
@@ -4941,9 +4973,9 @@ function legacyHandlerPropertyMatchesPath(property, method, physicalPath, bindin
4941
4973
  const expectedTokens = knownAliases[`${binding} ${method} ${physicalPath}`] ?? pathTokens;
4942
4974
  return handlerTokens.length === expectedTokens.length && handlerTokens.every((token, index) => token === expectedTokens[index]);
4943
4975
  }
4944
- function parseLegacyApiAdapter(path3, source) {
4945
- const physicalPath = appRouteFileToNextRelativePath(path3);
4946
- if (expectedMarkerKindForPath(path3) !== "endpoint" || physicalPath == null) return null;
4976
+ function parseLegacyApiAdapter(path5, source) {
4977
+ const physicalPath = appRouteFileToNextRelativePath(path5);
4978
+ if (expectedMarkerKindForPath(path5) !== "endpoint" || physicalPath == null) return null;
4947
4979
  const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
4948
4980
  const match = firstLine.match(
4949
4981
  new RegExp(
@@ -4953,7 +4985,7 @@ function parseLegacyApiAdapter(path3, source) {
4953
4985
  if (!match || match[2] !== physicalPath) return null;
4954
4986
  const owner = match[1];
4955
4987
  if (!isValidModuleName(owner)) return null;
4956
- const file = import_typescript4.default.createSourceFile(path3, source, import_typescript4.default.ScriptTarget.Latest, false, import_typescript4.default.ScriptKind.TS);
4988
+ const file = import_typescript4.default.createSourceFile(path5, source, import_typescript4.default.ScriptTarget.Latest, false, import_typescript4.default.ScriptKind.TS);
4957
4989
  if (file.parseDiagnostics.length > 0) {
4958
4990
  return null;
4959
4991
  }
@@ -4995,9 +5027,9 @@ function parseLegacyApiAdapter(path3, source) {
4995
5027
  }
4996
5028
  return handlerBinding != null && methods.size > 0 ? { owner, methods: [...methods] } : null;
4997
5029
  }
4998
- function parseLegacyRouteAdapter(path3, source) {
4999
- const physicalPath = appRouteFileToNextRelativePath(path3);
5000
- if (expectedMarkerKindForPath(path3) !== "route" || physicalPath == null) return null;
5030
+ function parseLegacyRouteAdapter(path5, source) {
5031
+ const physicalPath = appRouteFileToNextRelativePath(path5);
5032
+ if (expectedMarkerKindForPath(path5) !== "route" || physicalPath == null) return null;
5001
5033
  const normalized = source.replace(/\r\n/g, "\n");
5002
5034
  const match = normalized.match(
5003
5035
  new RegExp(
@@ -5009,29 +5041,30 @@ function parseLegacyRouteAdapter(path3, source) {
5009
5041
  const nextRelativePath = match[2];
5010
5042
  return isValidModuleName(owner) && nextRelativePath === physicalPath ? { owner, nextRelativePath } : null;
5011
5043
  }
5012
- function legacyRouteAdapterMatchesStub(path3, source, stub) {
5013
- const legacy = parseLegacyRouteAdapter(path3, source);
5044
+ function legacyRouteAdapterMatchesStub(path5, source, stub) {
5045
+ const legacy = parseLegacyRouteAdapter(path5, source);
5014
5046
  return legacy != null && legacy.owner === stub.owner;
5015
5047
  }
5016
5048
  var API_ROUTE_DIR_PREFIX = `${APP_PACKAGE_DIR}/src/app/api/`;
5017
5049
  var APP_DIR_PREFIX = `${APP_PACKAGE_DIR}/src/app/`;
5018
5050
  var PAGE_FILE_SUFFIXES = ["/page.tsx", "/page.ts", "/page.jsx", "/page.js"];
5019
- function expectedMarkerKindForPath(path3) {
5020
- if (path3 === MODULES_GEN_PATH) return "registry";
5021
- if (path3 === MODULE_CONTRIBUTIONS_GEN_PATH) return "contributions";
5022
- if (path3 === MODULE_DATASTORES_GEN_PATH) return "datastores";
5023
- if (path3 === MODULE_I18N_GEN_PATH) return "i18n";
5024
- if (path3 === MODULE_INIT_SERVER_GEN_PATH) return "init";
5025
- if (path3.startsWith(API_ROUTE_DIR_PREFIX) && path3.endsWith("/route.ts")) return "endpoint";
5026
- if (path3.startsWith(APP_DIR_PREFIX) && !path3.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path3.endsWith(suffix))) {
5051
+ function expectedMarkerKindForPath(path5) {
5052
+ if (path5 === MODULES_GEN_PATH) return "registry";
5053
+ if (path5 === MODULE_CONTRIBUTIONS_GEN_PATH) return "contributions";
5054
+ if (path5 === MODULE_DATASTORES_GEN_PATH) return "datastores";
5055
+ if (path5 === MODULE_I18N_GEN_PATH) return "i18n";
5056
+ if (path5 === MODULE_INIT_SERVER_GEN_PATH) return "init";
5057
+ if (path5 === MODULE_SERVER_GEN_PATH) return "server";
5058
+ if (path5.startsWith(API_ROUTE_DIR_PREFIX) && path5.endsWith("/route.ts")) return "endpoint";
5059
+ if (path5.startsWith(APP_DIR_PREFIX) && !path5.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path5.endsWith(suffix))) {
5027
5060
  return "route";
5028
5061
  }
5029
5062
  return null;
5030
5063
  }
5031
- function markerCongruentWithPath(path3, marker) {
5032
- if (expectedMarkerKindForPath(path3) !== marker.kind) return false;
5064
+ function markerCongruentWithPath(path5, marker) {
5065
+ if (expectedMarkerKindForPath(path5) !== marker.kind) return false;
5033
5066
  if (marker.kind === "route") {
5034
- const derived = appPageFileToNextRelativePath(path3, APP_PACKAGE_DIR);
5067
+ const derived = appPageFileToNextRelativePath(path5, APP_PACKAGE_DIR);
5035
5068
  return derived != null && derived === marker.nextRelativePath;
5036
5069
  }
5037
5070
  return true;
@@ -5302,8 +5335,8 @@ function buildGeneratedModuleRegistry(input) {
5302
5335
  function activeEnhancementPeers(registryModule) {
5303
5336
  return new Set(registryModule.enhancements.filter((e) => e.active).map((e) => e.peer));
5304
5337
  }
5305
- function endpointBucketKey(path3, owner, scope) {
5306
- return `${path3}\0${owner}\0${scopeLabel(scope)}`;
5338
+ function endpointBucketKey(path5, owner, scope) {
5339
+ return `${path5}\0${owner}\0${scopeLabel(scope)}`;
5307
5340
  }
5308
5341
  function planRouteStubs(input) {
5309
5342
  const endpointBuckets = /* @__PURE__ */ new Map();
@@ -5427,7 +5460,7 @@ function planRouteStubs(input) {
5427
5460
  for (const entry of entries) {
5428
5461
  const nextRelativePath = entry.nextRelativePath;
5429
5462
  const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
5430
- const path3 = routeEntryToAppPageFile(nextRelativePath);
5463
+ const path5 = routeEntryToAppPageFile(nextRelativePath);
5431
5464
  const existing = routeDestinationOwners.get(destination);
5432
5465
  if (existing) {
5433
5466
  throw new Error(
@@ -5437,13 +5470,13 @@ function planRouteStubs(input) {
5437
5470
  routeDestinationOwners.set(destination, {
5438
5471
  owner: regMod.name,
5439
5472
  nextRelativePath,
5440
- path: path3
5473
+ path: path5
5441
5474
  });
5442
5475
  const importModule = routeEntryImport(regMod.name, nextRelativePath);
5443
5476
  const analyzed = analyzeRouteEntryPath(nextRelativePath);
5444
5477
  const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
5445
5478
  routeStubs.push({
5446
- path: path3,
5479
+ path: path5,
5447
5480
  owner: regMod.name,
5448
5481
  nextRelativePath,
5449
5482
  importModule,
@@ -5504,16 +5537,16 @@ function planCompositionArtifacts(input) {
5504
5537
  ...stubs.routeStubs.map((stub) => stub.path)
5505
5538
  ]);
5506
5539
  const existingGeneratedPathSet = new Set(input.existingGeneratedPaths);
5507
- const isProvenStaleDeletionCandidate = (path3, content) => {
5508
- if (path3 === MODULES_GEN_PATH) return false;
5509
- if (COMPOSITION_RUNTIME_GEN_PATHS.includes(path3)) return false;
5510
- if (desiredPaths.has(path3)) return false;
5511
- if (!existingGeneratedPathSet.has(path3)) return false;
5512
- const legacy = parseLegacyApiAdapter(path3, content);
5540
+ const isProvenStaleDeletionCandidate = (path5, content) => {
5541
+ if (path5 === MODULES_GEN_PATH) return false;
5542
+ if (COMPOSITION_RUNTIME_GEN_PATHS.includes(path5)) return false;
5543
+ if (desiredPaths.has(path5)) return false;
5544
+ if (!existingGeneratedPathSet.has(path5)) return false;
5545
+ const legacy = parseLegacyApiAdapter(path5, content);
5513
5546
  if (legacy) return true;
5514
- if (parseLegacyRouteAdapter(path3, content)) return true;
5547
+ if (parseLegacyRouteAdapter(path5, content)) return true;
5515
5548
  const marker = parseCompositionArtifactMarker(content);
5516
- return marker != null && markerCongruentWithPath(path3, marker);
5549
+ return marker != null && markerCongruentWithPath(path5, marker);
5517
5550
  };
5518
5551
  const plannedByDestination = /* @__PURE__ */ new Map();
5519
5552
  for (const stub of stubs.routeStubs) {
@@ -5621,6 +5654,7 @@ function planCompositionArtifacts(input) {
5621
5654
  let datastoresContent;
5622
5655
  let i18nContent;
5623
5656
  let initContent;
5657
+ let serverContent;
5624
5658
  try {
5625
5659
  contributionsContent = emitModuleContributionsGenTs({
5626
5660
  registry: input.registry,
@@ -5645,6 +5679,10 @@ function planCompositionArtifacts(input) {
5645
5679
  registry: input.registry,
5646
5680
  facts: compositionEntryFacts
5647
5681
  });
5682
+ serverContent = emitModuleServerGenTs({
5683
+ registry: input.registry,
5684
+ facts: compositionEntryFacts
5685
+ });
5648
5686
  } catch (error) {
5649
5687
  return {
5650
5688
  ok: false,
@@ -5657,17 +5695,18 @@ function planCompositionArtifacts(input) {
5657
5695
  desired.set(MODULE_DATASTORES_GEN_PATH, datastoresContent);
5658
5696
  desired.set(MODULE_I18N_GEN_PATH, i18nContent);
5659
5697
  desired.set(MODULE_INIT_SERVER_GEN_PATH, initContent);
5698
+ desired.set(MODULE_SERVER_GEN_PATH, serverContent);
5660
5699
  for (const stub of stubs.endpointStubs) desired.set(stub.path, stub.content);
5661
5700
  for (const stub of stubs.routeStubs) desired.set(stub.path, stub.content);
5662
5701
  const endpointStubByPath = new Map(stubs.endpointStubs.map((stub) => [stub.path, stub]));
5663
5702
  const routeStubByPath = new Map(stubs.routeStubs.map((stub) => [stub.path, stub]));
5664
- for (const [path3] of desired) {
5665
- const existing = input.existingFiles[path3];
5703
+ for (const [path5] of desired) {
5704
+ const existing = input.existingFiles[path5];
5666
5705
  if (existing == null) continue;
5667
5706
  let marker = parseCompositionArtifactMarker(existing);
5668
- const plannedEndpoint = endpointStubByPath.get(path3);
5669
- const plannedRoute = routeStubByPath.get(path3);
5670
- const legacy = parseLegacyApiAdapter(path3, existing);
5707
+ const plannedEndpoint = endpointStubByPath.get(path5);
5708
+ const plannedRoute = routeStubByPath.get(path5);
5709
+ const legacy = parseLegacyApiAdapter(path5, existing);
5671
5710
  if (legacy && plannedEndpoint) {
5672
5711
  marker = {
5673
5712
  kind: "endpoint",
@@ -5675,66 +5714,66 @@ function planCompositionArtifacts(input) {
5675
5714
  scope: plannedEndpoint.scope
5676
5715
  };
5677
5716
  }
5678
- if (!marker && plannedRoute && legacyRouteAdapterMatchesStub(path3, existing, plannedRoute)) {
5717
+ if (!marker && plannedRoute && legacyRouteAdapterMatchesStub(path5, existing, plannedRoute)) {
5679
5718
  marker = {
5680
5719
  kind: "route",
5681
5720
  owner: plannedRoute.owner,
5682
5721
  nextRelativePath: plannedRoute.nextRelativePath
5683
5722
  };
5684
5723
  }
5685
- const isGenRuntimePath = COMPOSITION_RUNTIME_GEN_PATHS.includes(path3);
5724
+ const isGenRuntimePath = COMPOSITION_RUNTIME_GEN_PATHS.includes(path5);
5686
5725
  if (!marker) {
5687
5726
  return {
5688
5727
  ok: false,
5689
- error: path3 === MODULES_GEN_PATH ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged registry at ${path3}` : isGenRuntimePath ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged generated file at ${path3}` : `[ModuleRail] composition artifacts: refusing to overwrite unmanaged route at ${path3}`,
5690
- path: path3
5728
+ error: path5 === MODULES_GEN_PATH ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged registry at ${path5}` : isGenRuntimePath ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged generated file at ${path5}` : `[ModuleRail] composition artifacts: refusing to overwrite unmanaged route at ${path5}`,
5729
+ path: path5
5691
5730
  };
5692
5731
  }
5693
- const expectedKind = expectedMarkerKindForPath(path3);
5732
+ const expectedKind = expectedMarkerKindForPath(path5);
5694
5733
  if (marker.kind !== expectedKind) {
5695
5734
  return {
5696
5735
  ok: false,
5697
- error: path3 === MODULES_GEN_PATH ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged registry at ${path3} (incongruent marker ownership: found ${marker.kind} marker)` : isGenRuntimePath ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged generated file at ${path3} (incongruent marker ownership: found ${marker.kind} marker)` : `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path3} (expected ${expectedKind ?? "no"} marker, found ${marker.kind} marker)`,
5698
- path: path3
5736
+ error: path5 === MODULES_GEN_PATH ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged registry at ${path5} (incongruent marker ownership: found ${marker.kind} marker)` : isGenRuntimePath ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged generated file at ${path5} (incongruent marker ownership: found ${marker.kind} marker)` : `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path5} (expected ${expectedKind ?? "no"} marker, found ${marker.kind} marker)`,
5737
+ path: path5
5699
5738
  };
5700
5739
  }
5701
5740
  if (marker.kind === "endpoint") {
5702
- const stub = endpointStubByPath.get(path3);
5741
+ const stub = endpointStubByPath.get(path5);
5703
5742
  const ownerMatches = stub != null && marker.owner === stub.owner;
5704
5743
  const scopeMatches = stub != null && scopeLabel(marker.scope) === scopeLabel(stub.scope);
5705
5744
  if (!ownerMatches || !scopeMatches) {
5706
5745
  return {
5707
5746
  ok: false,
5708
- error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path3} (existing owner=${marker.owner} scope=${scopeLabel(marker.scope)} vs planned owner=${stub?.owner ?? "?"} scope=${stub ? scopeLabel(stub.scope) : "?"})`,
5709
- path: path3,
5747
+ error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path5} (existing owner=${marker.owner} scope=${scopeLabel(marker.scope)} vs planned owner=${stub?.owner ?? "?"} scope=${stub ? scopeLabel(stub.scope) : "?"})`,
5748
+ path: path5,
5710
5749
  owner: stub?.owner
5711
5750
  };
5712
5751
  }
5713
5752
  } else if (marker.kind === "route") {
5714
- const stub = routeStubByPath.get(path3);
5753
+ const stub = routeStubByPath.get(path5);
5715
5754
  const ownerMatches = stub != null && marker.owner === stub.owner;
5716
5755
  const pathMatches = stub != null && marker.nextRelativePath === stub.nextRelativePath;
5717
5756
  if (!ownerMatches || !pathMatches) {
5718
5757
  return {
5719
5758
  ok: false,
5720
- error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path3} (existing owner=${marker.owner} path=${marker.nextRelativePath} vs planned owner=${stub?.owner ?? "?"} path=${stub?.nextRelativePath ?? "?"})`,
5721
- path: path3,
5759
+ error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path5} (existing owner=${marker.owner} path=${marker.nextRelativePath} vs planned owner=${stub?.owner ?? "?"} path=${stub?.nextRelativePath ?? "?"})`,
5760
+ path: path5,
5722
5761
  owner: stub?.owner
5723
5762
  };
5724
5763
  }
5725
5764
  }
5726
5765
  }
5727
5766
  const deletes = [];
5728
- for (const path3 of input.existingGeneratedPaths) {
5729
- const existing = input.existingFiles[path3];
5767
+ for (const path5 of input.existingGeneratedPaths) {
5768
+ const existing = input.existingFiles[path5];
5730
5769
  if (existing == null) continue;
5731
- if (!isProvenStaleDeletionCandidate(path3, existing)) {
5770
+ if (!isProvenStaleDeletionCandidate(path5, existing)) {
5732
5771
  continue;
5733
5772
  }
5734
- deletes.push(path3);
5773
+ deletes.push(path5);
5735
5774
  }
5736
5775
  deletes.sort(compareNames2);
5737
- const writes = [...desired.entries()].filter(([path3, content]) => input.existingFiles[path3] !== content).map(([path3, content]) => ({ path: path3, content })).sort((a, b) => compareNames2(a.path, b.path));
5776
+ const writes = [...desired.entries()].filter(([path5, content]) => input.existingFiles[path5] !== content).map(([path5, content]) => ({ path: path5, content })).sort((a, b) => compareNames2(a.path, b.path));
5738
5777
  return {
5739
5778
  ok: true,
5740
5779
  plan: {
@@ -5763,8 +5802,16 @@ async function checkoutParsesTypedStores(sandbox) {
5763
5802
  try {
5764
5803
  if (!await sandbox.fileExists(APP_PACKAGE_JSON_PATH)) return false;
5765
5804
  const pkg = JSON.parse(await sandbox.readFile(APP_PACKAGE_JSON_PATH));
5766
- const range = pkg.devDependencies?.[COMPOSE_PACKAGE] ?? pkg.dependencies?.[COMPOSE_PACKAGE];
5767
- if (!range) return false;
5805
+ return composeRangeParsesTypedStores(
5806
+ pkg.devDependencies?.[COMPOSE_PACKAGE] ?? pkg.dependencies?.[COMPOSE_PACKAGE]
5807
+ );
5808
+ } catch {
5809
+ return false;
5810
+ }
5811
+ }
5812
+ function composeRangeParsesTypedStores(range) {
5813
+ if (!range) return false;
5814
+ try {
5768
5815
  const lowest = import_semver.default.minVersion(range);
5769
5816
  return lowest != null && import_semver.default.gte(lowest, TYPED_STORES_MIN_COMPOSE_VERSION);
5770
5817
  } catch {
@@ -6209,11 +6256,11 @@ function countedCompositionContext(context) {
6209
6256
  counts.sandboxExecs += 1;
6210
6257
  return context.sandbox.exec(...args);
6211
6258
  },
6212
- readFile: (path3) => {
6259
+ readFile: (path5) => {
6213
6260
  counts.sandboxReads += 1;
6214
- return context.sandbox.readFile(path3);
6261
+ return context.sandbox.readFile(path5);
6215
6262
  },
6216
- fileExists: (path3) => context.sandbox.fileExists(path3),
6263
+ fileExists: (path5) => context.sandbox.fileExists(path5),
6217
6264
  fetchRemoteRef: (...args) => context.sandbox.fetchRemoteRef(...args),
6218
6265
  addDependencies: (specs) => context.sandbox.addDependencies(specs)
6219
6266
  }
@@ -6425,14 +6472,14 @@ async function discoverModuleRouteEntries(context, moduleName) {
6425
6472
  }
6426
6473
  return pages.sort((a, b) => a.nextRelativePath.localeCompare(b.nextRelativePath));
6427
6474
  }
6428
- async function writeSandboxFileAtomic(target, path3, content) {
6429
- const dir = dirnamePosix(path3);
6430
- const tmp = `${path3}.tmp.${target.runId}`;
6475
+ async function writeSandboxFileAtomic(target, path5, content) {
6476
+ const dir = dirnamePosix(path5);
6477
+ const tmp = `${path5}.tmp.${target.runId}`;
6431
6478
  const b64 = Buffer.from(content, "utf8").toString("base64");
6432
6479
  const cmd = [
6433
6480
  `mkdir -p ${shellQuote(dir)}`,
6434
6481
  `printf '%s' ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)}`,
6435
- `mv -f ${shellQuote(tmp)} ${shellQuote(path3)}`
6482
+ `mv -f ${shellQuote(tmp)} ${shellQuote(path5)}`
6436
6483
  ].join(" && ");
6437
6484
  const result = await target.sandbox.exec(cmd, { raiseOnError: false });
6438
6485
  if (result.exitCode !== 0) {
@@ -6447,14 +6494,14 @@ async function writeSandboxFileAtomic(target, path3, content) {
6447
6494
  }
6448
6495
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
6449
6496
  throw new Error(
6450
- `[ModuleRail] composition artifacts: failed to write ${redactSecrets(path3)}: ${redactSecrets(detail)}`
6497
+ `[ModuleRail] composition artifacts: failed to write ${redactSecrets(path5)}: ${redactSecrets(detail)}`
6451
6498
  );
6452
6499
  }
6453
6500
  }
6454
6501
  async function collectAbsentWriteParentDirs(context, writePaths) {
6455
6502
  const absent = /* @__PURE__ */ new Set();
6456
- for (const path3 of writePaths) {
6457
- let dir = dirnamePosix(path3);
6503
+ for (const path5 of writePaths) {
6504
+ let dir = dirnamePosix(path5);
6458
6505
  while (dir !== "." && dir !== "") {
6459
6506
  if (absent.has(dir)) {
6460
6507
  dir = dirnamePosix(dir);
@@ -6491,12 +6538,12 @@ async function removeAbsentWriteParentDirsOnRollback(context, dirsDeepestFirst)
6491
6538
  );
6492
6539
  }
6493
6540
  }
6494
- async function deleteSandboxFile(context, path3) {
6495
- const result = await context.sandbox.exec(`rm -f ${shellQuote(path3)}`, { raiseOnError: false });
6541
+ async function deleteSandboxFile(context, path5) {
6542
+ const result = await context.sandbox.exec(`rm -f ${shellQuote(path5)}`, { raiseOnError: false });
6496
6543
  if (result.exitCode !== 0) {
6497
6544
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
6498
6545
  throw new Error(
6499
- `[ModuleRail] composition artifacts: failed to delete ${redactSecrets(path3)}: ${redactSecrets(detail)}`
6546
+ `[ModuleRail] composition artifacts: failed to delete ${redactSecrets(path5)}: ${redactSecrets(detail)}`
6500
6547
  );
6501
6548
  }
6502
6549
  }
@@ -6563,37 +6610,37 @@ async function applyCompositionArtifactMutations(context, plan, existingFiles) {
6563
6610
  await writeSandboxFileAtomic(context, write.path, write.content);
6564
6611
  appliedWrites.push({ path: write.path, previous });
6565
6612
  }
6566
- for (const path3 of plan.deletes) {
6567
- const previous = existingFiles[path3];
6613
+ for (const path5 of plan.deletes) {
6614
+ const previous = existingFiles[path5];
6568
6615
  if (previous == null) continue;
6569
- await deleteSandboxFile(context, path3);
6570
- appliedDeletes.push({ path: path3, previous });
6616
+ await deleteSandboxFile(context, path5);
6617
+ appliedDeletes.push({ path: path5, previous });
6571
6618
  }
6572
6619
  } catch (error) {
6573
6620
  console.log(
6574
6621
  `[ModuleRail] runId=${context.runId} composition artifacts mid-apply failure; rolling back writes=${appliedWrites.length} deletes=${appliedDeletes.length} newDirs=${newlyCreatedParentDirs.length}`
6575
6622
  );
6576
- for (const { path: path3, previous } of [...appliedDeletes].reverse()) {
6623
+ for (const { path: path5, previous } of [...appliedDeletes].reverse()) {
6577
6624
  try {
6578
- await writeSandboxFileAtomic(context, path3, previous);
6625
+ await writeSandboxFileAtomic(context, path5, previous);
6579
6626
  } catch (rollbackError) {
6580
6627
  console.log(
6581
- `[ModuleRail] runId=${context.runId} composition artifacts rollback restore failed for ${redactSecrets(path3)}: ${redactSecrets(
6628
+ `[ModuleRail] runId=${context.runId} composition artifacts rollback restore failed for ${redactSecrets(path5)}: ${redactSecrets(
6582
6629
  rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
6583
6630
  )}`
6584
6631
  );
6585
6632
  }
6586
6633
  }
6587
- for (const { path: path3, previous } of [...appliedWrites].reverse()) {
6634
+ for (const { path: path5, previous } of [...appliedWrites].reverse()) {
6588
6635
  try {
6589
6636
  if (previous == null) {
6590
- await deleteSandboxFile(context, path3);
6637
+ await deleteSandboxFile(context, path5);
6591
6638
  } else {
6592
- await writeSandboxFileAtomic(context, path3, previous);
6639
+ await writeSandboxFileAtomic(context, path5, previous);
6593
6640
  }
6594
6641
  } catch (rollbackError) {
6595
6642
  console.log(
6596
- `[ModuleRail] runId=${context.runId} composition artifacts rollback write failed for ${redactSecrets(path3)}: ${redactSecrets(
6643
+ `[ModuleRail] runId=${context.runId} composition artifacts rollback write failed for ${redactSecrets(path5)}: ${redactSecrets(
6597
6644
  rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
6598
6645
  )}`
6599
6646
  );
@@ -6627,39 +6674,39 @@ async function listExistingGeneratedStubPaths(context) {
6627
6674
  COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN
6628
6675
  );
6629
6676
  const presentPaths = [];
6630
- for (const path3 of candidates) {
6677
+ for (const path5 of candidates) {
6631
6678
  const presence = assertRealCompositionArtifactFilePresence(
6632
- path3,
6633
- kinds.get(path3) ?? "unreadable"
6679
+ path5,
6680
+ kinds.get(path5) ?? "unreadable"
6634
6681
  );
6635
- if (presence === "file") presentPaths.push(path3);
6682
+ if (presence === "file") presentPaths.push(path5);
6636
6683
  }
6637
6684
  let contents;
6638
6685
  try {
6639
6686
  contents = await readFilesMany(context, presentPaths);
6640
6687
  } catch (error) {
6641
- const path3 = readFailurePath(error);
6688
+ const path5 = readFailurePath(error);
6642
6689
  throw new Error(
6643
- `[ModuleRail] composition artifacts: cannot read candidate stub ${redactSecrets(path3)}: ${redactSecrets(
6690
+ `[ModuleRail] composition artifacts: cannot read candidate stub ${redactSecrets(path5)}: ${redactSecrets(
6644
6691
  error instanceof Error ? error.message : String(error)
6645
6692
  )}`
6646
6693
  );
6647
6694
  }
6648
6695
  const owned = [];
6649
- for (const path3 of presentPaths) {
6650
- const content = contents.get(path3);
6696
+ for (const path5 of presentPaths) {
6697
+ const content = contents.get(path5);
6651
6698
  const marker = parseCompositionArtifactMarker(content);
6652
- if (parseLegacyApiAdapter(path3, content) || parseLegacyRouteAdapter(path3, content)) {
6653
- owned.push(path3);
6699
+ if (parseLegacyApiAdapter(path5, content) || parseLegacyRouteAdapter(path5, content)) {
6700
+ owned.push(path5);
6654
6701
  continue;
6655
6702
  }
6656
6703
  if (marker == null) continue;
6657
- if (!markerCongruentWithPath(path3, marker)) {
6704
+ if (!markerCongruentWithPath(path5, marker)) {
6658
6705
  throw new Error(
6659
- `[ModuleRail] composition artifacts: incongruent marker at ${redactSecrets(path3)} (found ${marker.kind} marker on a path shaped for ${expectedMarkerKindForPath(path3) ?? "no"} artifacts; refusing to reconcile a swapped marker)`
6706
+ `[ModuleRail] composition artifacts: incongruent marker at ${redactSecrets(path5)} (found ${marker.kind} marker on a path shaped for ${expectedMarkerKindForPath(path5) ?? "no"} artifacts; refusing to reconcile a swapped marker)`
6660
6707
  );
6661
6708
  }
6662
- owned.push(path3);
6709
+ owned.push(path5);
6663
6710
  }
6664
6711
  return owned;
6665
6712
  }
@@ -6792,7 +6839,7 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6792
6839
  { raiseOnError: false }
6793
6840
  );
6794
6841
  if (result.exitCode !== 0) {
6795
- for (const path3 of chunk) kinds.set(path3, "unreadable");
6842
+ for (const path5 of chunk) kinds.set(path5, "unreadable");
6796
6843
  continue;
6797
6844
  }
6798
6845
  const lines = result.output.split("\n").filter(Boolean);
@@ -6803,31 +6850,31 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6803
6850
  }
6804
6851
  } else {
6805
6852
  for (const line of lines) {
6806
- const [rawKind, path3] = line.split(" ", 2);
6853
+ const [rawKind, path5] = line.split(" ", 2);
6807
6854
  const kind = rawKind?.replace(/^entry-kind:/, "");
6808
- if (path3 && chunk.includes(path3) && CONVENTIONAL_ENTRY_PRESENCE_KINDS.includes(kind ?? "")) {
6809
- kinds.set(path3, kind);
6855
+ if (path5 && chunk.includes(path5) && CONVENTIONAL_ENTRY_PRESENCE_KINDS.includes(kind ?? "")) {
6856
+ kinds.set(path5, kind);
6810
6857
  }
6811
6858
  }
6812
6859
  }
6813
- for (const path3 of chunk) {
6814
- if (!kinds.has(path3)) kinds.set(path3, "unreadable");
6860
+ for (const path5 of chunk) {
6861
+ if (!kinds.has(path5)) kinds.set(path5, "unreadable");
6815
6862
  }
6816
6863
  }
6817
6864
  return kinds;
6818
6865
  }
6819
- async function classifySandboxPathPresenceKind(context, path3, probeToken) {
6820
- return (await classifySandboxPathPresenceKindMany(context, [path3], probeToken)).get(path3);
6866
+ async function classifySandboxPathPresenceKind(context, path5, probeToken) {
6867
+ return (await classifySandboxPathPresenceKindMany(context, [path5], probeToken)).get(path5);
6821
6868
  }
6822
6869
  var LOCALE_JSON_KIND_PROBE_TOKEN = "locale-json-kind";
6823
- function assertRealLocaleJsonFilePresence(path3, presence) {
6870
+ function assertRealLocaleJsonFilePresence(path5, presence) {
6824
6871
  if (presence === "file") return;
6825
6872
  if (presence === "absent") {
6826
- throw new Error(`[ModuleRail] composition artifacts: locale file missing at ${path3}`);
6873
+ throw new Error(`[ModuleRail] composition artifacts: locale file missing at ${path5}`);
6827
6874
  }
6828
- const presenceError = formatConventionalEntryPresenceError(path3, presence);
6875
+ const presenceError = formatConventionalEntryPresenceError(path5, presence);
6829
6876
  throw new Error(
6830
- `[ModuleRail] composition artifacts: ${presenceError ?? `${path3} is not a real locale JSON file`}`
6877
+ `[ModuleRail] composition artifacts: ${presenceError ?? `${path5} is not a real locale JSON file`}`
6831
6878
  );
6832
6879
  }
6833
6880
  function splitTaggedLine(line) {
@@ -6902,11 +6949,11 @@ async function listLocaleBasenamesIfDirMany(context, dirs) {
6902
6949
  }
6903
6950
  async function readFilesMany(context, paths) {
6904
6951
  const sortedPaths = [...paths].sort((a, b) => a.localeCompare(b));
6905
- const settled = await mapConcurrent(sortedPaths, SANDBOX_READ_CONCURRENCY, async (path3) => {
6952
+ const settled = await mapConcurrent(sortedPaths, SANDBOX_READ_CONCURRENCY, async (path5) => {
6906
6953
  try {
6907
- return { path: path3, value: await context.sandbox.readFile(path3) };
6954
+ return { path: path5, value: await context.sandbox.readFile(path5) };
6908
6955
  } catch (error) {
6909
- return { path: path3, error };
6956
+ return { path: path5, error };
6910
6957
  }
6911
6958
  });
6912
6959
  const files = /* @__PURE__ */ new Map();
@@ -6927,55 +6974,60 @@ function readFailurePath(error) {
6927
6974
  return typeof error === "object" && error !== null && "path" in error ? String(error.path) : "unknown";
6928
6975
  }
6929
6976
  var CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN = "conventional-entry-kind";
6977
+ var SERVER_REGISTRATIONS_FILE = "server/registrations.ts";
6930
6978
  var COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN = "composition-artifact-kind";
6931
- async function classifyCompositionArtifactPathKind(context, path3) {
6932
- return classifySandboxPathPresenceKind(context, path3, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
6979
+ async function classifyCompositionArtifactPathKind(context, path5) {
6980
+ return classifySandboxPathPresenceKind(context, path5, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
6933
6981
  }
6934
- function assertRealCompositionArtifactFilePresence(path3, presence) {
6982
+ function assertRealCompositionArtifactFilePresence(path5, presence) {
6935
6983
  if (presence === "file") return "file";
6936
6984
  if (presence === "absent") return "absent";
6937
- const presenceError = formatConventionalEntryPresenceError(path3, presence);
6985
+ const presenceError = formatConventionalEntryPresenceError(path5, presence);
6938
6986
  throw new Error(
6939
- `[ModuleRail] composition artifacts: ${presenceError ?? `${path3} is not a real composition artifact file`}`
6987
+ `[ModuleRail] composition artifacts: ${presenceError ?? `${path5} is not a real composition artifact file`}`
6940
6988
  );
6941
6989
  }
6942
- async function readConventionalEntryIfRealFile(path3, kind, presences, files) {
6943
- const presence = presences.get(path3) ?? "unreadable";
6944
- const presenceError = formatConventionalEntryPresenceError(path3, presence);
6990
+ async function readConventionalEntryIfRealFile(path5, kind, presences, files) {
6991
+ const presence = presences.get(path5) ?? "unreadable";
6992
+ const presenceError = formatConventionalEntryPresenceError(path5, presence);
6945
6993
  if (presenceError) {
6946
6994
  throw new Error(`[ModuleRail] composition artifacts: ${presenceError}`);
6947
6995
  }
6948
6996
  if (presence === "absent") {
6949
6997
  return null;
6950
6998
  }
6951
- const source = files.get(path3);
6999
+ const source = files.get(path5);
6952
7000
  switch (kind) {
6953
7001
  case "contributions":
6954
- assertContributionsExport(source, path3);
6955
- assertClientSafeConventionalEntryImports(source, path3, "contributions");
7002
+ assertContributionsExport(source, path5);
7003
+ assertClientSafeConventionalEntryImports(source, path5, "contributions");
6956
7004
  break;
6957
7005
  case "slotCatalogs":
6958
- assertSlotCatalogsExport(source, path3);
6959
- assertClientSafeConventionalEntryImports(source, path3, "slotCatalogs");
7006
+ assertSlotCatalogsExport(source, path5);
7007
+ assertClientSafeConventionalEntryImports(source, path5, "slotCatalogs");
6960
7008
  break;
6961
7009
  case "initializeEnhancement":
6962
- assertInitializeEnhancementExport(source, path3);
7010
+ assertInitializeEnhancementExport(source, path5);
6963
7011
  break;
6964
7012
  }
6965
7013
  return source;
6966
7014
  }
6967
- async function readJsonObjectFile(path3, files) {
6968
- const raw = files.get(path3);
7015
+ function hasRealServerRegistrations(modulePath, presences) {
7016
+ const path5 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
7017
+ return assertRealCompositionArtifactFilePresence(path5, presences.get(path5) ?? "unreadable") === "file";
7018
+ }
7019
+ async function readJsonObjectFile(path5, files) {
7020
+ const raw = files.get(path5);
6969
7021
  let parsed;
6970
7022
  try {
6971
7023
  parsed = JSON.parse(raw);
6972
7024
  } catch (error) {
6973
7025
  throw new Error(
6974
- `[ModuleRail] composition artifacts: invalid JSON at ${path3}: ${error instanceof Error ? error.message : String(error)}`
7026
+ `[ModuleRail] composition artifacts: invalid JSON at ${path5}: ${error instanceof Error ? error.message : String(error)}`
6975
7027
  );
6976
7028
  }
6977
7029
  if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
6978
- throw new Error(`[ModuleRail] composition artifacts: ${path3} root must be an object`);
7030
+ throw new Error(`[ModuleRail] composition artifacts: ${path5} root must be an object`);
6979
7031
  }
6980
7032
  return parsed;
6981
7033
  }
@@ -6987,6 +7039,9 @@ async function collectCompositionEntryFacts(context, registry) {
6987
7039
  const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
6988
7040
  const localeDirs = [substrateDir, overridesDir];
6989
7041
  const conventionalPaths = [];
7042
+ const serverRegistrationPaths = registry.modules.map(
7043
+ (mod) => `${MODULES_SANDBOX_DIR}/${mod.name}/${SERVER_REGISTRATIONS_FILE}`
7044
+ );
6990
7045
  for (const mod of registry.modules) {
6991
7046
  const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
6992
7047
  localeDirs.push(`${modulePath}/i18n`);
@@ -7015,17 +7070,17 @@ async function collectCompositionEntryFacts(context, registry) {
7015
7070
  const [conventionalPresences, localePresences] = await Promise.all([
7016
7071
  classifySandboxPathPresenceKindMany(
7017
7072
  context,
7018
- conventionalPaths,
7073
+ [...conventionalPaths, ...serverRegistrationPaths],
7019
7074
  CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN
7020
7075
  ),
7021
7076
  classifySandboxPathPresenceKindMany(context, localePaths, LOCALE_JSON_KIND_PROBE_TOKEN)
7022
7077
  ]);
7023
7078
  const presentConventionalPaths = [];
7024
- for (const path3 of conventionalPaths) {
7025
- const presence = conventionalPresences.get(path3) ?? "unreadable";
7026
- if (presence === "file") presentConventionalPaths.push(path3);
7079
+ for (const path5 of conventionalPaths) {
7080
+ const presence = conventionalPresences.get(path5) ?? "unreadable";
7081
+ if (presence === "file") presentConventionalPaths.push(path5);
7027
7082
  }
7028
- const presentLocalePaths = localePaths.filter((path3) => localePresences.get(path3) === "file");
7083
+ const presentLocalePaths = localePaths.filter((path5) => localePresences.get(path5) === "file");
7029
7084
  const files = await readFilesMany(context, [...presentConventionalPaths, ...presentLocalePaths]);
7030
7085
  const substrateLocales = [];
7031
7086
  for (const locale of substrateNames) {
@@ -7077,6 +7132,7 @@ async function collectCompositionEntryFacts(context, registry) {
7077
7132
  );
7078
7133
  }
7079
7134
  }
7135
+ const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
7080
7136
  const rootLocales = localesByDir.get(`${modulePath}/i18n`);
7081
7137
  if (rootLocales.length > 0 && !rootLocales.includes("en")) {
7082
7138
  throw new Error(
@@ -7116,13 +7172,14 @@ async function collectCompositionEntryFacts(context, registry) {
7116
7172
  moduleName: mod.name,
7117
7173
  hasRootContributions,
7118
7174
  hasRootSlots,
7175
+ hasServerRegistrations,
7119
7176
  rootLocales,
7120
7177
  enhancements
7121
7178
  });
7122
7179
  if (rootLocales.length > 0) {
7123
7180
  for (const locale of rootLocales) {
7124
- const path3 = `${modulePath}/i18n/${locale}.json`;
7125
- assertRealLocaleJsonFilePresence(path3, localePresences.get(path3) ?? "unreadable");
7181
+ const path5 = `${modulePath}/i18n/${locale}.json`;
7182
+ assertRealLocaleJsonFilePresence(path5, localePresences.get(path5) ?? "unreadable");
7126
7183
  }
7127
7184
  const enMessages = await readJsonObjectFile(`${modulePath}/i18n/en.json`, files);
7128
7185
  const localeMessages = { en: enMessages };
@@ -7146,8 +7203,8 @@ async function collectCompositionEntryFacts(context, registry) {
7146
7203
  if (!enh.active || enh.locales.length === 0) continue;
7147
7204
  const peerDir = `${modulePath}/enhancements/${enh.peer}`;
7148
7205
  for (const locale of enh.locales) {
7149
- const path3 = `${peerDir}/i18n/${locale}.json`;
7150
- assertRealLocaleJsonFilePresence(path3, localePresences.get(path3) ?? "unreadable");
7206
+ const path5 = `${peerDir}/i18n/${locale}.json`;
7207
+ assertRealLocaleJsonFilePresence(path5, localePresences.get(path5) ?? "unreadable");
7151
7208
  }
7152
7209
  const enMessages = await readJsonObjectFile(`${peerDir}/i18n/en.json`, files);
7153
7210
  const localeMessages = { en: enMessages };
@@ -7319,6 +7376,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7319
7376
  MODULE_DATASTORES_GEN_PATH,
7320
7377
  MODULE_I18N_GEN_PATH,
7321
7378
  MODULE_INIT_SERVER_GEN_PATH,
7379
+ MODULE_SERVER_GEN_PATH,
7322
7380
  ...existingGeneratedPaths,
7323
7381
  ...Object.keys(existingAppPages),
7324
7382
  ...Object.keys(existingAppRoutes)
@@ -7347,16 +7405,16 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7347
7405
  for (const stub of probePlan.plan.routeStubs) candidatePaths.add(stub.path);
7348
7406
  const existingFiles = {};
7349
7407
  const remainingCandidatePaths = [];
7350
- for (const path3 of [...candidatePaths].sort((a, b) => a.localeCompare(b))) {
7351
- if (Object.prototype.hasOwnProperty.call(existingAppPages, path3)) {
7352
- existingFiles[path3] = existingAppPages[path3];
7408
+ for (const path5 of [...candidatePaths].sort((a, b) => a.localeCompare(b))) {
7409
+ if (Object.prototype.hasOwnProperty.call(existingAppPages, path5)) {
7410
+ existingFiles[path5] = existingAppPages[path5];
7353
7411
  continue;
7354
7412
  }
7355
- if (Object.prototype.hasOwnProperty.call(existingAppRoutes, path3)) {
7356
- existingFiles[path3] = existingAppRoutes[path3];
7413
+ if (Object.prototype.hasOwnProperty.call(existingAppRoutes, path5)) {
7414
+ existingFiles[path5] = existingAppRoutes[path5];
7357
7415
  continue;
7358
7416
  }
7359
- remainingCandidatePaths.push(path3);
7417
+ remainingCandidatePaths.push(path5);
7360
7418
  }
7361
7419
  const candidateKinds = await classifySandboxPathPresenceKindMany(
7362
7420
  context,
@@ -7364,12 +7422,12 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7364
7422
  COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN
7365
7423
  );
7366
7424
  const presentCandidatePaths = [];
7367
- for (const path3 of remainingCandidatePaths) {
7425
+ for (const path5 of remainingCandidatePaths) {
7368
7426
  const presence = assertRealCompositionArtifactFilePresence(
7369
- path3,
7370
- candidateKinds.get(path3) ?? "unreadable"
7427
+ path5,
7428
+ candidateKinds.get(path5) ?? "unreadable"
7371
7429
  );
7372
- if (presence === "file") presentCandidatePaths.push(path3);
7430
+ if (presence === "file") presentCandidatePaths.push(path5);
7373
7431
  }
7374
7432
  try {
7375
7433
  Object.assign(
@@ -7377,9 +7435,9 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7377
7435
  Object.fromEntries(await readFilesMany(context, presentCandidatePaths))
7378
7436
  );
7379
7437
  } catch (error) {
7380
- const path3 = readFailurePath(error);
7438
+ const path5 = readFailurePath(error);
7381
7439
  throw new Error(
7382
- `[ModuleRail] composition artifacts: cannot read ${redactSecrets(path3)} during preflight: ${redactSecrets(
7440
+ `[ModuleRail] composition artifacts: cannot read ${redactSecrets(path5)} during preflight: ${redactSecrets(
7383
7441
  error instanceof Error ? error.message : String(error)
7384
7442
  )}`
7385
7443
  );
@@ -7448,14 +7506,139 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7448
7506
  var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
7449
7507
  var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
7450
7508
 
7509
+ // src/module-boundaries.ts
7510
+ var import_node_fs2 = __toESM(require("fs"));
7511
+ var import_node_path2 = __toESM(require("path"));
7512
+ var import_typescript5 = __toESM(require("typescript"));
7513
+ var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
7514
+ var RESOLVE_SUFFIXES = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
7515
+ var DATA_FILE = /\.(?:sql|json)$/;
7516
+ function dataPathsIn(fileName, source) {
7517
+ if (!/\.(?:sql|json)["'`]/.test(source)) return [];
7518
+ const paths = [];
7519
+ const visit = (node) => {
7520
+ if ((import_typescript5.default.isStringLiteralLike(node) || import_typescript5.default.isTemplateTail(node)) && DATA_FILE.test(node.text)) {
7521
+ paths.push(node.text);
7522
+ }
7523
+ import_typescript5.default.forEachChild(node, visit);
7524
+ };
7525
+ visit(import_typescript5.default.createSourceFile(fileName, source, import_typescript5.default.ScriptTarget.Latest, false));
7526
+ return paths;
7527
+ }
7528
+ function scanModule(modulesDir, module2) {
7529
+ const scanned = /* @__PURE__ */ new Map();
7530
+ const moduleDir = import_node_path2.default.join(modulesDir, module2);
7531
+ if (!import_node_fs2.default.existsSync(moduleDir)) return scanned;
7532
+ const files = import_node_fs2.default.readdirSync(moduleDir, { recursive: true, encoding: "utf8" }).filter((relative) => SOURCE_FILE.test(relative) && !relative.includes("node_modules"));
7533
+ for (const relative of files) {
7534
+ const absolute = import_node_path2.default.join(moduleDir, relative);
7535
+ if (!import_node_fs2.default.statSync(absolute).isFile()) continue;
7536
+ const source = import_node_fs2.default.readFileSync(absolute, "utf8");
7537
+ const specifiers = import_typescript5.default.preProcessFile(source, true, true).importedFiles.map((f) => f.fileName);
7538
+ const dataPaths = dataPathsIn(absolute, source);
7539
+ const targets = [...specifiers, ...dataPaths].flatMap((spec) => {
7540
+ if (spec.startsWith("@/modules/"))
7541
+ return [import_node_path2.default.join(modulesDir, spec.slice("@/modules/".length))];
7542
+ if (spec.startsWith("./") || spec.startsWith("../")) {
7543
+ return [import_node_path2.default.resolve(import_node_path2.default.dirname(absolute), spec)];
7544
+ }
7545
+ return [];
7546
+ });
7547
+ scanned.set(absolute, {
7548
+ module: module2,
7549
+ relative: relative.split(import_node_path2.default.sep).join("/"),
7550
+ targets,
7551
+ dataPaths
7552
+ });
7553
+ }
7554
+ return scanned;
7555
+ }
7556
+ function findModuleBoundaryViolations(modulesDir, peersByModule, { pruning = false } = {}) {
7557
+ const files = /* @__PURE__ */ new Map();
7558
+ for (const [module2, peers] of Object.entries(peersByModule)) {
7559
+ if (peers.length === 0) continue;
7560
+ for (const [absolute, file] of scanModule(modulesDir, module2)) files.set(absolute, file);
7561
+ }
7562
+ const exempt = (file, peer) => (pruning ? peersByModule[file.module] ?? [] : [peer]).some(
7563
+ (q) => file.relative.startsWith(`enhancements/${q}/`)
7564
+ );
7565
+ const goneWith = (target, peer) => {
7566
+ const relative = import_node_path2.default.relative(modulesDir, target).split(import_node_path2.default.sep).join("/");
7567
+ return relative === peer || relative.startsWith(`${peer}/`) || new RegExp(`^[^/]+/enhancements/${peer}(/|$)`).test(relative);
7568
+ };
7569
+ const tainted = /* @__PURE__ */ new Map();
7570
+ const queue = [];
7571
+ const taint = (absolute, peer) => {
7572
+ const peers = tainted.get(absolute) ?? /* @__PURE__ */ new Set();
7573
+ if (peers.has(peer)) return;
7574
+ peers.add(peer);
7575
+ tainted.set(absolute, peers);
7576
+ queue.push([absolute, peer]);
7577
+ };
7578
+ const importers = /* @__PURE__ */ new Map();
7579
+ for (const [absolute, file] of files) {
7580
+ for (const target of file.targets) {
7581
+ const hit = RESOLVE_SUFFIXES.map((suffix) => target + suffix).find((c) => files.has(c));
7582
+ if (hit) importers.set(hit, [...importers.get(hit) ?? [], absolute]);
7583
+ }
7584
+ for (const peer of peersByModule[file.module] ?? []) {
7585
+ if (exempt(file, peer)) continue;
7586
+ if (file.targets.some((target) => goneWith(target, peer)) || file.dataPaths.some(
7587
+ (p) => p.startsWith(`modules/${peer}/`) || p.includes(`/modules/${peer}/`)
7588
+ )) {
7589
+ taint(absolute, peer);
7590
+ }
7591
+ }
7592
+ }
7593
+ for (let next = queue.shift(); next; next = queue.shift()) {
7594
+ const [target, peer] = next;
7595
+ for (const importer of importers.get(target) ?? []) {
7596
+ const file = files.get(importer);
7597
+ if ((peersByModule[file.module] ?? []).includes(peer) && !exempt(file, peer)) {
7598
+ taint(importer, peer);
7599
+ }
7600
+ }
7601
+ }
7602
+ const violations = [];
7603
+ for (const [absolute, peers] of tainted) {
7604
+ const file = files.get(absolute);
7605
+ for (const peer of peers) {
7606
+ violations.push({ file: `${file.module}/${file.relative}`, module: file.module, peer });
7607
+ }
7608
+ }
7609
+ return violations.sort((a, b) => a.file.localeCompare(b.file) || a.peer.localeCompare(b.peer));
7610
+ }
7611
+ function findOptionalPeerViolations(modulesDir) {
7612
+ if (!import_node_fs2.default.existsSync(modulesDir)) return [];
7613
+ const manifests = {};
7614
+ for (const name of import_node_fs2.default.readdirSync(modulesDir)) {
7615
+ const manifestPath = import_node_path2.default.join(modulesDir, name, "module.json");
7616
+ if (!import_node_fs2.default.existsSync(manifestPath)) continue;
7617
+ manifests[name] = JSON.parse(import_node_fs2.default.readFileSync(manifestPath, "utf8"));
7618
+ }
7619
+ const features = Object.keys(manifests).filter((name) => manifests[name].kind === "feature");
7620
+ const peersByModule = {};
7621
+ for (const [name, manifest] of Object.entries(manifests)) {
7622
+ const uses = new Set(manifest.uses ?? []);
7623
+ peersByModule[name] = features.filter((peer) => peer !== name && !uses.has(peer));
7624
+ }
7625
+ return findModuleBoundaryViolations(modulesDir, peersByModule);
7626
+ }
7627
+ function formatBoundaryViolations(modulesDirLabel, violations) {
7628
+ const files = new Set(violations.map((v) => v.file)).size;
7629
+ return `${files} Module file(s) import a peer Module that may be absent (not in the Module's \`uses\`, or excluded by --enabled).
7630
+ \`next build\` type-checks them after the peer is gone. For each line, move the file into ${modulesDirLabel}/<module>/enhancements/<peer>/ or declare <peer> in the module's module.json \`uses\` (and enable it).
7631
+ ` + violations.map((v) => `${modulesDirLabel}/${v.file}: ${v.peer}`).join("\n");
7632
+ }
7633
+
7451
7634
  // src/index.ts
7452
7635
  var MODULES_DIR2 = MODULES_SANDBOX_DIR;
7453
7636
  function readComposeInputs(cwd) {
7454
- const file = import_node_path2.default.join(cwd, COMPOSE_INPUTS_PATH);
7455
- if (!import_node_fs2.default.existsSync(file)) return { connectedStores: [], installs: {} };
7637
+ const file = import_node_path3.default.join(cwd, COMPOSE_INPUTS_PATH);
7638
+ if (!import_node_fs3.default.existsSync(file)) return { connectedStores: [], installs: {} };
7456
7639
  let raw;
7457
7640
  try {
7458
- raw = JSON.parse(import_node_fs2.default.readFileSync(file, "utf8"));
7641
+ raw = JSON.parse(import_node_fs3.default.readFileSync(file, "utf8"));
7459
7642
  } catch (error) {
7460
7643
  throw new Error(
7461
7644
  `[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
@@ -7497,18 +7680,18 @@ function stubGitignoreEntries(artifactPaths) {
7497
7680
  function writeStubGitignore(cwd, artifactPaths) {
7498
7681
  const entries = stubGitignoreEntries(artifactPaths);
7499
7682
  const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
7500
- const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
7501
- const existing = import_node_fs2.default.existsSync(file) ? import_node_fs2.default.readFileSync(file, "utf8") : null;
7683
+ const file = import_node_path3.default.join(cwd, STUB_GITIGNORE_PATH);
7684
+ const existing = import_node_fs3.default.existsSync(file) ? import_node_fs3.default.readFileSync(file, "utf8") : null;
7502
7685
  if (existing !== content) {
7503
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
7504
- import_node_fs2.default.writeFileSync(file, content);
7686
+ import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(file), { recursive: true });
7687
+ import_node_fs3.default.writeFileSync(file, content);
7505
7688
  }
7506
7689
  return entries.length;
7507
7690
  }
7508
7691
  function assertStubGitignoreOwned(cwd) {
7509
- const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
7510
- if (!import_node_fs2.default.existsSync(file)) return;
7511
- const firstLine = import_node_fs2.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
7692
+ const file = import_node_path3.default.join(cwd, STUB_GITIGNORE_PATH);
7693
+ if (!import_node_fs3.default.existsSync(file)) return;
7694
+ const firstLine = import_node_fs3.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
7512
7695
  if (firstLine !== STUB_GITIGNORE_HEADER) {
7513
7696
  throw new Error(
7514
7697
  `[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
@@ -7516,8 +7699,8 @@ function assertStubGitignoreOwned(cwd) {
7516
7699
  }
7517
7700
  }
7518
7701
  function assertAppRoot(cwd) {
7519
- const appPath = import_node_path2.default.join(cwd, APP_PACKAGE_JSON);
7520
- if (!import_node_fs2.default.existsSync(appPath)) {
7702
+ const appPath = import_node_path3.default.join(cwd, APP_PACKAGE_JSON);
7703
+ if (!import_node_fs3.default.existsSync(appPath)) {
7521
7704
  throw new Error(
7522
7705
  `[compose] ${appPath} not found \u2014 run this from the repo root of a Stardeck app`
7523
7706
  );
@@ -7563,9 +7746,9 @@ ${porcelain.trim()}`
7563
7746
  }
7564
7747
  var APP_SRC_DIR = "apps/web/src";
7565
7748
  function walkSources(dir, skip, out) {
7566
- if (!import_node_fs2.default.existsSync(dir)) return;
7567
- for (const entry of import_node_fs2.default.readdirSync(dir, { withFileTypes: true })) {
7568
- const full = import_node_path2.default.join(dir, entry.name);
7749
+ if (!import_node_fs3.default.existsSync(dir)) return;
7750
+ for (const entry of import_node_fs3.default.readdirSync(dir, { withFileTypes: true })) {
7751
+ const full = import_node_path3.default.join(dir, entry.name);
7569
7752
  if (full === skip) continue;
7570
7753
  if (entry.isDirectory()) walkSources(full, skip, out);
7571
7754
  else if (/\.[cm]?[jt]sx?$/.test(entry.name)) out.push(full);
@@ -7574,13 +7757,13 @@ function walkSources(dir, skip, out) {
7574
7757
  function assertNoAppLayerImporters(cwd, excluded) {
7575
7758
  if (excluded.length === 0) return;
7576
7759
  const files = [];
7577
- walkSources(import_node_path2.default.join(cwd, APP_SRC_DIR), import_node_path2.default.join(cwd, MODULES_DIR2), files);
7760
+ walkSources(import_node_path3.default.join(cwd, APP_SRC_DIR), import_node_path3.default.join(cwd, MODULES_DIR2), files);
7578
7761
  const offenders = [];
7579
7762
  for (const file of files) {
7580
- const source = import_node_fs2.default.readFileSync(file, "utf8");
7763
+ const source = import_node_fs3.default.readFileSync(file, "utf8");
7581
7764
  for (const name of excluded) {
7582
7765
  if (new RegExp(`@/modules/${name}(?=["'\`/])`).test(source)) {
7583
- offenders.push(`${import_node_path2.default.relative(cwd, file)}: ${name}`);
7766
+ offenders.push(`${import_node_path3.default.relative(cwd, file)}: ${name}`);
7584
7767
  }
7585
7768
  }
7586
7769
  }
@@ -7593,8 +7776,8 @@ function assertNoAppLayerImporters(cwd, excluded) {
7593
7776
  }
7594
7777
  }
7595
7778
  function removeDir(absolute) {
7596
- if (!import_node_fs2.default.existsSync(absolute)) return false;
7597
- import_node_fs2.default.rmSync(absolute, { recursive: true, force: true });
7779
+ if (!import_node_fs3.default.existsSync(absolute)) return false;
7780
+ import_node_fs3.default.rmSync(absolute, { recursive: true, force: true });
7598
7781
  return true;
7599
7782
  }
7600
7783
  async function compose(options) {
@@ -7621,6 +7804,16 @@ async function compose(options) {
7621
7804
  dryRun: true
7622
7805
  });
7623
7806
  assertNoAppLayerImporters(options.cwd, preview.excludedModules);
7807
+ const violations = findModuleBoundaryViolations(
7808
+ import_node_path3.default.join(options.cwd, MODULES_DIR2),
7809
+ Object.fromEntries(preview.composedModules.map((name) => [name, preview.excludedModules])),
7810
+ { pruning: true }
7811
+ );
7812
+ if (violations.length > 0) {
7813
+ throw new Error(
7814
+ `[compose] refusing --prune: ${formatBoundaryViolations(MODULES_DIR2, violations)}`
7815
+ );
7816
+ }
7624
7817
  }
7625
7818
  const result = await reconcileCompositionArtifacts(context, {
7626
7819
  enabledModulesRaw: options.enabled,
@@ -7632,11 +7825,11 @@ async function compose(options) {
7632
7825
  let pruned = 0;
7633
7826
  if (options.prune) {
7634
7827
  for (const name of result.excludedModules) {
7635
- if (removeDir(import_node_path2.default.join(options.cwd, MODULES_DIR2, name))) pruned++;
7828
+ if (removeDir(import_node_path3.default.join(options.cwd, MODULES_DIR2, name))) pruned++;
7636
7829
  }
7637
7830
  for (const owner of result.composedModules) {
7638
7831
  for (const peer of result.excludedModules) {
7639
- if (removeDir(import_node_path2.default.join(options.cwd, MODULES_DIR2, owner, "enhancements", peer))) pruned++;
7832
+ if (removeDir(import_node_path3.default.join(options.cwd, MODULES_DIR2, owner, "enhancements", peer))) pruned++;
7640
7833
  }
7641
7834
  }
7642
7835
  }
@@ -7652,15 +7845,28 @@ async function compose(options) {
7652
7845
 
7653
7846
  // src/cli.ts
7654
7847
  async function main() {
7655
- assertSupportedTypescript(import_typescript5.default.versionMajorMinor);
7848
+ assertSupportedTypescript(import_typescript6.default.versionMajorMinor);
7656
7849
  const { values } = (0, import_node_util.parseArgs)({
7657
7850
  options: {
7658
7851
  enabled: { type: "string" },
7659
7852
  prune: { type: "boolean", default: false },
7660
- cwd: { type: "string" }
7853
+ cwd: { type: "string" },
7854
+ "check-boundaries": { type: "boolean", default: false }
7661
7855
  },
7662
7856
  strict: true
7663
7857
  });
7858
+ if (values["check-boundaries"]) {
7859
+ const violations = findOptionalPeerViolations(
7860
+ import_node_path4.default.join(values.cwd ?? process.cwd(), MODULES_SANDBOX_DIR)
7861
+ );
7862
+ if (violations.length > 0) {
7863
+ throw new Error(
7864
+ `[compose] --check-boundaries: ${formatBoundaryViolations(MODULES_SANDBOX_DIR, violations)}`
7865
+ );
7866
+ }
7867
+ console.log("[compose] --check-boundaries: no Module file reaches a peer outside its `uses`");
7868
+ return;
7869
+ }
7664
7870
  const enabled = values.enabled ?? process.env[ENABLED_MODULES_ENV];
7665
7871
  if (values.prune && !enabled) {
7666
7872
  throw new Error(