@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/index.js CHANGED
@@ -2031,12 +2031,15 @@ __export(index_exports, {
2031
2031
  assertSupportedTypescript: () => assertSupportedTypescript,
2032
2032
  compose: () => compose,
2033
2033
  escapeGitignore: () => escapeGitignore,
2034
+ findModuleBoundaryViolations: () => findModuleBoundaryViolations,
2035
+ findOptionalPeerViolations: () => findOptionalPeerViolations,
2036
+ formatBoundaryViolations: () => formatBoundaryViolations,
2034
2037
  readComposeInputs: () => readComposeInputs
2035
2038
  });
2036
2039
  module.exports = __toCommonJS(index_exports);
2037
2040
  var import_node_child_process2 = require("child_process");
2038
- var import_node_fs2 = __toESM(require("fs"));
2039
- var import_node_path2 = __toESM(require("path"));
2041
+ var import_node_fs3 = __toESM(require("fs"));
2042
+ var import_node_path3 = __toESM(require("path"));
2040
2043
 
2041
2044
  // ../../packages/lib/src/server/module-rail/local-fs-sandbox.ts
2042
2045
  var import_node_child_process = require("child_process");
@@ -2141,23 +2144,23 @@ function isValidModuleName(value) {
2141
2144
  var projectSlugSchema = import_zod.z.string().refine((value) => validateSlug(value).valid, "must be a valid project slug");
2142
2145
  var moduleKindSchema = import_zod.z.enum(["feature", "library", "contract"]);
2143
2146
  var endpointMethodSchema = import_zod.z.enum(ENDPOINT_METHODS);
2144
- function endpointPathValidationError(path3) {
2145
- if (!path3.startsWith("/api/")) {
2147
+ function endpointPathValidationError(path4) {
2148
+ if (!path4.startsWith("/api/")) {
2146
2149
  return 'must begin with "/api/"';
2147
2150
  }
2148
- if (path3.endsWith("/")) {
2151
+ if (path4.endsWith("/")) {
2149
2152
  return "must not end with a trailing slash";
2150
2153
  }
2151
- if (path3.includes("?") || path3.includes("#")) {
2154
+ if (path4.includes("?") || path4.includes("#")) {
2152
2155
  return "must not include query or hash";
2153
2156
  }
2154
- if (path3.includes("\\")) {
2157
+ if (path4.includes("\\")) {
2155
2158
  return "must not include backslashes";
2156
2159
  }
2157
- if (path3.includes("%")) {
2160
+ if (path4.includes("%")) {
2158
2161
  return "must not include percent-encoded segments";
2159
2162
  }
2160
- const parts = path3.split("/");
2163
+ const parts = path4.split("/");
2161
2164
  if (parts.length < 3 || parts[0] !== "" || parts[1] !== "api") {
2162
2165
  return 'must begin with "/api/"';
2163
2166
  }
@@ -2207,12 +2210,12 @@ function endpointPathValidationError(path3) {
2207
2210
  }
2208
2211
  return null;
2209
2212
  }
2210
- function normalizeEndpointPathForCollision(path3) {
2211
- const error = endpointPathValidationError(path3);
2213
+ function normalizeEndpointPathForCollision(path4) {
2214
+ const error = endpointPathValidationError(path4);
2212
2215
  if (error != null) {
2213
2216
  throw new Error(`invalid endpoint path: ${error}`);
2214
2217
  }
2215
- return path3.split("/").map((segment, index) => {
2218
+ return path4.split("/").map((segment, index) => {
2216
2219
  if (index < 2) {
2217
2220
  return segment;
2218
2221
  }
@@ -2225,8 +2228,8 @@ function normalizeEndpointPathForCollision(path3) {
2225
2228
  return segment;
2226
2229
  }).join("/");
2227
2230
  }
2228
- var endpointPathSchema = import_zod.z.string().superRefine((path3, ctx) => {
2229
- const error = endpointPathValidationError(path3);
2231
+ var endpointPathSchema = import_zod.z.string().superRefine((path4, ctx) => {
2232
+ const error = endpointPathValidationError(path4);
2230
2233
  if (error != null) {
2231
2234
  ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: error });
2232
2235
  }
@@ -2473,16 +2476,16 @@ var moduleManifestSchema = import_zod.z.object({
2473
2476
  }
2474
2477
  }
2475
2478
  const seenWorkflows = /* @__PURE__ */ new Map();
2476
- const noteWorkflow = (name, path3) => {
2479
+ const noteWorkflow = (name, path4) => {
2477
2480
  const existing = seenWorkflows.get(name);
2478
2481
  if (existing != null) {
2479
2482
  ctx.addIssue({
2480
2483
  code: import_zod.z.ZodIssueCode.custom,
2481
2484
  message: `duplicate workflow name "${name}" (also at ${existing})`,
2482
- path: path3
2485
+ path: path4
2483
2486
  });
2484
2487
  } else {
2485
- seenWorkflows.set(name, path3.join("."));
2488
+ seenWorkflows.set(name, path4.join("."));
2486
2489
  }
2487
2490
  };
2488
2491
  for (let i = 0; i < manifest.workflows.length; i += 1) {
@@ -2494,7 +2497,7 @@ var moduleManifestSchema = import_zod.z.object({
2494
2497
  }
2495
2498
  }
2496
2499
  const seenEndpointKeys = /* @__PURE__ */ new Map();
2497
- const noteEndpoint = (claim, path3) => {
2500
+ const noteEndpoint = (claim, path4) => {
2498
2501
  let key;
2499
2502
  try {
2500
2503
  key = normalizeEndpointPathForCollision(claim.path);
@@ -2506,10 +2509,10 @@ var moduleManifestSchema = import_zod.z.object({
2506
2509
  ctx.addIssue({
2507
2510
  code: import_zod.z.ZodIssueCode.custom,
2508
2511
  message: `duplicate normalized endpoint path "${key}" (also at ${existing})`,
2509
- path: path3
2512
+ path: path4
2510
2513
  });
2511
2514
  } else {
2512
- seenEndpointKeys.set(key, path3.join("."));
2515
+ seenEndpointKeys.set(key, path4.join("."));
2513
2516
  }
2514
2517
  };
2515
2518
  for (let i = 0; i < manifest.endpoints.length; i += 1) {
@@ -2610,8 +2613,8 @@ var moduleManifestSchema = import_zod.z.object({
2610
2613
  }).transform(({ version: _legacyVersion, ...manifest }) => manifest);
2611
2614
  function formatZodErrors(error) {
2612
2615
  return error.issues.map((issue) => {
2613
- const path3 = issue.path.length > 0 ? issue.path.join(".") : "root";
2614
- return `${path3}: ${issue.message}`;
2616
+ const path4 = issue.path.length > 0 ? issue.path.join(".") : "root";
2617
+ return `${path4}: ${issue.message}`;
2615
2618
  });
2616
2619
  }
2617
2620
  function parseModuleManifest(raw) {
@@ -3011,16 +3014,16 @@ var collectedE2eFactsSchema = import_zod3.z.object({
3011
3014
  function validateRouteExportEvidenceCongruence(input) {
3012
3015
  const entrySet = new Set(input.routeEntries);
3013
3016
  const exportPaths = input.routePageExports.map((e) => e.nextRelativePath);
3014
- for (const path3 of exportPaths) {
3015
- if (!entrySet.has(path3)) {
3016
- input.onIssue(`routePageExports path "${path3}" is not listed in routeEntries`);
3017
+ for (const path4 of exportPaths) {
3018
+ if (!entrySet.has(path4)) {
3019
+ input.onIssue(`routePageExports path "${path4}" is not listed in routeEntries`);
3017
3020
  }
3018
3021
  }
3019
3022
  if (input.routeCollectionErrors.length === 0) {
3020
3023
  const exportSet = new Set(exportPaths);
3021
- for (const path3 of input.routeEntries) {
3022
- if (!exportSet.has(path3)) {
3023
- input.onIssue(`routeEntries path "${path3}" is missing from routePageExports`);
3024
+ for (const path4 of input.routeEntries) {
3025
+ if (!exportSet.has(path4)) {
3026
+ input.onIssue(`routeEntries path "${path4}" is missing from routePageExports`);
3024
3027
  }
3025
3028
  }
3026
3029
  }
@@ -4134,6 +4137,7 @@ var MODULE_CONTRIBUTIONS_GEN_PATH = `${APP_WEB_PREFIX}src/module-contributions.g
4134
4137
  var MODULE_DATASTORES_GEN_PATH = `${APP_WEB_PREFIX}src/module-datastores.gen.ts`;
4135
4138
  var MODULE_I18N_GEN_PATH = `${APP_WEB_PREFIX}src/module-i18n.gen.ts`;
4136
4139
  var MODULE_INIT_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-init.server.gen.ts`;
4140
+ var MODULE_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-server.gen.ts`;
4137
4141
  var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
4138
4142
  var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
4139
4143
 
@@ -4150,7 +4154,8 @@ var COMPOSITION_RUNTIME_GEN_PATHS = [
4150
4154
  MODULE_CONTRIBUTIONS_GEN_PATH,
4151
4155
  MODULE_I18N_GEN_PATH,
4152
4156
  MODULE_INIT_SERVER_GEN_PATH,
4153
- MODULE_DATASTORES_GEN_PATH
4157
+ MODULE_DATASTORES_GEN_PATH,
4158
+ MODULE_SERVER_GEN_PATH
4154
4159
  ];
4155
4160
  function compareNames(a, b) {
4156
4161
  return a < b ? -1 : a > b ? 1 : 0;
@@ -4171,6 +4176,9 @@ function formatI18nMarker() {
4171
4176
  function formatInitMarker() {
4172
4177
  return `// ${MODULE_STUB_MARKER} scope=init`;
4173
4178
  }
4179
+ function formatServerMarker() {
4180
+ return `// ${MODULE_STUB_MARKER} scope=server`;
4181
+ }
4174
4182
  function formatDatastoresMarker() {
4175
4183
  return `// ${MODULE_STUB_MARKER} scope=datastores`;
4176
4184
  }
@@ -4266,13 +4274,13 @@ function assertClientSafeConventionalEntryImports(source, pathLabel, kind) {
4266
4274
  function createMessageTree() {
4267
4275
  return /* @__PURE__ */ Object.create(null);
4268
4276
  }
4269
- function assertMessageTree(value, path3) {
4277
+ function assertMessageTree(value, path4) {
4270
4278
  if (typeof value === "string") return;
4271
4279
  if (value === null || Array.isArray(value) || typeof value !== "object") {
4272
- throw new Error(`Invalid message at ${path3}: expected string or nested object`);
4280
+ throw new Error(`Invalid message at ${path4}: expected string or nested object`);
4273
4281
  }
4274
4282
  for (const [key, child] of Object.entries(value)) {
4275
- assertMessageTree(child, `${path3}.${key}`);
4283
+ assertMessageTree(child, `${path4}.${key}`);
4276
4284
  }
4277
4285
  }
4278
4286
  function cloneTree(value) {
@@ -4282,17 +4290,17 @@ function cloneTree(value) {
4282
4290
  }
4283
4291
  return clone;
4284
4292
  }
4285
- function collectLeafPaths(value, path3, out) {
4293
+ function collectLeafPaths(value, path4, out) {
4286
4294
  if (typeof value === "string") {
4287
- out.push(path3);
4295
+ out.push(path4);
4288
4296
  return;
4289
4297
  }
4290
- assertMessageTree(value, path3);
4291
- for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path3}.${key}`, out);
4298
+ assertMessageTree(value, path4);
4299
+ for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path4}.${key}`, out);
4292
4300
  }
4293
- function overrideInto(target, source, owner, path3, missing) {
4301
+ function overrideInto(target, source, owner, path4, missing) {
4294
4302
  for (const [key, value] of Object.entries(source)) {
4295
- const nextPath = path3 ? `${path3}.${key}` : key;
4303
+ const nextPath = path4 ? `${path4}.${key}` : key;
4296
4304
  if (!Object.prototype.hasOwnProperty.call(target, key)) {
4297
4305
  collectLeafPaths(value, nextPath, missing);
4298
4306
  continue;
@@ -4319,9 +4327,9 @@ function overrideInto(target, source, owner, path3, missing) {
4319
4327
  function composeLocaleMessagesForPlan(layers) {
4320
4328
  const result = createMessageTree();
4321
4329
  const leafOwners = /* @__PURE__ */ new Map();
4322
- function mergeInto(target, source, owner, path3) {
4330
+ function mergeInto(target, source, owner, path4) {
4323
4331
  for (const [key, value] of Object.entries(source)) {
4324
- const nextPath = path3 ? `${path3}.${key}` : key;
4332
+ const nextPath = path4 ? `${path4}.${key}` : key;
4325
4333
  const hasExisting = Object.prototype.hasOwnProperty.call(target, key);
4326
4334
  const existing = hasExisting ? target[key] : void 0;
4327
4335
  if (typeof value === "string") {
@@ -4814,6 +4822,42 @@ function emitModuleInitServerGenTs(input) {
4814
4822
  ""
4815
4823
  ].join("\n");
4816
4824
  }
4825
+ function emitModuleServerGenTs(input) {
4826
+ const installed = new Set(input.registry.modules.map((m) => m.name));
4827
+ const moduleNames = input.facts.filter((f) => f.hasServerRegistrations && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames);
4828
+ for (const name of moduleNames) {
4829
+ if (!isValidModuleName(name)) {
4830
+ throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
4831
+ }
4832
+ }
4833
+ const importLines = moduleNames.map(
4834
+ (name, index) => [
4835
+ `@/modules/${name}/server/registrations`,
4836
+ `import * as serverRegistrations${index} from ${JSON.stringify(`@/modules/${name}/server/registrations`)};`
4837
+ ]
4838
+ ).sort((a, b) => compareImportSpecifiers(a[0], b[0])).map(([, line]) => line);
4839
+ const entryLines = moduleNames.map(
4840
+ (name, index) => ` { module: ${JSON.stringify(name)}, registrations: serverRegistrations${index} },`
4841
+ );
4842
+ return [
4843
+ formatServerMarker(),
4844
+ "/** DO NOT EDIT \u2014 owned by the module install rail. */",
4845
+ "",
4846
+ ...importLines,
4847
+ ...importLines.length > 0 ? [""] : [],
4848
+ "export type ModuleServerRegistration = {",
4849
+ " module: string;",
4850
+ " registrations: Readonly<Record<string, unknown>>;",
4851
+ "};",
4852
+ "",
4853
+ ...entryLines.length === 0 ? ["export const MODULE_SERVER_REGISTRATIONS: readonly ModuleServerRegistration[] = [];"] : [
4854
+ "export const MODULE_SERVER_REGISTRATIONS: readonly ModuleServerRegistration[] = [",
4855
+ ...entryLines,
4856
+ "];"
4857
+ ],
4858
+ ""
4859
+ ].join("\n");
4860
+ }
4817
4861
 
4818
4862
  // ../../packages/lib/src/server/module-rail/composition-artifacts.ts
4819
4863
  var COMPOSITION_ARTIFACTS_GENERATED_BY = "module-rail";
@@ -4829,8 +4873,8 @@ var METHOD_ORDER = {
4829
4873
  function compareNames2(a, b) {
4830
4874
  return a < b ? -1 : a > b ? 1 : 0;
4831
4875
  }
4832
- function endpointPairKey(method, path3) {
4833
- return `${method} ${path3}`;
4876
+ function endpointPairKey(method, path4) {
4877
+ return `${method} ${path4}`;
4834
4878
  }
4835
4879
  function apiPathToRouteFile(apiPath) {
4836
4880
  if (!apiPath.startsWith("/api/")) {
@@ -4844,11 +4888,11 @@ function routeEntryToAppPageFile(nextRelativePath) {
4844
4888
  }
4845
4889
  var APP_ROUTE_FILE_PREFIX = `${APP_PACKAGE_DIR}/src/app/`;
4846
4890
  var API_ROUTE_FILE_SUFFIXES = ["/route.ts", "/route.tsx", "/route.jsx", "/route.js"];
4847
- function appRouteFileToNextRelativePath(path3) {
4848
- const pagePath = appPageFileToNextRelativePath(path3, APP_PACKAGE_DIR);
4891
+ function appRouteFileToNextRelativePath(path4) {
4892
+ const pagePath = appPageFileToNextRelativePath(path4, APP_PACKAGE_DIR);
4849
4893
  if (pagePath != null) return pagePath;
4850
- if (!path3.startsWith(APP_ROUTE_FILE_PREFIX)) return null;
4851
- const relative = path3.slice(APP_ROUTE_FILE_PREFIX.length);
4894
+ if (!path4.startsWith(APP_ROUTE_FILE_PREFIX)) return null;
4895
+ const relative = path4.slice(APP_ROUTE_FILE_PREFIX.length);
4852
4896
  if (!relative.startsWith("api/")) return null;
4853
4897
  const suffix = API_ROUTE_FILE_SUFFIXES.find((candidate) => relative.endsWith(candidate));
4854
4898
  if (!suffix) return null;
@@ -4876,6 +4920,14 @@ function formatRouteMarker(owner, nextRelativePath) {
4876
4920
  function formatRegistryMarker() {
4877
4921
  return `// ${MODULE_STUB_MARKER} scope=registry`;
4878
4922
  }
4923
+ var OWNERLESS_MARKER_KINDS = [
4924
+ "registry",
4925
+ "contributions",
4926
+ "datastores",
4927
+ "i18n",
4928
+ "init",
4929
+ "server"
4930
+ ];
4879
4931
  function parseCompositionArtifactMarker(source) {
4880
4932
  const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
4881
4933
  const match = firstLine.match(
@@ -4886,26 +4938,8 @@ function parseCompositionArtifactMarker(source) {
4886
4938
  if (!match) return null;
4887
4939
  const owner = match[1];
4888
4940
  const scope = match[2];
4889
- if (scope === "registry") {
4890
- if (owner) return null;
4891
- return { kind: "registry" };
4892
- }
4893
- if (scope === "contributions") {
4894
- if (owner) return null;
4895
- return { kind: "contributions" };
4896
- }
4897
- if (scope === "datastores") {
4898
- if (owner) return null;
4899
- return { kind: "datastores" };
4900
- }
4901
- if (scope === "i18n") {
4902
- if (owner) return null;
4903
- return { kind: "i18n" };
4904
- }
4905
- if (scope === "init") {
4906
- if (owner) return null;
4907
- return { kind: "init" };
4908
- }
4941
+ const ownerless = OWNERLESS_MARKER_KINDS.find((kind) => kind === scope);
4942
+ if (ownerless) return owner ? null : { kind: ownerless };
4909
4943
  if (!owner || !isValidModuleName(owner)) return null;
4910
4944
  if (scope === "route") {
4911
4945
  const nextRelativePath = match[3]?.trim();
@@ -4950,9 +4984,9 @@ function legacyHandlerPropertyMatchesPath(property, method, physicalPath, bindin
4950
4984
  const expectedTokens = knownAliases[`${binding} ${method} ${physicalPath}`] ?? pathTokens;
4951
4985
  return handlerTokens.length === expectedTokens.length && handlerTokens.every((token, index) => token === expectedTokens[index]);
4952
4986
  }
4953
- function parseLegacyApiAdapter(path3, source) {
4954
- const physicalPath = appRouteFileToNextRelativePath(path3);
4955
- if (expectedMarkerKindForPath(path3) !== "endpoint" || physicalPath == null) return null;
4987
+ function parseLegacyApiAdapter(path4, source) {
4988
+ const physicalPath = appRouteFileToNextRelativePath(path4);
4989
+ if (expectedMarkerKindForPath(path4) !== "endpoint" || physicalPath == null) return null;
4956
4990
  const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
4957
4991
  const match = firstLine.match(
4958
4992
  new RegExp(
@@ -4962,7 +4996,7 @@ function parseLegacyApiAdapter(path3, source) {
4962
4996
  if (!match || match[2] !== physicalPath) return null;
4963
4997
  const owner = match[1];
4964
4998
  if (!isValidModuleName(owner)) return null;
4965
- const file = import_typescript4.default.createSourceFile(path3, source, import_typescript4.default.ScriptTarget.Latest, false, import_typescript4.default.ScriptKind.TS);
4999
+ const file = import_typescript4.default.createSourceFile(path4, source, import_typescript4.default.ScriptTarget.Latest, false, import_typescript4.default.ScriptKind.TS);
4966
5000
  if (file.parseDiagnostics.length > 0) {
4967
5001
  return null;
4968
5002
  }
@@ -5004,9 +5038,9 @@ function parseLegacyApiAdapter(path3, source) {
5004
5038
  }
5005
5039
  return handlerBinding != null && methods.size > 0 ? { owner, methods: [...methods] } : null;
5006
5040
  }
5007
- function parseLegacyRouteAdapter(path3, source) {
5008
- const physicalPath = appRouteFileToNextRelativePath(path3);
5009
- if (expectedMarkerKindForPath(path3) !== "route" || physicalPath == null) return null;
5041
+ function parseLegacyRouteAdapter(path4, source) {
5042
+ const physicalPath = appRouteFileToNextRelativePath(path4);
5043
+ if (expectedMarkerKindForPath(path4) !== "route" || physicalPath == null) return null;
5010
5044
  const normalized = source.replace(/\r\n/g, "\n");
5011
5045
  const match = normalized.match(
5012
5046
  new RegExp(
@@ -5018,29 +5052,30 @@ function parseLegacyRouteAdapter(path3, source) {
5018
5052
  const nextRelativePath = match[2];
5019
5053
  return isValidModuleName(owner) && nextRelativePath === physicalPath ? { owner, nextRelativePath } : null;
5020
5054
  }
5021
- function legacyRouteAdapterMatchesStub(path3, source, stub) {
5022
- const legacy = parseLegacyRouteAdapter(path3, source);
5055
+ function legacyRouteAdapterMatchesStub(path4, source, stub) {
5056
+ const legacy = parseLegacyRouteAdapter(path4, source);
5023
5057
  return legacy != null && legacy.owner === stub.owner;
5024
5058
  }
5025
5059
  var API_ROUTE_DIR_PREFIX = `${APP_PACKAGE_DIR}/src/app/api/`;
5026
5060
  var APP_DIR_PREFIX = `${APP_PACKAGE_DIR}/src/app/`;
5027
5061
  var PAGE_FILE_SUFFIXES = ["/page.tsx", "/page.ts", "/page.jsx", "/page.js"];
5028
- function expectedMarkerKindForPath(path3) {
5029
- if (path3 === MODULES_GEN_PATH) return "registry";
5030
- if (path3 === MODULE_CONTRIBUTIONS_GEN_PATH) return "contributions";
5031
- if (path3 === MODULE_DATASTORES_GEN_PATH) return "datastores";
5032
- if (path3 === MODULE_I18N_GEN_PATH) return "i18n";
5033
- if (path3 === MODULE_INIT_SERVER_GEN_PATH) return "init";
5034
- if (path3.startsWith(API_ROUTE_DIR_PREFIX) && path3.endsWith("/route.ts")) return "endpoint";
5035
- if (path3.startsWith(APP_DIR_PREFIX) && !path3.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path3.endsWith(suffix))) {
5062
+ function expectedMarkerKindForPath(path4) {
5063
+ if (path4 === MODULES_GEN_PATH) return "registry";
5064
+ if (path4 === MODULE_CONTRIBUTIONS_GEN_PATH) return "contributions";
5065
+ if (path4 === MODULE_DATASTORES_GEN_PATH) return "datastores";
5066
+ if (path4 === MODULE_I18N_GEN_PATH) return "i18n";
5067
+ if (path4 === MODULE_INIT_SERVER_GEN_PATH) return "init";
5068
+ if (path4 === MODULE_SERVER_GEN_PATH) return "server";
5069
+ if (path4.startsWith(API_ROUTE_DIR_PREFIX) && path4.endsWith("/route.ts")) return "endpoint";
5070
+ if (path4.startsWith(APP_DIR_PREFIX) && !path4.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path4.endsWith(suffix))) {
5036
5071
  return "route";
5037
5072
  }
5038
5073
  return null;
5039
5074
  }
5040
- function markerCongruentWithPath(path3, marker) {
5041
- if (expectedMarkerKindForPath(path3) !== marker.kind) return false;
5075
+ function markerCongruentWithPath(path4, marker) {
5076
+ if (expectedMarkerKindForPath(path4) !== marker.kind) return false;
5042
5077
  if (marker.kind === "route") {
5043
- const derived = appPageFileToNextRelativePath(path3, APP_PACKAGE_DIR);
5078
+ const derived = appPageFileToNextRelativePath(path4, APP_PACKAGE_DIR);
5044
5079
  return derived != null && derived === marker.nextRelativePath;
5045
5080
  }
5046
5081
  return true;
@@ -5311,8 +5346,8 @@ function buildGeneratedModuleRegistry(input) {
5311
5346
  function activeEnhancementPeers(registryModule) {
5312
5347
  return new Set(registryModule.enhancements.filter((e) => e.active).map((e) => e.peer));
5313
5348
  }
5314
- function endpointBucketKey(path3, owner, scope) {
5315
- return `${path3}\0${owner}\0${scopeLabel(scope)}`;
5349
+ function endpointBucketKey(path4, owner, scope) {
5350
+ return `${path4}\0${owner}\0${scopeLabel(scope)}`;
5316
5351
  }
5317
5352
  function planRouteStubs(input) {
5318
5353
  const endpointBuckets = /* @__PURE__ */ new Map();
@@ -5436,7 +5471,7 @@ function planRouteStubs(input) {
5436
5471
  for (const entry of entries) {
5437
5472
  const nextRelativePath = entry.nextRelativePath;
5438
5473
  const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
5439
- const path3 = routeEntryToAppPageFile(nextRelativePath);
5474
+ const path4 = routeEntryToAppPageFile(nextRelativePath);
5440
5475
  const existing = routeDestinationOwners.get(destination);
5441
5476
  if (existing) {
5442
5477
  throw new Error(
@@ -5446,13 +5481,13 @@ function planRouteStubs(input) {
5446
5481
  routeDestinationOwners.set(destination, {
5447
5482
  owner: regMod.name,
5448
5483
  nextRelativePath,
5449
- path: path3
5484
+ path: path4
5450
5485
  });
5451
5486
  const importModule = routeEntryImport(regMod.name, nextRelativePath);
5452
5487
  const analyzed = analyzeRouteEntryPath(nextRelativePath);
5453
5488
  const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
5454
5489
  routeStubs.push({
5455
- path: path3,
5490
+ path: path4,
5456
5491
  owner: regMod.name,
5457
5492
  nextRelativePath,
5458
5493
  importModule,
@@ -5513,16 +5548,16 @@ function planCompositionArtifacts(input) {
5513
5548
  ...stubs.routeStubs.map((stub) => stub.path)
5514
5549
  ]);
5515
5550
  const existingGeneratedPathSet = new Set(input.existingGeneratedPaths);
5516
- const isProvenStaleDeletionCandidate = (path3, content) => {
5517
- if (path3 === MODULES_GEN_PATH) return false;
5518
- if (COMPOSITION_RUNTIME_GEN_PATHS.includes(path3)) return false;
5519
- if (desiredPaths.has(path3)) return false;
5520
- if (!existingGeneratedPathSet.has(path3)) return false;
5521
- const legacy = parseLegacyApiAdapter(path3, content);
5551
+ const isProvenStaleDeletionCandidate = (path4, content) => {
5552
+ if (path4 === MODULES_GEN_PATH) return false;
5553
+ if (COMPOSITION_RUNTIME_GEN_PATHS.includes(path4)) return false;
5554
+ if (desiredPaths.has(path4)) return false;
5555
+ if (!existingGeneratedPathSet.has(path4)) return false;
5556
+ const legacy = parseLegacyApiAdapter(path4, content);
5522
5557
  if (legacy) return true;
5523
- if (parseLegacyRouteAdapter(path3, content)) return true;
5558
+ if (parseLegacyRouteAdapter(path4, content)) return true;
5524
5559
  const marker = parseCompositionArtifactMarker(content);
5525
- return marker != null && markerCongruentWithPath(path3, marker);
5560
+ return marker != null && markerCongruentWithPath(path4, marker);
5526
5561
  };
5527
5562
  const plannedByDestination = /* @__PURE__ */ new Map();
5528
5563
  for (const stub of stubs.routeStubs) {
@@ -5630,6 +5665,7 @@ function planCompositionArtifacts(input) {
5630
5665
  let datastoresContent;
5631
5666
  let i18nContent;
5632
5667
  let initContent;
5668
+ let serverContent;
5633
5669
  try {
5634
5670
  contributionsContent = emitModuleContributionsGenTs({
5635
5671
  registry: input.registry,
@@ -5654,6 +5690,10 @@ function planCompositionArtifacts(input) {
5654
5690
  registry: input.registry,
5655
5691
  facts: compositionEntryFacts
5656
5692
  });
5693
+ serverContent = emitModuleServerGenTs({
5694
+ registry: input.registry,
5695
+ facts: compositionEntryFacts
5696
+ });
5657
5697
  } catch (error) {
5658
5698
  return {
5659
5699
  ok: false,
@@ -5666,17 +5706,18 @@ function planCompositionArtifacts(input) {
5666
5706
  desired.set(MODULE_DATASTORES_GEN_PATH, datastoresContent);
5667
5707
  desired.set(MODULE_I18N_GEN_PATH, i18nContent);
5668
5708
  desired.set(MODULE_INIT_SERVER_GEN_PATH, initContent);
5709
+ desired.set(MODULE_SERVER_GEN_PATH, serverContent);
5669
5710
  for (const stub of stubs.endpointStubs) desired.set(stub.path, stub.content);
5670
5711
  for (const stub of stubs.routeStubs) desired.set(stub.path, stub.content);
5671
5712
  const endpointStubByPath = new Map(stubs.endpointStubs.map((stub) => [stub.path, stub]));
5672
5713
  const routeStubByPath = new Map(stubs.routeStubs.map((stub) => [stub.path, stub]));
5673
- for (const [path3] of desired) {
5674
- const existing = input.existingFiles[path3];
5714
+ for (const [path4] of desired) {
5715
+ const existing = input.existingFiles[path4];
5675
5716
  if (existing == null) continue;
5676
5717
  let marker = parseCompositionArtifactMarker(existing);
5677
- const plannedEndpoint = endpointStubByPath.get(path3);
5678
- const plannedRoute = routeStubByPath.get(path3);
5679
- const legacy = parseLegacyApiAdapter(path3, existing);
5718
+ const plannedEndpoint = endpointStubByPath.get(path4);
5719
+ const plannedRoute = routeStubByPath.get(path4);
5720
+ const legacy = parseLegacyApiAdapter(path4, existing);
5680
5721
  if (legacy && plannedEndpoint) {
5681
5722
  marker = {
5682
5723
  kind: "endpoint",
@@ -5684,66 +5725,66 @@ function planCompositionArtifacts(input) {
5684
5725
  scope: plannedEndpoint.scope
5685
5726
  };
5686
5727
  }
5687
- if (!marker && plannedRoute && legacyRouteAdapterMatchesStub(path3, existing, plannedRoute)) {
5728
+ if (!marker && plannedRoute && legacyRouteAdapterMatchesStub(path4, existing, plannedRoute)) {
5688
5729
  marker = {
5689
5730
  kind: "route",
5690
5731
  owner: plannedRoute.owner,
5691
5732
  nextRelativePath: plannedRoute.nextRelativePath
5692
5733
  };
5693
5734
  }
5694
- const isGenRuntimePath = COMPOSITION_RUNTIME_GEN_PATHS.includes(path3);
5735
+ const isGenRuntimePath = COMPOSITION_RUNTIME_GEN_PATHS.includes(path4);
5695
5736
  if (!marker) {
5696
5737
  return {
5697
5738
  ok: false,
5698
- 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}`,
5699
- path: path3
5739
+ error: path4 === MODULES_GEN_PATH ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged registry at ${path4}` : isGenRuntimePath ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged generated file at ${path4}` : `[ModuleRail] composition artifacts: refusing to overwrite unmanaged route at ${path4}`,
5740
+ path: path4
5700
5741
  };
5701
5742
  }
5702
- const expectedKind = expectedMarkerKindForPath(path3);
5743
+ const expectedKind = expectedMarkerKindForPath(path4);
5703
5744
  if (marker.kind !== expectedKind) {
5704
5745
  return {
5705
5746
  ok: false,
5706
- 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)`,
5707
- path: path3
5747
+ error: path4 === MODULES_GEN_PATH ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged registry at ${path4} (incongruent marker ownership: found ${marker.kind} marker)` : isGenRuntimePath ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged generated file at ${path4} (incongruent marker ownership: found ${marker.kind} marker)` : `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path4} (expected ${expectedKind ?? "no"} marker, found ${marker.kind} marker)`,
5748
+ path: path4
5708
5749
  };
5709
5750
  }
5710
5751
  if (marker.kind === "endpoint") {
5711
- const stub = endpointStubByPath.get(path3);
5752
+ const stub = endpointStubByPath.get(path4);
5712
5753
  const ownerMatches = stub != null && marker.owner === stub.owner;
5713
5754
  const scopeMatches = stub != null && scopeLabel(marker.scope) === scopeLabel(stub.scope);
5714
5755
  if (!ownerMatches || !scopeMatches) {
5715
5756
  return {
5716
5757
  ok: false,
5717
- 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) : "?"})`,
5718
- path: path3,
5758
+ error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path4} (existing owner=${marker.owner} scope=${scopeLabel(marker.scope)} vs planned owner=${stub?.owner ?? "?"} scope=${stub ? scopeLabel(stub.scope) : "?"})`,
5759
+ path: path4,
5719
5760
  owner: stub?.owner
5720
5761
  };
5721
5762
  }
5722
5763
  } else if (marker.kind === "route") {
5723
- const stub = routeStubByPath.get(path3);
5764
+ const stub = routeStubByPath.get(path4);
5724
5765
  const ownerMatches = stub != null && marker.owner === stub.owner;
5725
5766
  const pathMatches = stub != null && marker.nextRelativePath === stub.nextRelativePath;
5726
5767
  if (!ownerMatches || !pathMatches) {
5727
5768
  return {
5728
5769
  ok: false,
5729
- 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 ?? "?"})`,
5730
- path: path3,
5770
+ error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path4} (existing owner=${marker.owner} path=${marker.nextRelativePath} vs planned owner=${stub?.owner ?? "?"} path=${stub?.nextRelativePath ?? "?"})`,
5771
+ path: path4,
5731
5772
  owner: stub?.owner
5732
5773
  };
5733
5774
  }
5734
5775
  }
5735
5776
  }
5736
5777
  const deletes = [];
5737
- for (const path3 of input.existingGeneratedPaths) {
5738
- const existing = input.existingFiles[path3];
5778
+ for (const path4 of input.existingGeneratedPaths) {
5779
+ const existing = input.existingFiles[path4];
5739
5780
  if (existing == null) continue;
5740
- if (!isProvenStaleDeletionCandidate(path3, existing)) {
5781
+ if (!isProvenStaleDeletionCandidate(path4, existing)) {
5741
5782
  continue;
5742
5783
  }
5743
- deletes.push(path3);
5784
+ deletes.push(path4);
5744
5785
  }
5745
5786
  deletes.sort(compareNames2);
5746
- 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));
5787
+ const writes = [...desired.entries()].filter(([path4, content]) => input.existingFiles[path4] !== content).map(([path4, content]) => ({ path: path4, content })).sort((a, b) => compareNames2(a.path, b.path));
5747
5788
  return {
5748
5789
  ok: true,
5749
5790
  plan: {
@@ -5772,8 +5813,16 @@ async function checkoutParsesTypedStores(sandbox) {
5772
5813
  try {
5773
5814
  if (!await sandbox.fileExists(APP_PACKAGE_JSON_PATH)) return false;
5774
5815
  const pkg = JSON.parse(await sandbox.readFile(APP_PACKAGE_JSON_PATH));
5775
- const range = pkg.devDependencies?.[COMPOSE_PACKAGE] ?? pkg.dependencies?.[COMPOSE_PACKAGE];
5776
- if (!range) return false;
5816
+ return composeRangeParsesTypedStores(
5817
+ pkg.devDependencies?.[COMPOSE_PACKAGE] ?? pkg.dependencies?.[COMPOSE_PACKAGE]
5818
+ );
5819
+ } catch {
5820
+ return false;
5821
+ }
5822
+ }
5823
+ function composeRangeParsesTypedStores(range) {
5824
+ if (!range) return false;
5825
+ try {
5777
5826
  const lowest = import_semver.default.minVersion(range);
5778
5827
  return lowest != null && import_semver.default.gte(lowest, TYPED_STORES_MIN_COMPOSE_VERSION);
5779
5828
  } catch {
@@ -6218,11 +6267,11 @@ function countedCompositionContext(context) {
6218
6267
  counts.sandboxExecs += 1;
6219
6268
  return context.sandbox.exec(...args);
6220
6269
  },
6221
- readFile: (path3) => {
6270
+ readFile: (path4) => {
6222
6271
  counts.sandboxReads += 1;
6223
- return context.sandbox.readFile(path3);
6272
+ return context.sandbox.readFile(path4);
6224
6273
  },
6225
- fileExists: (path3) => context.sandbox.fileExists(path3),
6274
+ fileExists: (path4) => context.sandbox.fileExists(path4),
6226
6275
  fetchRemoteRef: (...args) => context.sandbox.fetchRemoteRef(...args),
6227
6276
  addDependencies: (specs) => context.sandbox.addDependencies(specs)
6228
6277
  }
@@ -6434,14 +6483,14 @@ async function discoverModuleRouteEntries(context, moduleName) {
6434
6483
  }
6435
6484
  return pages.sort((a, b) => a.nextRelativePath.localeCompare(b.nextRelativePath));
6436
6485
  }
6437
- async function writeSandboxFileAtomic(target, path3, content) {
6438
- const dir = dirnamePosix(path3);
6439
- const tmp = `${path3}.tmp.${target.runId}`;
6486
+ async function writeSandboxFileAtomic(target, path4, content) {
6487
+ const dir = dirnamePosix(path4);
6488
+ const tmp = `${path4}.tmp.${target.runId}`;
6440
6489
  const b64 = Buffer.from(content, "utf8").toString("base64");
6441
6490
  const cmd = [
6442
6491
  `mkdir -p ${shellQuote(dir)}`,
6443
6492
  `printf '%s' ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)}`,
6444
- `mv -f ${shellQuote(tmp)} ${shellQuote(path3)}`
6493
+ `mv -f ${shellQuote(tmp)} ${shellQuote(path4)}`
6445
6494
  ].join(" && ");
6446
6495
  const result = await target.sandbox.exec(cmd, { raiseOnError: false });
6447
6496
  if (result.exitCode !== 0) {
@@ -6456,14 +6505,14 @@ async function writeSandboxFileAtomic(target, path3, content) {
6456
6505
  }
6457
6506
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
6458
6507
  throw new Error(
6459
- `[ModuleRail] composition artifacts: failed to write ${redactSecrets(path3)}: ${redactSecrets(detail)}`
6508
+ `[ModuleRail] composition artifacts: failed to write ${redactSecrets(path4)}: ${redactSecrets(detail)}`
6460
6509
  );
6461
6510
  }
6462
6511
  }
6463
6512
  async function collectAbsentWriteParentDirs(context, writePaths) {
6464
6513
  const absent = /* @__PURE__ */ new Set();
6465
- for (const path3 of writePaths) {
6466
- let dir = dirnamePosix(path3);
6514
+ for (const path4 of writePaths) {
6515
+ let dir = dirnamePosix(path4);
6467
6516
  while (dir !== "." && dir !== "") {
6468
6517
  if (absent.has(dir)) {
6469
6518
  dir = dirnamePosix(dir);
@@ -6500,12 +6549,12 @@ async function removeAbsentWriteParentDirsOnRollback(context, dirsDeepestFirst)
6500
6549
  );
6501
6550
  }
6502
6551
  }
6503
- async function deleteSandboxFile(context, path3) {
6504
- const result = await context.sandbox.exec(`rm -f ${shellQuote(path3)}`, { raiseOnError: false });
6552
+ async function deleteSandboxFile(context, path4) {
6553
+ const result = await context.sandbox.exec(`rm -f ${shellQuote(path4)}`, { raiseOnError: false });
6505
6554
  if (result.exitCode !== 0) {
6506
6555
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
6507
6556
  throw new Error(
6508
- `[ModuleRail] composition artifacts: failed to delete ${redactSecrets(path3)}: ${redactSecrets(detail)}`
6557
+ `[ModuleRail] composition artifacts: failed to delete ${redactSecrets(path4)}: ${redactSecrets(detail)}`
6509
6558
  );
6510
6559
  }
6511
6560
  }
@@ -6572,37 +6621,37 @@ async function applyCompositionArtifactMutations(context, plan, existingFiles) {
6572
6621
  await writeSandboxFileAtomic(context, write.path, write.content);
6573
6622
  appliedWrites.push({ path: write.path, previous });
6574
6623
  }
6575
- for (const path3 of plan.deletes) {
6576
- const previous = existingFiles[path3];
6624
+ for (const path4 of plan.deletes) {
6625
+ const previous = existingFiles[path4];
6577
6626
  if (previous == null) continue;
6578
- await deleteSandboxFile(context, path3);
6579
- appliedDeletes.push({ path: path3, previous });
6627
+ await deleteSandboxFile(context, path4);
6628
+ appliedDeletes.push({ path: path4, previous });
6580
6629
  }
6581
6630
  } catch (error) {
6582
6631
  console.log(
6583
6632
  `[ModuleRail] runId=${context.runId} composition artifacts mid-apply failure; rolling back writes=${appliedWrites.length} deletes=${appliedDeletes.length} newDirs=${newlyCreatedParentDirs.length}`
6584
6633
  );
6585
- for (const { path: path3, previous } of [...appliedDeletes].reverse()) {
6634
+ for (const { path: path4, previous } of [...appliedDeletes].reverse()) {
6586
6635
  try {
6587
- await writeSandboxFileAtomic(context, path3, previous);
6636
+ await writeSandboxFileAtomic(context, path4, previous);
6588
6637
  } catch (rollbackError) {
6589
6638
  console.log(
6590
- `[ModuleRail] runId=${context.runId} composition artifacts rollback restore failed for ${redactSecrets(path3)}: ${redactSecrets(
6639
+ `[ModuleRail] runId=${context.runId} composition artifacts rollback restore failed for ${redactSecrets(path4)}: ${redactSecrets(
6591
6640
  rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
6592
6641
  )}`
6593
6642
  );
6594
6643
  }
6595
6644
  }
6596
- for (const { path: path3, previous } of [...appliedWrites].reverse()) {
6645
+ for (const { path: path4, previous } of [...appliedWrites].reverse()) {
6597
6646
  try {
6598
6647
  if (previous == null) {
6599
- await deleteSandboxFile(context, path3);
6648
+ await deleteSandboxFile(context, path4);
6600
6649
  } else {
6601
- await writeSandboxFileAtomic(context, path3, previous);
6650
+ await writeSandboxFileAtomic(context, path4, previous);
6602
6651
  }
6603
6652
  } catch (rollbackError) {
6604
6653
  console.log(
6605
- `[ModuleRail] runId=${context.runId} composition artifacts rollback write failed for ${redactSecrets(path3)}: ${redactSecrets(
6654
+ `[ModuleRail] runId=${context.runId} composition artifacts rollback write failed for ${redactSecrets(path4)}: ${redactSecrets(
6606
6655
  rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
6607
6656
  )}`
6608
6657
  );
@@ -6636,39 +6685,39 @@ async function listExistingGeneratedStubPaths(context) {
6636
6685
  COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN
6637
6686
  );
6638
6687
  const presentPaths = [];
6639
- for (const path3 of candidates) {
6688
+ for (const path4 of candidates) {
6640
6689
  const presence = assertRealCompositionArtifactFilePresence(
6641
- path3,
6642
- kinds.get(path3) ?? "unreadable"
6690
+ path4,
6691
+ kinds.get(path4) ?? "unreadable"
6643
6692
  );
6644
- if (presence === "file") presentPaths.push(path3);
6693
+ if (presence === "file") presentPaths.push(path4);
6645
6694
  }
6646
6695
  let contents;
6647
6696
  try {
6648
6697
  contents = await readFilesMany(context, presentPaths);
6649
6698
  } catch (error) {
6650
- const path3 = readFailurePath(error);
6699
+ const path4 = readFailurePath(error);
6651
6700
  throw new Error(
6652
- `[ModuleRail] composition artifacts: cannot read candidate stub ${redactSecrets(path3)}: ${redactSecrets(
6701
+ `[ModuleRail] composition artifacts: cannot read candidate stub ${redactSecrets(path4)}: ${redactSecrets(
6653
6702
  error instanceof Error ? error.message : String(error)
6654
6703
  )}`
6655
6704
  );
6656
6705
  }
6657
6706
  const owned = [];
6658
- for (const path3 of presentPaths) {
6659
- const content = contents.get(path3);
6707
+ for (const path4 of presentPaths) {
6708
+ const content = contents.get(path4);
6660
6709
  const marker = parseCompositionArtifactMarker(content);
6661
- if (parseLegacyApiAdapter(path3, content) || parseLegacyRouteAdapter(path3, content)) {
6662
- owned.push(path3);
6710
+ if (parseLegacyApiAdapter(path4, content) || parseLegacyRouteAdapter(path4, content)) {
6711
+ owned.push(path4);
6663
6712
  continue;
6664
6713
  }
6665
6714
  if (marker == null) continue;
6666
- if (!markerCongruentWithPath(path3, marker)) {
6715
+ if (!markerCongruentWithPath(path4, marker)) {
6667
6716
  throw new Error(
6668
- `[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)`
6717
+ `[ModuleRail] composition artifacts: incongruent marker at ${redactSecrets(path4)} (found ${marker.kind} marker on a path shaped for ${expectedMarkerKindForPath(path4) ?? "no"} artifacts; refusing to reconcile a swapped marker)`
6669
6718
  );
6670
6719
  }
6671
- owned.push(path3);
6720
+ owned.push(path4);
6672
6721
  }
6673
6722
  return owned;
6674
6723
  }
@@ -6801,7 +6850,7 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6801
6850
  { raiseOnError: false }
6802
6851
  );
6803
6852
  if (result.exitCode !== 0) {
6804
- for (const path3 of chunk) kinds.set(path3, "unreadable");
6853
+ for (const path4 of chunk) kinds.set(path4, "unreadable");
6805
6854
  continue;
6806
6855
  }
6807
6856
  const lines = result.output.split("\n").filter(Boolean);
@@ -6812,31 +6861,31 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6812
6861
  }
6813
6862
  } else {
6814
6863
  for (const line of lines) {
6815
- const [rawKind, path3] = line.split(" ", 2);
6864
+ const [rawKind, path4] = line.split(" ", 2);
6816
6865
  const kind = rawKind?.replace(/^entry-kind:/, "");
6817
- if (path3 && chunk.includes(path3) && CONVENTIONAL_ENTRY_PRESENCE_KINDS.includes(kind ?? "")) {
6818
- kinds.set(path3, kind);
6866
+ if (path4 && chunk.includes(path4) && CONVENTIONAL_ENTRY_PRESENCE_KINDS.includes(kind ?? "")) {
6867
+ kinds.set(path4, kind);
6819
6868
  }
6820
6869
  }
6821
6870
  }
6822
- for (const path3 of chunk) {
6823
- if (!kinds.has(path3)) kinds.set(path3, "unreadable");
6871
+ for (const path4 of chunk) {
6872
+ if (!kinds.has(path4)) kinds.set(path4, "unreadable");
6824
6873
  }
6825
6874
  }
6826
6875
  return kinds;
6827
6876
  }
6828
- async function classifySandboxPathPresenceKind(context, path3, probeToken) {
6829
- return (await classifySandboxPathPresenceKindMany(context, [path3], probeToken)).get(path3);
6877
+ async function classifySandboxPathPresenceKind(context, path4, probeToken) {
6878
+ return (await classifySandboxPathPresenceKindMany(context, [path4], probeToken)).get(path4);
6830
6879
  }
6831
6880
  var LOCALE_JSON_KIND_PROBE_TOKEN = "locale-json-kind";
6832
- function assertRealLocaleJsonFilePresence(path3, presence) {
6881
+ function assertRealLocaleJsonFilePresence(path4, presence) {
6833
6882
  if (presence === "file") return;
6834
6883
  if (presence === "absent") {
6835
- throw new Error(`[ModuleRail] composition artifacts: locale file missing at ${path3}`);
6884
+ throw new Error(`[ModuleRail] composition artifacts: locale file missing at ${path4}`);
6836
6885
  }
6837
- const presenceError = formatConventionalEntryPresenceError(path3, presence);
6886
+ const presenceError = formatConventionalEntryPresenceError(path4, presence);
6838
6887
  throw new Error(
6839
- `[ModuleRail] composition artifacts: ${presenceError ?? `${path3} is not a real locale JSON file`}`
6888
+ `[ModuleRail] composition artifacts: ${presenceError ?? `${path4} is not a real locale JSON file`}`
6840
6889
  );
6841
6890
  }
6842
6891
  function splitTaggedLine(line) {
@@ -6911,11 +6960,11 @@ async function listLocaleBasenamesIfDirMany(context, dirs) {
6911
6960
  }
6912
6961
  async function readFilesMany(context, paths) {
6913
6962
  const sortedPaths = [...paths].sort((a, b) => a.localeCompare(b));
6914
- const settled = await mapConcurrent(sortedPaths, SANDBOX_READ_CONCURRENCY, async (path3) => {
6963
+ const settled = await mapConcurrent(sortedPaths, SANDBOX_READ_CONCURRENCY, async (path4) => {
6915
6964
  try {
6916
- return { path: path3, value: await context.sandbox.readFile(path3) };
6965
+ return { path: path4, value: await context.sandbox.readFile(path4) };
6917
6966
  } catch (error) {
6918
- return { path: path3, error };
6967
+ return { path: path4, error };
6919
6968
  }
6920
6969
  });
6921
6970
  const files = /* @__PURE__ */ new Map();
@@ -6936,55 +6985,60 @@ function readFailurePath(error) {
6936
6985
  return typeof error === "object" && error !== null && "path" in error ? String(error.path) : "unknown";
6937
6986
  }
6938
6987
  var CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN = "conventional-entry-kind";
6988
+ var SERVER_REGISTRATIONS_FILE = "server/registrations.ts";
6939
6989
  var COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN = "composition-artifact-kind";
6940
- async function classifyCompositionArtifactPathKind(context, path3) {
6941
- return classifySandboxPathPresenceKind(context, path3, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
6990
+ async function classifyCompositionArtifactPathKind(context, path4) {
6991
+ return classifySandboxPathPresenceKind(context, path4, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
6942
6992
  }
6943
- function assertRealCompositionArtifactFilePresence(path3, presence) {
6993
+ function assertRealCompositionArtifactFilePresence(path4, presence) {
6944
6994
  if (presence === "file") return "file";
6945
6995
  if (presence === "absent") return "absent";
6946
- const presenceError = formatConventionalEntryPresenceError(path3, presence);
6996
+ const presenceError = formatConventionalEntryPresenceError(path4, presence);
6947
6997
  throw new Error(
6948
- `[ModuleRail] composition artifacts: ${presenceError ?? `${path3} is not a real composition artifact file`}`
6998
+ `[ModuleRail] composition artifacts: ${presenceError ?? `${path4} is not a real composition artifact file`}`
6949
6999
  );
6950
7000
  }
6951
- async function readConventionalEntryIfRealFile(path3, kind, presences, files) {
6952
- const presence = presences.get(path3) ?? "unreadable";
6953
- const presenceError = formatConventionalEntryPresenceError(path3, presence);
7001
+ async function readConventionalEntryIfRealFile(path4, kind, presences, files) {
7002
+ const presence = presences.get(path4) ?? "unreadable";
7003
+ const presenceError = formatConventionalEntryPresenceError(path4, presence);
6954
7004
  if (presenceError) {
6955
7005
  throw new Error(`[ModuleRail] composition artifacts: ${presenceError}`);
6956
7006
  }
6957
7007
  if (presence === "absent") {
6958
7008
  return null;
6959
7009
  }
6960
- const source = files.get(path3);
7010
+ const source = files.get(path4);
6961
7011
  switch (kind) {
6962
7012
  case "contributions":
6963
- assertContributionsExport(source, path3);
6964
- assertClientSafeConventionalEntryImports(source, path3, "contributions");
7013
+ assertContributionsExport(source, path4);
7014
+ assertClientSafeConventionalEntryImports(source, path4, "contributions");
6965
7015
  break;
6966
7016
  case "slotCatalogs":
6967
- assertSlotCatalogsExport(source, path3);
6968
- assertClientSafeConventionalEntryImports(source, path3, "slotCatalogs");
7017
+ assertSlotCatalogsExport(source, path4);
7018
+ assertClientSafeConventionalEntryImports(source, path4, "slotCatalogs");
6969
7019
  break;
6970
7020
  case "initializeEnhancement":
6971
- assertInitializeEnhancementExport(source, path3);
7021
+ assertInitializeEnhancementExport(source, path4);
6972
7022
  break;
6973
7023
  }
6974
7024
  return source;
6975
7025
  }
6976
- async function readJsonObjectFile(path3, files) {
6977
- const raw = files.get(path3);
7026
+ function hasRealServerRegistrations(modulePath, presences) {
7027
+ const path4 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
7028
+ return assertRealCompositionArtifactFilePresence(path4, presences.get(path4) ?? "unreadable") === "file";
7029
+ }
7030
+ async function readJsonObjectFile(path4, files) {
7031
+ const raw = files.get(path4);
6978
7032
  let parsed;
6979
7033
  try {
6980
7034
  parsed = JSON.parse(raw);
6981
7035
  } catch (error) {
6982
7036
  throw new Error(
6983
- `[ModuleRail] composition artifacts: invalid JSON at ${path3}: ${error instanceof Error ? error.message : String(error)}`
7037
+ `[ModuleRail] composition artifacts: invalid JSON at ${path4}: ${error instanceof Error ? error.message : String(error)}`
6984
7038
  );
6985
7039
  }
6986
7040
  if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
6987
- throw new Error(`[ModuleRail] composition artifacts: ${path3} root must be an object`);
7041
+ throw new Error(`[ModuleRail] composition artifacts: ${path4} root must be an object`);
6988
7042
  }
6989
7043
  return parsed;
6990
7044
  }
@@ -6996,6 +7050,9 @@ async function collectCompositionEntryFacts(context, registry) {
6996
7050
  const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
6997
7051
  const localeDirs = [substrateDir, overridesDir];
6998
7052
  const conventionalPaths = [];
7053
+ const serverRegistrationPaths = registry.modules.map(
7054
+ (mod) => `${MODULES_SANDBOX_DIR}/${mod.name}/${SERVER_REGISTRATIONS_FILE}`
7055
+ );
6999
7056
  for (const mod of registry.modules) {
7000
7057
  const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
7001
7058
  localeDirs.push(`${modulePath}/i18n`);
@@ -7024,17 +7081,17 @@ async function collectCompositionEntryFacts(context, registry) {
7024
7081
  const [conventionalPresences, localePresences] = await Promise.all([
7025
7082
  classifySandboxPathPresenceKindMany(
7026
7083
  context,
7027
- conventionalPaths,
7084
+ [...conventionalPaths, ...serverRegistrationPaths],
7028
7085
  CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN
7029
7086
  ),
7030
7087
  classifySandboxPathPresenceKindMany(context, localePaths, LOCALE_JSON_KIND_PROBE_TOKEN)
7031
7088
  ]);
7032
7089
  const presentConventionalPaths = [];
7033
- for (const path3 of conventionalPaths) {
7034
- const presence = conventionalPresences.get(path3) ?? "unreadable";
7035
- if (presence === "file") presentConventionalPaths.push(path3);
7090
+ for (const path4 of conventionalPaths) {
7091
+ const presence = conventionalPresences.get(path4) ?? "unreadable";
7092
+ if (presence === "file") presentConventionalPaths.push(path4);
7036
7093
  }
7037
- const presentLocalePaths = localePaths.filter((path3) => localePresences.get(path3) === "file");
7094
+ const presentLocalePaths = localePaths.filter((path4) => localePresences.get(path4) === "file");
7038
7095
  const files = await readFilesMany(context, [...presentConventionalPaths, ...presentLocalePaths]);
7039
7096
  const substrateLocales = [];
7040
7097
  for (const locale of substrateNames) {
@@ -7086,6 +7143,7 @@ async function collectCompositionEntryFacts(context, registry) {
7086
7143
  );
7087
7144
  }
7088
7145
  }
7146
+ const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
7089
7147
  const rootLocales = localesByDir.get(`${modulePath}/i18n`);
7090
7148
  if (rootLocales.length > 0 && !rootLocales.includes("en")) {
7091
7149
  throw new Error(
@@ -7125,13 +7183,14 @@ async function collectCompositionEntryFacts(context, registry) {
7125
7183
  moduleName: mod.name,
7126
7184
  hasRootContributions,
7127
7185
  hasRootSlots,
7186
+ hasServerRegistrations,
7128
7187
  rootLocales,
7129
7188
  enhancements
7130
7189
  });
7131
7190
  if (rootLocales.length > 0) {
7132
7191
  for (const locale of rootLocales) {
7133
- const path3 = `${modulePath}/i18n/${locale}.json`;
7134
- assertRealLocaleJsonFilePresence(path3, localePresences.get(path3) ?? "unreadable");
7192
+ const path4 = `${modulePath}/i18n/${locale}.json`;
7193
+ assertRealLocaleJsonFilePresence(path4, localePresences.get(path4) ?? "unreadable");
7135
7194
  }
7136
7195
  const enMessages = await readJsonObjectFile(`${modulePath}/i18n/en.json`, files);
7137
7196
  const localeMessages = { en: enMessages };
@@ -7155,8 +7214,8 @@ async function collectCompositionEntryFacts(context, registry) {
7155
7214
  if (!enh.active || enh.locales.length === 0) continue;
7156
7215
  const peerDir = `${modulePath}/enhancements/${enh.peer}`;
7157
7216
  for (const locale of enh.locales) {
7158
- const path3 = `${peerDir}/i18n/${locale}.json`;
7159
- assertRealLocaleJsonFilePresence(path3, localePresences.get(path3) ?? "unreadable");
7217
+ const path4 = `${peerDir}/i18n/${locale}.json`;
7218
+ assertRealLocaleJsonFilePresence(path4, localePresences.get(path4) ?? "unreadable");
7160
7219
  }
7161
7220
  const enMessages = await readJsonObjectFile(`${peerDir}/i18n/en.json`, files);
7162
7221
  const localeMessages = { en: enMessages };
@@ -7328,6 +7387,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7328
7387
  MODULE_DATASTORES_GEN_PATH,
7329
7388
  MODULE_I18N_GEN_PATH,
7330
7389
  MODULE_INIT_SERVER_GEN_PATH,
7390
+ MODULE_SERVER_GEN_PATH,
7331
7391
  ...existingGeneratedPaths,
7332
7392
  ...Object.keys(existingAppPages),
7333
7393
  ...Object.keys(existingAppRoutes)
@@ -7356,16 +7416,16 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7356
7416
  for (const stub of probePlan.plan.routeStubs) candidatePaths.add(stub.path);
7357
7417
  const existingFiles = {};
7358
7418
  const remainingCandidatePaths = [];
7359
- for (const path3 of [...candidatePaths].sort((a, b) => a.localeCompare(b))) {
7360
- if (Object.prototype.hasOwnProperty.call(existingAppPages, path3)) {
7361
- existingFiles[path3] = existingAppPages[path3];
7419
+ for (const path4 of [...candidatePaths].sort((a, b) => a.localeCompare(b))) {
7420
+ if (Object.prototype.hasOwnProperty.call(existingAppPages, path4)) {
7421
+ existingFiles[path4] = existingAppPages[path4];
7362
7422
  continue;
7363
7423
  }
7364
- if (Object.prototype.hasOwnProperty.call(existingAppRoutes, path3)) {
7365
- existingFiles[path3] = existingAppRoutes[path3];
7424
+ if (Object.prototype.hasOwnProperty.call(existingAppRoutes, path4)) {
7425
+ existingFiles[path4] = existingAppRoutes[path4];
7366
7426
  continue;
7367
7427
  }
7368
- remainingCandidatePaths.push(path3);
7428
+ remainingCandidatePaths.push(path4);
7369
7429
  }
7370
7430
  const candidateKinds = await classifySandboxPathPresenceKindMany(
7371
7431
  context,
@@ -7373,12 +7433,12 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7373
7433
  COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN
7374
7434
  );
7375
7435
  const presentCandidatePaths = [];
7376
- for (const path3 of remainingCandidatePaths) {
7436
+ for (const path4 of remainingCandidatePaths) {
7377
7437
  const presence = assertRealCompositionArtifactFilePresence(
7378
- path3,
7379
- candidateKinds.get(path3) ?? "unreadable"
7438
+ path4,
7439
+ candidateKinds.get(path4) ?? "unreadable"
7380
7440
  );
7381
- if (presence === "file") presentCandidatePaths.push(path3);
7441
+ if (presence === "file") presentCandidatePaths.push(path4);
7382
7442
  }
7383
7443
  try {
7384
7444
  Object.assign(
@@ -7386,9 +7446,9 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7386
7446
  Object.fromEntries(await readFilesMany(context, presentCandidatePaths))
7387
7447
  );
7388
7448
  } catch (error) {
7389
- const path3 = readFailurePath(error);
7449
+ const path4 = readFailurePath(error);
7390
7450
  throw new Error(
7391
- `[ModuleRail] composition artifacts: cannot read ${redactSecrets(path3)} during preflight: ${redactSecrets(
7451
+ `[ModuleRail] composition artifacts: cannot read ${redactSecrets(path4)} during preflight: ${redactSecrets(
7392
7452
  error instanceof Error ? error.message : String(error)
7393
7453
  )}`
7394
7454
  );
@@ -7457,14 +7517,139 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7457
7517
  var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
7458
7518
  var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
7459
7519
 
7520
+ // src/module-boundaries.ts
7521
+ var import_node_fs2 = __toESM(require("fs"));
7522
+ var import_node_path2 = __toESM(require("path"));
7523
+ var import_typescript5 = __toESM(require("typescript"));
7524
+ var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
7525
+ var RESOLVE_SUFFIXES = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
7526
+ var DATA_FILE = /\.(?:sql|json)$/;
7527
+ function dataPathsIn(fileName, source) {
7528
+ if (!/\.(?:sql|json)["'`]/.test(source)) return [];
7529
+ const paths = [];
7530
+ const visit = (node) => {
7531
+ if ((import_typescript5.default.isStringLiteralLike(node) || import_typescript5.default.isTemplateTail(node)) && DATA_FILE.test(node.text)) {
7532
+ paths.push(node.text);
7533
+ }
7534
+ import_typescript5.default.forEachChild(node, visit);
7535
+ };
7536
+ visit(import_typescript5.default.createSourceFile(fileName, source, import_typescript5.default.ScriptTarget.Latest, false));
7537
+ return paths;
7538
+ }
7539
+ function scanModule(modulesDir, module2) {
7540
+ const scanned = /* @__PURE__ */ new Map();
7541
+ const moduleDir = import_node_path2.default.join(modulesDir, module2);
7542
+ if (!import_node_fs2.default.existsSync(moduleDir)) return scanned;
7543
+ const files = import_node_fs2.default.readdirSync(moduleDir, { recursive: true, encoding: "utf8" }).filter((relative) => SOURCE_FILE.test(relative) && !relative.includes("node_modules"));
7544
+ for (const relative of files) {
7545
+ const absolute = import_node_path2.default.join(moduleDir, relative);
7546
+ if (!import_node_fs2.default.statSync(absolute).isFile()) continue;
7547
+ const source = import_node_fs2.default.readFileSync(absolute, "utf8");
7548
+ const specifiers = import_typescript5.default.preProcessFile(source, true, true).importedFiles.map((f) => f.fileName);
7549
+ const dataPaths = dataPathsIn(absolute, source);
7550
+ const targets = [...specifiers, ...dataPaths].flatMap((spec) => {
7551
+ if (spec.startsWith("@/modules/"))
7552
+ return [import_node_path2.default.join(modulesDir, spec.slice("@/modules/".length))];
7553
+ if (spec.startsWith("./") || spec.startsWith("../")) {
7554
+ return [import_node_path2.default.resolve(import_node_path2.default.dirname(absolute), spec)];
7555
+ }
7556
+ return [];
7557
+ });
7558
+ scanned.set(absolute, {
7559
+ module: module2,
7560
+ relative: relative.split(import_node_path2.default.sep).join("/"),
7561
+ targets,
7562
+ dataPaths
7563
+ });
7564
+ }
7565
+ return scanned;
7566
+ }
7567
+ function findModuleBoundaryViolations(modulesDir, peersByModule, { pruning = false } = {}) {
7568
+ const files = /* @__PURE__ */ new Map();
7569
+ for (const [module2, peers] of Object.entries(peersByModule)) {
7570
+ if (peers.length === 0) continue;
7571
+ for (const [absolute, file] of scanModule(modulesDir, module2)) files.set(absolute, file);
7572
+ }
7573
+ const exempt = (file, peer) => (pruning ? peersByModule[file.module] ?? [] : [peer]).some(
7574
+ (q) => file.relative.startsWith(`enhancements/${q}/`)
7575
+ );
7576
+ const goneWith = (target, peer) => {
7577
+ const relative = import_node_path2.default.relative(modulesDir, target).split(import_node_path2.default.sep).join("/");
7578
+ return relative === peer || relative.startsWith(`${peer}/`) || new RegExp(`^[^/]+/enhancements/${peer}(/|$)`).test(relative);
7579
+ };
7580
+ const tainted = /* @__PURE__ */ new Map();
7581
+ const queue = [];
7582
+ const taint = (absolute, peer) => {
7583
+ const peers = tainted.get(absolute) ?? /* @__PURE__ */ new Set();
7584
+ if (peers.has(peer)) return;
7585
+ peers.add(peer);
7586
+ tainted.set(absolute, peers);
7587
+ queue.push([absolute, peer]);
7588
+ };
7589
+ const importers = /* @__PURE__ */ new Map();
7590
+ for (const [absolute, file] of files) {
7591
+ for (const target of file.targets) {
7592
+ const hit = RESOLVE_SUFFIXES.map((suffix) => target + suffix).find((c) => files.has(c));
7593
+ if (hit) importers.set(hit, [...importers.get(hit) ?? [], absolute]);
7594
+ }
7595
+ for (const peer of peersByModule[file.module] ?? []) {
7596
+ if (exempt(file, peer)) continue;
7597
+ if (file.targets.some((target) => goneWith(target, peer)) || file.dataPaths.some(
7598
+ (p) => p.startsWith(`modules/${peer}/`) || p.includes(`/modules/${peer}/`)
7599
+ )) {
7600
+ taint(absolute, peer);
7601
+ }
7602
+ }
7603
+ }
7604
+ for (let next = queue.shift(); next; next = queue.shift()) {
7605
+ const [target, peer] = next;
7606
+ for (const importer of importers.get(target) ?? []) {
7607
+ const file = files.get(importer);
7608
+ if ((peersByModule[file.module] ?? []).includes(peer) && !exempt(file, peer)) {
7609
+ taint(importer, peer);
7610
+ }
7611
+ }
7612
+ }
7613
+ const violations = [];
7614
+ for (const [absolute, peers] of tainted) {
7615
+ const file = files.get(absolute);
7616
+ for (const peer of peers) {
7617
+ violations.push({ file: `${file.module}/${file.relative}`, module: file.module, peer });
7618
+ }
7619
+ }
7620
+ return violations.sort((a, b) => a.file.localeCompare(b.file) || a.peer.localeCompare(b.peer));
7621
+ }
7622
+ function findOptionalPeerViolations(modulesDir) {
7623
+ if (!import_node_fs2.default.existsSync(modulesDir)) return [];
7624
+ const manifests = {};
7625
+ for (const name of import_node_fs2.default.readdirSync(modulesDir)) {
7626
+ const manifestPath = import_node_path2.default.join(modulesDir, name, "module.json");
7627
+ if (!import_node_fs2.default.existsSync(manifestPath)) continue;
7628
+ manifests[name] = JSON.parse(import_node_fs2.default.readFileSync(manifestPath, "utf8"));
7629
+ }
7630
+ const features = Object.keys(manifests).filter((name) => manifests[name].kind === "feature");
7631
+ const peersByModule = {};
7632
+ for (const [name, manifest] of Object.entries(manifests)) {
7633
+ const uses = new Set(manifest.uses ?? []);
7634
+ peersByModule[name] = features.filter((peer) => peer !== name && !uses.has(peer));
7635
+ }
7636
+ return findModuleBoundaryViolations(modulesDir, peersByModule);
7637
+ }
7638
+ function formatBoundaryViolations(modulesDirLabel, violations) {
7639
+ const files = new Set(violations.map((v) => v.file)).size;
7640
+ return `${files} Module file(s) import a peer Module that may be absent (not in the Module's \`uses\`, or excluded by --enabled).
7641
+ \`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).
7642
+ ` + violations.map((v) => `${modulesDirLabel}/${v.file}: ${v.peer}`).join("\n");
7643
+ }
7644
+
7460
7645
  // src/index.ts
7461
7646
  var MODULES_DIR2 = MODULES_SANDBOX_DIR;
7462
7647
  function readComposeInputs(cwd) {
7463
- const file = import_node_path2.default.join(cwd, COMPOSE_INPUTS_PATH);
7464
- if (!import_node_fs2.default.existsSync(file)) return { connectedStores: [], installs: {} };
7648
+ const file = import_node_path3.default.join(cwd, COMPOSE_INPUTS_PATH);
7649
+ if (!import_node_fs3.default.existsSync(file)) return { connectedStores: [], installs: {} };
7465
7650
  let raw;
7466
7651
  try {
7467
- raw = JSON.parse(import_node_fs2.default.readFileSync(file, "utf8"));
7652
+ raw = JSON.parse(import_node_fs3.default.readFileSync(file, "utf8"));
7468
7653
  } catch (error) {
7469
7654
  throw new Error(
7470
7655
  `[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
@@ -7506,18 +7691,18 @@ function stubGitignoreEntries(artifactPaths) {
7506
7691
  function writeStubGitignore(cwd, artifactPaths) {
7507
7692
  const entries = stubGitignoreEntries(artifactPaths);
7508
7693
  const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
7509
- const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
7510
- const existing = import_node_fs2.default.existsSync(file) ? import_node_fs2.default.readFileSync(file, "utf8") : null;
7694
+ const file = import_node_path3.default.join(cwd, STUB_GITIGNORE_PATH);
7695
+ const existing = import_node_fs3.default.existsSync(file) ? import_node_fs3.default.readFileSync(file, "utf8") : null;
7511
7696
  if (existing !== content) {
7512
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
7513
- import_node_fs2.default.writeFileSync(file, content);
7697
+ import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(file), { recursive: true });
7698
+ import_node_fs3.default.writeFileSync(file, content);
7514
7699
  }
7515
7700
  return entries.length;
7516
7701
  }
7517
7702
  function assertStubGitignoreOwned(cwd) {
7518
- const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
7519
- if (!import_node_fs2.default.existsSync(file)) return;
7520
- const firstLine = import_node_fs2.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
7703
+ const file = import_node_path3.default.join(cwd, STUB_GITIGNORE_PATH);
7704
+ if (!import_node_fs3.default.existsSync(file)) return;
7705
+ const firstLine = import_node_fs3.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
7521
7706
  if (firstLine !== STUB_GITIGNORE_HEADER) {
7522
7707
  throw new Error(
7523
7708
  `[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
@@ -7525,8 +7710,8 @@ function assertStubGitignoreOwned(cwd) {
7525
7710
  }
7526
7711
  }
7527
7712
  function assertAppRoot(cwd) {
7528
- const appPath = import_node_path2.default.join(cwd, APP_PACKAGE_JSON);
7529
- if (!import_node_fs2.default.existsSync(appPath)) {
7713
+ const appPath = import_node_path3.default.join(cwd, APP_PACKAGE_JSON);
7714
+ if (!import_node_fs3.default.existsSync(appPath)) {
7530
7715
  throw new Error(
7531
7716
  `[compose] ${appPath} not found \u2014 run this from the repo root of a Stardeck app`
7532
7717
  );
@@ -7572,9 +7757,9 @@ ${porcelain.trim()}`
7572
7757
  }
7573
7758
  var APP_SRC_DIR = "apps/web/src";
7574
7759
  function walkSources(dir, skip, out) {
7575
- if (!import_node_fs2.default.existsSync(dir)) return;
7576
- for (const entry of import_node_fs2.default.readdirSync(dir, { withFileTypes: true })) {
7577
- const full = import_node_path2.default.join(dir, entry.name);
7760
+ if (!import_node_fs3.default.existsSync(dir)) return;
7761
+ for (const entry of import_node_fs3.default.readdirSync(dir, { withFileTypes: true })) {
7762
+ const full = import_node_path3.default.join(dir, entry.name);
7578
7763
  if (full === skip) continue;
7579
7764
  if (entry.isDirectory()) walkSources(full, skip, out);
7580
7765
  else if (/\.[cm]?[jt]sx?$/.test(entry.name)) out.push(full);
@@ -7583,13 +7768,13 @@ function walkSources(dir, skip, out) {
7583
7768
  function assertNoAppLayerImporters(cwd, excluded) {
7584
7769
  if (excluded.length === 0) return;
7585
7770
  const files = [];
7586
- walkSources(import_node_path2.default.join(cwd, APP_SRC_DIR), import_node_path2.default.join(cwd, MODULES_DIR2), files);
7771
+ walkSources(import_node_path3.default.join(cwd, APP_SRC_DIR), import_node_path3.default.join(cwd, MODULES_DIR2), files);
7587
7772
  const offenders = [];
7588
7773
  for (const file of files) {
7589
- const source = import_node_fs2.default.readFileSync(file, "utf8");
7774
+ const source = import_node_fs3.default.readFileSync(file, "utf8");
7590
7775
  for (const name of excluded) {
7591
7776
  if (new RegExp(`@/modules/${name}(?=["'\`/])`).test(source)) {
7592
- offenders.push(`${import_node_path2.default.relative(cwd, file)}: ${name}`);
7777
+ offenders.push(`${import_node_path3.default.relative(cwd, file)}: ${name}`);
7593
7778
  }
7594
7779
  }
7595
7780
  }
@@ -7602,8 +7787,8 @@ function assertNoAppLayerImporters(cwd, excluded) {
7602
7787
  }
7603
7788
  }
7604
7789
  function removeDir(absolute) {
7605
- if (!import_node_fs2.default.existsSync(absolute)) return false;
7606
- import_node_fs2.default.rmSync(absolute, { recursive: true, force: true });
7790
+ if (!import_node_fs3.default.existsSync(absolute)) return false;
7791
+ import_node_fs3.default.rmSync(absolute, { recursive: true, force: true });
7607
7792
  return true;
7608
7793
  }
7609
7794
  async function compose(options) {
@@ -7630,6 +7815,16 @@ async function compose(options) {
7630
7815
  dryRun: true
7631
7816
  });
7632
7817
  assertNoAppLayerImporters(options.cwd, preview.excludedModules);
7818
+ const violations = findModuleBoundaryViolations(
7819
+ import_node_path3.default.join(options.cwd, MODULES_DIR2),
7820
+ Object.fromEntries(preview.composedModules.map((name) => [name, preview.excludedModules])),
7821
+ { pruning: true }
7822
+ );
7823
+ if (violations.length > 0) {
7824
+ throw new Error(
7825
+ `[compose] refusing --prune: ${formatBoundaryViolations(MODULES_DIR2, violations)}`
7826
+ );
7827
+ }
7633
7828
  }
7634
7829
  const result = await reconcileCompositionArtifacts(context, {
7635
7830
  enabledModulesRaw: options.enabled,
@@ -7641,11 +7836,11 @@ async function compose(options) {
7641
7836
  let pruned = 0;
7642
7837
  if (options.prune) {
7643
7838
  for (const name of result.excludedModules) {
7644
- if (removeDir(import_node_path2.default.join(options.cwd, MODULES_DIR2, name))) pruned++;
7839
+ if (removeDir(import_node_path3.default.join(options.cwd, MODULES_DIR2, name))) pruned++;
7645
7840
  }
7646
7841
  for (const owner of result.composedModules) {
7647
7842
  for (const peer of result.excludedModules) {
7648
- if (removeDir(import_node_path2.default.join(options.cwd, MODULES_DIR2, owner, "enhancements", peer))) pruned++;
7843
+ if (removeDir(import_node_path3.default.join(options.cwd, MODULES_DIR2, owner, "enhancements", peer))) pruned++;
7649
7844
  }
7650
7845
  }
7651
7846
  }
@@ -7664,5 +7859,8 @@ async function compose(options) {
7664
7859
  assertSupportedTypescript,
7665
7860
  compose,
7666
7861
  escapeGitignore,
7862
+ findModuleBoundaryViolations,
7863
+ findOptionalPeerViolations,
7864
+ formatBoundaryViolations,
7667
7865
  readComposeInputs
7668
7866
  });