@stardeck-customer-apps/compose 0.8.0 → 0.10.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
  }
@@ -3901,12 +3901,13 @@ var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
3901
3901
  var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
3902
3902
 
3903
3903
  // src/cli.ts
3904
- var import_typescript5 = __toESM(require("typescript"));
3904
+ var import_typescript7 = __toESM(require("typescript"));
3905
+ var import_node_path4 = __toESM(require("path"));
3905
3906
 
3906
3907
  // src/index.ts
3907
3908
  var import_node_child_process2 = require("child_process");
3908
- var import_node_fs2 = __toESM(require("fs"));
3909
- var import_node_path2 = __toESM(require("path"));
3909
+ var import_node_fs3 = __toESM(require("fs"));
3910
+ var import_node_path3 = __toESM(require("path"));
3910
3911
 
3911
3912
  // ../../packages/lib/src/server/module-rail/local-fs-sandbox.ts
3912
3913
  var import_node_child_process = require("child_process");
@@ -3965,147 +3966,29 @@ function shellQuote(value) {
3965
3966
  return `'${value.replace(/'/g, `'\\''`)}'`;
3966
3967
  }
3967
3968
 
3968
- // ../../packages/lib/src/shared/module-composition.ts
3969
- var import_zod6 = require("zod");
3970
- var moduleRefSchema = import_zod6.z.object({
3971
- homeProjectId: import_zod6.z.string().uuid(),
3972
- moduleName: moduleNameSchema
3973
- });
3974
- var creationSourceBlankSchema = import_zod6.z.object({
3975
- type: import_zod6.z.literal("blank")
3976
- });
3977
- var creationSourceBlueprintSchema = import_zod6.z.object({
3978
- type: import_zod6.z.literal("blueprint"),
3979
- /** Internal template/fork project id — keep template naming in identifiers. */
3980
- blueprintProjectId: import_zod6.z.string().uuid(),
3981
- /**
3982
- * `null`/absent means every feature Module. `[]` is refused: the platform
3983
- * serialises it to an empty `NEXT_PUBLIC_STARDECK_ENABLED_MODULES`, which the
3984
- * enablement parser reads back as "all" (`module-enablement.ts`) — the
3985
- * opposite of what an empty selection means.
3986
- */
3987
- enabledModules: import_zod6.z.array(moduleNameSchema).min(1, "Enable at least one Module").max(100).nullable().optional()
3988
- });
3989
- var creationSourceModulesSchema = import_zod6.z.object({
3990
- type: import_zod6.z.literal("modules"),
3991
- requested: import_zod6.z.array(moduleRefSchema).min(1)
3992
- });
3993
- var creationSourceSchema = import_zod6.z.discriminatedUnion("type", [
3994
- creationSourceBlankSchema,
3995
- creationSourceBlueprintSchema,
3996
- creationSourceModulesSchema
3997
- ]);
3998
- var moduleCompositionStatusSchema = import_zod6.z.enum([
3999
- "pending",
4000
- "installing",
4001
- "ready",
4002
- "partial",
4003
- "failed"
4004
- ]);
4005
- var resolvedModuleSnapshotSchema = moduleRefSchema.extend({
4006
- version: import_zod6.z.string().min(1),
4007
- homeCommitSha: import_zod6.z.string().min(1)
4008
- });
4009
- var moduleInstallHandoffSchema = import_zod6.z.object({
4010
- moduleName: import_zod6.z.string().min(1),
4011
- homeProjectId: import_zod6.z.string().uuid(),
4012
- installedVersion: import_zod6.z.string().min(1),
4013
- baselineSha: import_zod6.z.string().min(1),
4014
- mounts: import_zod6.z.array(
4015
- import_zod6.z.object({
4016
- group: import_zod6.z.string(),
4017
- segment: import_zod6.z.string()
4018
- })
4019
- ),
4020
- integration: import_zod6.z.array(import_zod6.z.string()),
4021
- workflows: import_zod6.z.array(import_zod6.z.string()),
4022
- prunedEnhancements: import_zod6.z.array(import_zod6.z.string()),
4023
- restoredEnhancements: import_zod6.z.array(import_zod6.z.string()).optional(),
4024
- permissions: import_zod6.z.array(
4025
- import_zod6.z.object({
4026
- key: import_zod6.z.string().min(1),
4027
- description: import_zod6.z.string().min(1)
4028
- })
4029
- ).optional()
4030
- });
4031
- var moduleCompositionRunAttemptSchema = import_zod6.z.object({
4032
- module: moduleRefSchema,
4033
- runId: import_zod6.z.string().min(1),
4034
- status: import_zod6.z.string().min(1),
4035
- schemaApplied: import_zod6.z.boolean(),
4036
- error: import_zod6.z.string().optional(),
4037
- /** Prefer full ModuleInstallHandoff; identity fields optional for older rows. */
4038
- handoff: moduleInstallHandoffSchema.partial({ moduleName: true, homeProjectId: true }).optional()
4039
- });
4040
- var initialModuleCompositionSchema = import_zod6.z.object({
4041
- version: import_zod6.z.literal(1),
4042
- status: moduleCompositionStatusSchema,
4043
- requested: import_zod6.z.array(moduleRefSchema).min(1),
4044
- resolved: import_zod6.z.array(resolvedModuleSnapshotSchema),
4045
- installOrder: import_zod6.z.array(moduleRefSchema),
4046
- runs: import_zod6.z.array(moduleCompositionRunAttemptSchema).default([]),
4047
- /**
4048
- * True when any failed/partial attempt applied schema before failing.
4049
- * Never claim transactional rollback across git/schema.
4050
- */
4051
- schemaApplied: import_zod6.z.boolean().default(false),
4052
- /**
4053
- * Present when retry would silently change pinned home SHAs — caller must
4054
- * reconfirm the new plan rather than continue.
4055
- */
4056
- reconfirmationRequired: import_zod6.z.object({
4057
- reason: import_zod6.z.string(),
4058
- previous: import_zod6.z.array(resolvedModuleSnapshotSchema),
4059
- next: import_zod6.z.array(resolvedModuleSnapshotSchema)
4060
- }).optional(),
4061
- error: import_zod6.z.string().optional(),
4062
- /**
4063
- * First-turn system handoff lifecycle. Absent/`pending` means inject when
4064
- * status is ready; `consumed` means a successful agent turn already used it.
4065
- */
4066
- systemHandoffStatus: import_zod6.z.enum(["pending", "consumed"]).optional(),
4067
- /**
4068
- * CAS ownership token for concurrent retry attempts. Only the attempt that
4069
- * currently owns this value may reset or compensate durable composition state.
4070
- */
4071
- retryAttemptId: import_zod6.z.string().min(1).optional(),
4072
- /**
4073
- * When the current `installing` attempt was claimed. A setup run that dies
4074
- * with its host (a killed Vercel instance) leaves the composition `installing`
4075
- * forever, and the creating page's poll only stops on ready/partial/failed —
4076
- * so staleness, not a queue's stall detector, is what settles it. Absent on
4077
- * rows claimed before this field existed; treated as stale.
4078
- */
4079
- attemptStartedAt: import_zod6.z.string().datetime().optional(),
4080
- /** Outside-module files the agent changed during connection work. */
4081
- integrationFiles: import_zod6.z.array(import_zod6.z.string()).optional(),
4082
- verificationSummary: import_zod6.z.string().optional()
4083
- });
4084
- var MODULE_SETUP_ATTEMPT_STALE_MS = 10 * 60 * 1e3;
4085
-
4086
3969
  // ../../packages/lib/src/shared/compose-inputs.ts
4087
- var import_zod7 = require("zod");
3970
+ var import_zod6 = require("zod");
4088
3971
  var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
4089
- var composeInputsSchema = import_zod7.z.strictObject({
4090
- connectedStores: import_zod7.z.array(
4091
- import_zod7.z.strictObject({
4092
- storeId: import_zod7.z.string().min(1),
4093
- slug: import_zod7.z.string().min(1),
3972
+ var composeInputsSchema = import_zod6.z.strictObject({
3973
+ connectedStores: import_zod6.z.array(
3974
+ import_zod6.z.strictObject({
3975
+ storeId: import_zod6.z.string().min(1),
3976
+ slug: import_zod6.z.string().min(1),
4094
3977
  // A row with an empty binding key must not fail a deploy: the rail
4095
3978
  // treats falsy as "no binding key", so normalise rather than reject.
4096
- bindingKey: import_zod7.z.string().nullish().transform((value) => value || null),
4097
- accessLevel: import_zod7.z.enum(["read", "write", "admin"]),
3979
+ bindingKey: import_zod6.z.string().nullish().transform((value) => value || null),
3980
+ accessLevel: import_zod6.z.enum(["read", "write", "admin"]),
4098
3981
  // Defaulted so a file written before the platform emitted store types
4099
3982
  // (database rows only) still parses. The platform starts writing
4100
3983
  // storage rows, with `type` set, once forks run a compose that reads it.
4101
- type: import_zod7.z.enum(["database", "storage"]).default("database")
3984
+ type: import_zod6.z.enum(["database", "storage"]).default("database")
4102
3985
  })
4103
3986
  ).default([]),
4104
- installs: import_zod7.z.record(
4105
- import_zod7.z.string(),
4106
- import_zod7.z.strictObject({
4107
- installedVersion: import_zod7.z.string().min(1),
4108
- datastoreBindings: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).nullish()
3987
+ installs: import_zod6.z.record(
3988
+ import_zod6.z.string(),
3989
+ import_zod6.z.strictObject({
3990
+ installedVersion: import_zod6.z.string().min(1),
3991
+ datastoreBindings: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).nullish()
4109
3992
  })
4110
3993
  ).default({})
4111
3994
  });
@@ -4262,13 +4145,13 @@ function assertClientSafeConventionalEntryImports(source, pathLabel, kind) {
4262
4145
  function createMessageTree() {
4263
4146
  return /* @__PURE__ */ Object.create(null);
4264
4147
  }
4265
- function assertMessageTree(value, path3) {
4148
+ function assertMessageTree(value, path5) {
4266
4149
  if (typeof value === "string") return;
4267
4150
  if (value === null || Array.isArray(value) || typeof value !== "object") {
4268
- throw new Error(`Invalid message at ${path3}: expected string or nested object`);
4151
+ throw new Error(`Invalid message at ${path5}: expected string or nested object`);
4269
4152
  }
4270
4153
  for (const [key, child] of Object.entries(value)) {
4271
- assertMessageTree(child, `${path3}.${key}`);
4154
+ assertMessageTree(child, `${path5}.${key}`);
4272
4155
  }
4273
4156
  }
4274
4157
  function cloneTree(value) {
@@ -4278,17 +4161,17 @@ function cloneTree(value) {
4278
4161
  }
4279
4162
  return clone;
4280
4163
  }
4281
- function collectLeafPaths(value, path3, out) {
4164
+ function collectLeafPaths(value, path5, out) {
4282
4165
  if (typeof value === "string") {
4283
- out.push(path3);
4166
+ out.push(path5);
4284
4167
  return;
4285
4168
  }
4286
- assertMessageTree(value, path3);
4287
- for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path3}.${key}`, out);
4169
+ assertMessageTree(value, path5);
4170
+ for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path5}.${key}`, out);
4288
4171
  }
4289
- function overrideInto(target, source, owner, path3, missing) {
4172
+ function overrideInto(target, source, owner, path5, missing) {
4290
4173
  for (const [key, value] of Object.entries(source)) {
4291
- const nextPath = path3 ? `${path3}.${key}` : key;
4174
+ const nextPath = path5 ? `${path5}.${key}` : key;
4292
4175
  if (!Object.prototype.hasOwnProperty.call(target, key)) {
4293
4176
  collectLeafPaths(value, nextPath, missing);
4294
4177
  continue;
@@ -4315,9 +4198,9 @@ function overrideInto(target, source, owner, path3, missing) {
4315
4198
  function composeLocaleMessagesForPlan(layers) {
4316
4199
  const result = createMessageTree();
4317
4200
  const leafOwners = /* @__PURE__ */ new Map();
4318
- function mergeInto(target, source, owner, path3) {
4201
+ function mergeInto(target, source, owner, path5) {
4319
4202
  for (const [key, value] of Object.entries(source)) {
4320
- const nextPath = path3 ? `${path3}.${key}` : key;
4203
+ const nextPath = path5 ? `${path5}.${key}` : key;
4321
4204
  const hasExisting = Object.prototype.hasOwnProperty.call(target, key);
4322
4205
  const existing = hasExisting ? target[key] : void 0;
4323
4206
  if (typeof value === "string") {
@@ -4861,8 +4744,8 @@ var METHOD_ORDER = {
4861
4744
  function compareNames2(a, b) {
4862
4745
  return a < b ? -1 : a > b ? 1 : 0;
4863
4746
  }
4864
- function endpointPairKey(method, path3) {
4865
- return `${method} ${path3}`;
4747
+ function endpointPairKey(method, path5) {
4748
+ return `${method} ${path5}`;
4866
4749
  }
4867
4750
  function apiPathToRouteFile(apiPath) {
4868
4751
  if (!apiPath.startsWith("/api/")) {
@@ -4876,11 +4759,11 @@ function routeEntryToAppPageFile(nextRelativePath) {
4876
4759
  }
4877
4760
  var APP_ROUTE_FILE_PREFIX = `${APP_PACKAGE_DIR}/src/app/`;
4878
4761
  var API_ROUTE_FILE_SUFFIXES = ["/route.ts", "/route.tsx", "/route.jsx", "/route.js"];
4879
- function appRouteFileToNextRelativePath(path3) {
4880
- const pagePath = appPageFileToNextRelativePath(path3, APP_PACKAGE_DIR);
4762
+ function appRouteFileToNextRelativePath(path5) {
4763
+ const pagePath = appPageFileToNextRelativePath(path5, APP_PACKAGE_DIR);
4881
4764
  if (pagePath != null) return pagePath;
4882
- if (!path3.startsWith(APP_ROUTE_FILE_PREFIX)) return null;
4883
- const relative = path3.slice(APP_ROUTE_FILE_PREFIX.length);
4765
+ if (!path5.startsWith(APP_ROUTE_FILE_PREFIX)) return null;
4766
+ const relative = path5.slice(APP_ROUTE_FILE_PREFIX.length);
4884
4767
  if (!relative.startsWith("api/")) return null;
4885
4768
  const suffix = API_ROUTE_FILE_SUFFIXES.find((candidate) => relative.endsWith(candidate));
4886
4769
  if (!suffix) return null;
@@ -4972,9 +4855,9 @@ function legacyHandlerPropertyMatchesPath(property, method, physicalPath, bindin
4972
4855
  const expectedTokens = knownAliases[`${binding} ${method} ${physicalPath}`] ?? pathTokens;
4973
4856
  return handlerTokens.length === expectedTokens.length && handlerTokens.every((token, index) => token === expectedTokens[index]);
4974
4857
  }
4975
- function parseLegacyApiAdapter(path3, source) {
4976
- const physicalPath = appRouteFileToNextRelativePath(path3);
4977
- if (expectedMarkerKindForPath(path3) !== "endpoint" || physicalPath == null) return null;
4858
+ function parseLegacyApiAdapter(path5, source) {
4859
+ const physicalPath = appRouteFileToNextRelativePath(path5);
4860
+ if (expectedMarkerKindForPath(path5) !== "endpoint" || physicalPath == null) return null;
4978
4861
  const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
4979
4862
  const match = firstLine.match(
4980
4863
  new RegExp(
@@ -4984,7 +4867,7 @@ function parseLegacyApiAdapter(path3, source) {
4984
4867
  if (!match || match[2] !== physicalPath) return null;
4985
4868
  const owner = match[1];
4986
4869
  if (!isValidModuleName(owner)) return null;
4987
- const file = import_typescript4.default.createSourceFile(path3, source, import_typescript4.default.ScriptTarget.Latest, false, import_typescript4.default.ScriptKind.TS);
4870
+ const file = import_typescript4.default.createSourceFile(path5, source, import_typescript4.default.ScriptTarget.Latest, false, import_typescript4.default.ScriptKind.TS);
4988
4871
  if (file.parseDiagnostics.length > 0) {
4989
4872
  return null;
4990
4873
  }
@@ -5026,9 +4909,9 @@ function parseLegacyApiAdapter(path3, source) {
5026
4909
  }
5027
4910
  return handlerBinding != null && methods.size > 0 ? { owner, methods: [...methods] } : null;
5028
4911
  }
5029
- function parseLegacyRouteAdapter(path3, source) {
5030
- const physicalPath = appRouteFileToNextRelativePath(path3);
5031
- if (expectedMarkerKindForPath(path3) !== "route" || physicalPath == null) return null;
4912
+ function parseLegacyRouteAdapter(path5, source) {
4913
+ const physicalPath = appRouteFileToNextRelativePath(path5);
4914
+ if (expectedMarkerKindForPath(path5) !== "route" || physicalPath == null) return null;
5032
4915
  const normalized = source.replace(/\r\n/g, "\n");
5033
4916
  const match = normalized.match(
5034
4917
  new RegExp(
@@ -5040,30 +4923,30 @@ function parseLegacyRouteAdapter(path3, source) {
5040
4923
  const nextRelativePath = match[2];
5041
4924
  return isValidModuleName(owner) && nextRelativePath === physicalPath ? { owner, nextRelativePath } : null;
5042
4925
  }
5043
- function legacyRouteAdapterMatchesStub(path3, source, stub) {
5044
- const legacy = parseLegacyRouteAdapter(path3, source);
4926
+ function legacyRouteAdapterMatchesStub(path5, source, stub) {
4927
+ const legacy = parseLegacyRouteAdapter(path5, source);
5045
4928
  return legacy != null && legacy.owner === stub.owner;
5046
4929
  }
5047
4930
  var API_ROUTE_DIR_PREFIX = `${APP_PACKAGE_DIR}/src/app/api/`;
5048
4931
  var APP_DIR_PREFIX = `${APP_PACKAGE_DIR}/src/app/`;
5049
4932
  var PAGE_FILE_SUFFIXES = ["/page.tsx", "/page.ts", "/page.jsx", "/page.js"];
5050
- function expectedMarkerKindForPath(path3) {
5051
- if (path3 === MODULES_GEN_PATH) return "registry";
5052
- if (path3 === MODULE_CONTRIBUTIONS_GEN_PATH) return "contributions";
5053
- if (path3 === MODULE_DATASTORES_GEN_PATH) return "datastores";
5054
- if (path3 === MODULE_I18N_GEN_PATH) return "i18n";
5055
- if (path3 === MODULE_INIT_SERVER_GEN_PATH) return "init";
5056
- if (path3 === MODULE_SERVER_GEN_PATH) return "server";
5057
- if (path3.startsWith(API_ROUTE_DIR_PREFIX) && path3.endsWith("/route.ts")) return "endpoint";
5058
- if (path3.startsWith(APP_DIR_PREFIX) && !path3.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path3.endsWith(suffix))) {
4933
+ function expectedMarkerKindForPath(path5) {
4934
+ if (path5 === MODULES_GEN_PATH) return "registry";
4935
+ if (path5 === MODULE_CONTRIBUTIONS_GEN_PATH) return "contributions";
4936
+ if (path5 === MODULE_DATASTORES_GEN_PATH) return "datastores";
4937
+ if (path5 === MODULE_I18N_GEN_PATH) return "i18n";
4938
+ if (path5 === MODULE_INIT_SERVER_GEN_PATH) return "init";
4939
+ if (path5 === MODULE_SERVER_GEN_PATH) return "server";
4940
+ if (path5.startsWith(API_ROUTE_DIR_PREFIX) && path5.endsWith("/route.ts")) return "endpoint";
4941
+ if (path5.startsWith(APP_DIR_PREFIX) && !path5.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path5.endsWith(suffix))) {
5059
4942
  return "route";
5060
4943
  }
5061
4944
  return null;
5062
4945
  }
5063
- function markerCongruentWithPath(path3, marker) {
5064
- if (expectedMarkerKindForPath(path3) !== marker.kind) return false;
4946
+ function markerCongruentWithPath(path5, marker) {
4947
+ if (expectedMarkerKindForPath(path5) !== marker.kind) return false;
5065
4948
  if (marker.kind === "route") {
5066
- const derived = appPageFileToNextRelativePath(path3, APP_PACKAGE_DIR);
4949
+ const derived = appPageFileToNextRelativePath(path5, APP_PACKAGE_DIR);
5067
4950
  return derived != null && derived === marker.nextRelativePath;
5068
4951
  }
5069
4952
  return true;
@@ -5334,8 +5217,8 @@ function buildGeneratedModuleRegistry(input) {
5334
5217
  function activeEnhancementPeers(registryModule) {
5335
5218
  return new Set(registryModule.enhancements.filter((e) => e.active).map((e) => e.peer));
5336
5219
  }
5337
- function endpointBucketKey(path3, owner, scope) {
5338
- return `${path3}\0${owner}\0${scopeLabel(scope)}`;
5220
+ function endpointBucketKey(path5, owner, scope) {
5221
+ return `${path5}\0${owner}\0${scopeLabel(scope)}`;
5339
5222
  }
5340
5223
  function planRouteStubs(input) {
5341
5224
  const endpointBuckets = /* @__PURE__ */ new Map();
@@ -5459,7 +5342,7 @@ function planRouteStubs(input) {
5459
5342
  for (const entry of entries) {
5460
5343
  const nextRelativePath = entry.nextRelativePath;
5461
5344
  const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
5462
- const path3 = routeEntryToAppPageFile(nextRelativePath);
5345
+ const path5 = routeEntryToAppPageFile(nextRelativePath);
5463
5346
  const existing = routeDestinationOwners.get(destination);
5464
5347
  if (existing) {
5465
5348
  throw new Error(
@@ -5469,13 +5352,13 @@ function planRouteStubs(input) {
5469
5352
  routeDestinationOwners.set(destination, {
5470
5353
  owner: regMod.name,
5471
5354
  nextRelativePath,
5472
- path: path3
5355
+ path: path5
5473
5356
  });
5474
5357
  const importModule = routeEntryImport(regMod.name, nextRelativePath);
5475
5358
  const analyzed = analyzeRouteEntryPath(nextRelativePath);
5476
5359
  const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
5477
5360
  routeStubs.push({
5478
- path: path3,
5361
+ path: path5,
5479
5362
  owner: regMod.name,
5480
5363
  nextRelativePath,
5481
5364
  importModule,
@@ -5536,16 +5419,16 @@ function planCompositionArtifacts(input) {
5536
5419
  ...stubs.routeStubs.map((stub) => stub.path)
5537
5420
  ]);
5538
5421
  const existingGeneratedPathSet = new Set(input.existingGeneratedPaths);
5539
- const isProvenStaleDeletionCandidate = (path3, content) => {
5540
- if (path3 === MODULES_GEN_PATH) return false;
5541
- if (COMPOSITION_RUNTIME_GEN_PATHS.includes(path3)) return false;
5542
- if (desiredPaths.has(path3)) return false;
5543
- if (!existingGeneratedPathSet.has(path3)) return false;
5544
- const legacy = parseLegacyApiAdapter(path3, content);
5422
+ const isProvenStaleDeletionCandidate = (path5, content) => {
5423
+ if (path5 === MODULES_GEN_PATH) return false;
5424
+ if (COMPOSITION_RUNTIME_GEN_PATHS.includes(path5)) return false;
5425
+ if (desiredPaths.has(path5)) return false;
5426
+ if (!existingGeneratedPathSet.has(path5)) return false;
5427
+ const legacy = parseLegacyApiAdapter(path5, content);
5545
5428
  if (legacy) return true;
5546
- if (parseLegacyRouteAdapter(path3, content)) return true;
5429
+ if (parseLegacyRouteAdapter(path5, content)) return true;
5547
5430
  const marker = parseCompositionArtifactMarker(content);
5548
- return marker != null && markerCongruentWithPath(path3, marker);
5431
+ return marker != null && markerCongruentWithPath(path5, marker);
5549
5432
  };
5550
5433
  const plannedByDestination = /* @__PURE__ */ new Map();
5551
5434
  for (const stub of stubs.routeStubs) {
@@ -5699,13 +5582,13 @@ function planCompositionArtifacts(input) {
5699
5582
  for (const stub of stubs.routeStubs) desired.set(stub.path, stub.content);
5700
5583
  const endpointStubByPath = new Map(stubs.endpointStubs.map((stub) => [stub.path, stub]));
5701
5584
  const routeStubByPath = new Map(stubs.routeStubs.map((stub) => [stub.path, stub]));
5702
- for (const [path3] of desired) {
5703
- const existing = input.existingFiles[path3];
5585
+ for (const [path5] of desired) {
5586
+ const existing = input.existingFiles[path5];
5704
5587
  if (existing == null) continue;
5705
5588
  let marker = parseCompositionArtifactMarker(existing);
5706
- const plannedEndpoint = endpointStubByPath.get(path3);
5707
- const plannedRoute = routeStubByPath.get(path3);
5708
- const legacy = parseLegacyApiAdapter(path3, existing);
5589
+ const plannedEndpoint = endpointStubByPath.get(path5);
5590
+ const plannedRoute = routeStubByPath.get(path5);
5591
+ const legacy = parseLegacyApiAdapter(path5, existing);
5709
5592
  if (legacy && plannedEndpoint) {
5710
5593
  marker = {
5711
5594
  kind: "endpoint",
@@ -5713,66 +5596,66 @@ function planCompositionArtifacts(input) {
5713
5596
  scope: plannedEndpoint.scope
5714
5597
  };
5715
5598
  }
5716
- if (!marker && plannedRoute && legacyRouteAdapterMatchesStub(path3, existing, plannedRoute)) {
5599
+ if (!marker && plannedRoute && legacyRouteAdapterMatchesStub(path5, existing, plannedRoute)) {
5717
5600
  marker = {
5718
5601
  kind: "route",
5719
5602
  owner: plannedRoute.owner,
5720
5603
  nextRelativePath: plannedRoute.nextRelativePath
5721
5604
  };
5722
5605
  }
5723
- const isGenRuntimePath = COMPOSITION_RUNTIME_GEN_PATHS.includes(path3);
5606
+ const isGenRuntimePath = COMPOSITION_RUNTIME_GEN_PATHS.includes(path5);
5724
5607
  if (!marker) {
5725
5608
  return {
5726
5609
  ok: false,
5727
- 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}`,
5728
- path: path3
5610
+ 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}`,
5611
+ path: path5
5729
5612
  };
5730
5613
  }
5731
- const expectedKind = expectedMarkerKindForPath(path3);
5614
+ const expectedKind = expectedMarkerKindForPath(path5);
5732
5615
  if (marker.kind !== expectedKind) {
5733
5616
  return {
5734
5617
  ok: false,
5735
- 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)`,
5736
- path: path3
5618
+ 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)`,
5619
+ path: path5
5737
5620
  };
5738
5621
  }
5739
5622
  if (marker.kind === "endpoint") {
5740
- const stub = endpointStubByPath.get(path3);
5623
+ const stub = endpointStubByPath.get(path5);
5741
5624
  const ownerMatches = stub != null && marker.owner === stub.owner;
5742
5625
  const scopeMatches = stub != null && scopeLabel(marker.scope) === scopeLabel(stub.scope);
5743
5626
  if (!ownerMatches || !scopeMatches) {
5744
5627
  return {
5745
5628
  ok: false,
5746
- 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) : "?"})`,
5747
- path: path3,
5629
+ 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) : "?"})`,
5630
+ path: path5,
5748
5631
  owner: stub?.owner
5749
5632
  };
5750
5633
  }
5751
5634
  } else if (marker.kind === "route") {
5752
- const stub = routeStubByPath.get(path3);
5635
+ const stub = routeStubByPath.get(path5);
5753
5636
  const ownerMatches = stub != null && marker.owner === stub.owner;
5754
5637
  const pathMatches = stub != null && marker.nextRelativePath === stub.nextRelativePath;
5755
5638
  if (!ownerMatches || !pathMatches) {
5756
5639
  return {
5757
5640
  ok: false,
5758
- 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 ?? "?"})`,
5759
- path: path3,
5641
+ 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 ?? "?"})`,
5642
+ path: path5,
5760
5643
  owner: stub?.owner
5761
5644
  };
5762
5645
  }
5763
5646
  }
5764
5647
  }
5765
5648
  const deletes = [];
5766
- for (const path3 of input.existingGeneratedPaths) {
5767
- const existing = input.existingFiles[path3];
5649
+ for (const path5 of input.existingGeneratedPaths) {
5650
+ const existing = input.existingFiles[path5];
5768
5651
  if (existing == null) continue;
5769
- if (!isProvenStaleDeletionCandidate(path3, existing)) {
5652
+ if (!isProvenStaleDeletionCandidate(path5, existing)) {
5770
5653
  continue;
5771
5654
  }
5772
- deletes.push(path3);
5655
+ deletes.push(path5);
5773
5656
  }
5774
5657
  deletes.sort(compareNames2);
5775
- 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));
5658
+ 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));
5776
5659
  return {
5777
5660
  ok: true,
5778
5661
  plan: {
@@ -5801,8 +5684,16 @@ async function checkoutParsesTypedStores(sandbox) {
5801
5684
  try {
5802
5685
  if (!await sandbox.fileExists(APP_PACKAGE_JSON_PATH)) return false;
5803
5686
  const pkg = JSON.parse(await sandbox.readFile(APP_PACKAGE_JSON_PATH));
5804
- const range = pkg.devDependencies?.[COMPOSE_PACKAGE] ?? pkg.dependencies?.[COMPOSE_PACKAGE];
5805
- if (!range) return false;
5687
+ return composeRangeParsesTypedStores(
5688
+ pkg.devDependencies?.[COMPOSE_PACKAGE] ?? pkg.dependencies?.[COMPOSE_PACKAGE]
5689
+ );
5690
+ } catch {
5691
+ return false;
5692
+ }
5693
+ }
5694
+ function composeRangeParsesTypedStores(range) {
5695
+ if (!range) return false;
5696
+ try {
5806
5697
  const lowest = import_semver.default.minVersion(range);
5807
5698
  return lowest != null && import_semver.default.gte(lowest, TYPED_STORES_MIN_COMPOSE_VERSION);
5808
5699
  } catch {
@@ -6034,6 +5925,50 @@ function rejectPseudoModuleEntries(entries, parentDir, label) {
6034
5925
  return dirs;
6035
5926
  }
6036
5927
 
5928
+ // ../../packages/lib/src/shared/module-composition.ts
5929
+ var import_zod7 = require("zod");
5930
+ var creationSourceBlankSchema = import_zod7.z.object({
5931
+ type: import_zod7.z.literal("blank")
5932
+ });
5933
+ var creationSourceBlueprintSchema = import_zod7.z.object({
5934
+ type: import_zod7.z.literal("blueprint"),
5935
+ /** Internal template/fork project id — keep template naming in identifiers. */
5936
+ blueprintProjectId: import_zod7.z.string().uuid(),
5937
+ /**
5938
+ * `null`/absent means every feature Module. `[]` is refused: the platform
5939
+ * serialises it to an empty `NEXT_PUBLIC_STARDECK_ENABLED_MODULES`, which the
5940
+ * enablement parser reads back as "all" (`module-enablement.ts`) — the
5941
+ * opposite of what an empty selection means.
5942
+ */
5943
+ enabledModules: import_zod7.z.array(moduleNameSchema).min(1, "Enable at least one Module").max(100).nullable().optional()
5944
+ });
5945
+ var creationSourceSchema = import_zod7.z.discriminatedUnion("type", [
5946
+ creationSourceBlankSchema,
5947
+ creationSourceBlueprintSchema
5948
+ ]);
5949
+ var moduleInstallHandoffSchema = import_zod7.z.object({
5950
+ moduleName: import_zod7.z.string().min(1),
5951
+ homeProjectId: import_zod7.z.string().uuid(),
5952
+ installedVersion: import_zod7.z.string().min(1),
5953
+ baselineSha: import_zod7.z.string().min(1),
5954
+ mounts: import_zod7.z.array(
5955
+ import_zod7.z.object({
5956
+ group: import_zod7.z.string(),
5957
+ segment: import_zod7.z.string()
5958
+ })
5959
+ ),
5960
+ integration: import_zod7.z.array(import_zod7.z.string()),
5961
+ workflows: import_zod7.z.array(import_zod7.z.string()),
5962
+ prunedEnhancements: import_zod7.z.array(import_zod7.z.string()),
5963
+ restoredEnhancements: import_zod7.z.array(import_zod7.z.string()).optional(),
5964
+ permissions: import_zod7.z.array(
5965
+ import_zod7.z.object({
5966
+ key: import_zod7.z.string().min(1),
5967
+ description: import_zod7.z.string().min(1)
5968
+ })
5969
+ ).optional()
5970
+ });
5971
+
6037
5972
  // ../../packages/lib/src/shared/schema-ops.ts
6038
5973
  var import_zod8 = require("zod");
6039
5974
  var columnSpecSchema = import_zod8.z.object({
@@ -6247,11 +6182,11 @@ function countedCompositionContext(context) {
6247
6182
  counts.sandboxExecs += 1;
6248
6183
  return context.sandbox.exec(...args);
6249
6184
  },
6250
- readFile: (path3) => {
6185
+ readFile: (path5) => {
6251
6186
  counts.sandboxReads += 1;
6252
- return context.sandbox.readFile(path3);
6187
+ return context.sandbox.readFile(path5);
6253
6188
  },
6254
- fileExists: (path3) => context.sandbox.fileExists(path3),
6189
+ fileExists: (path5) => context.sandbox.fileExists(path5),
6255
6190
  fetchRemoteRef: (...args) => context.sandbox.fetchRemoteRef(...args),
6256
6191
  addDependencies: (specs) => context.sandbox.addDependencies(specs)
6257
6192
  }
@@ -6463,14 +6398,14 @@ async function discoverModuleRouteEntries(context, moduleName) {
6463
6398
  }
6464
6399
  return pages.sort((a, b) => a.nextRelativePath.localeCompare(b.nextRelativePath));
6465
6400
  }
6466
- async function writeSandboxFileAtomic(target, path3, content) {
6467
- const dir = dirnamePosix(path3);
6468
- const tmp = `${path3}.tmp.${target.runId}`;
6401
+ async function writeSandboxFileAtomic(target, path5, content) {
6402
+ const dir = dirnamePosix(path5);
6403
+ const tmp = `${path5}.tmp.${target.runId}`;
6469
6404
  const b64 = Buffer.from(content, "utf8").toString("base64");
6470
6405
  const cmd = [
6471
6406
  `mkdir -p ${shellQuote(dir)}`,
6472
6407
  `printf '%s' ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)}`,
6473
- `mv -f ${shellQuote(tmp)} ${shellQuote(path3)}`
6408
+ `mv -f ${shellQuote(tmp)} ${shellQuote(path5)}`
6474
6409
  ].join(" && ");
6475
6410
  const result = await target.sandbox.exec(cmd, { raiseOnError: false });
6476
6411
  if (result.exitCode !== 0) {
@@ -6485,14 +6420,14 @@ async function writeSandboxFileAtomic(target, path3, content) {
6485
6420
  }
6486
6421
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
6487
6422
  throw new Error(
6488
- `[ModuleRail] composition artifacts: failed to write ${redactSecrets(path3)}: ${redactSecrets(detail)}`
6423
+ `[ModuleRail] composition artifacts: failed to write ${redactSecrets(path5)}: ${redactSecrets(detail)}`
6489
6424
  );
6490
6425
  }
6491
6426
  }
6492
6427
  async function collectAbsentWriteParentDirs(context, writePaths) {
6493
6428
  const absent = /* @__PURE__ */ new Set();
6494
- for (const path3 of writePaths) {
6495
- let dir = dirnamePosix(path3);
6429
+ for (const path5 of writePaths) {
6430
+ let dir = dirnamePosix(path5);
6496
6431
  while (dir !== "." && dir !== "") {
6497
6432
  if (absent.has(dir)) {
6498
6433
  dir = dirnamePosix(dir);
@@ -6529,12 +6464,12 @@ async function removeAbsentWriteParentDirsOnRollback(context, dirsDeepestFirst)
6529
6464
  );
6530
6465
  }
6531
6466
  }
6532
- async function deleteSandboxFile(context, path3) {
6533
- const result = await context.sandbox.exec(`rm -f ${shellQuote(path3)}`, { raiseOnError: false });
6467
+ async function deleteSandboxFile(context, path5) {
6468
+ const result = await context.sandbox.exec(`rm -f ${shellQuote(path5)}`, { raiseOnError: false });
6534
6469
  if (result.exitCode !== 0) {
6535
6470
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
6536
6471
  throw new Error(
6537
- `[ModuleRail] composition artifacts: failed to delete ${redactSecrets(path3)}: ${redactSecrets(detail)}`
6472
+ `[ModuleRail] composition artifacts: failed to delete ${redactSecrets(path5)}: ${redactSecrets(detail)}`
6538
6473
  );
6539
6474
  }
6540
6475
  }
@@ -6601,37 +6536,37 @@ async function applyCompositionArtifactMutations(context, plan, existingFiles) {
6601
6536
  await writeSandboxFileAtomic(context, write.path, write.content);
6602
6537
  appliedWrites.push({ path: write.path, previous });
6603
6538
  }
6604
- for (const path3 of plan.deletes) {
6605
- const previous = existingFiles[path3];
6539
+ for (const path5 of plan.deletes) {
6540
+ const previous = existingFiles[path5];
6606
6541
  if (previous == null) continue;
6607
- await deleteSandboxFile(context, path3);
6608
- appliedDeletes.push({ path: path3, previous });
6542
+ await deleteSandboxFile(context, path5);
6543
+ appliedDeletes.push({ path: path5, previous });
6609
6544
  }
6610
6545
  } catch (error) {
6611
6546
  console.log(
6612
6547
  `[ModuleRail] runId=${context.runId} composition artifacts mid-apply failure; rolling back writes=${appliedWrites.length} deletes=${appliedDeletes.length} newDirs=${newlyCreatedParentDirs.length}`
6613
6548
  );
6614
- for (const { path: path3, previous } of [...appliedDeletes].reverse()) {
6549
+ for (const { path: path5, previous } of [...appliedDeletes].reverse()) {
6615
6550
  try {
6616
- await writeSandboxFileAtomic(context, path3, previous);
6551
+ await writeSandboxFileAtomic(context, path5, previous);
6617
6552
  } catch (rollbackError) {
6618
6553
  console.log(
6619
- `[ModuleRail] runId=${context.runId} composition artifacts rollback restore failed for ${redactSecrets(path3)}: ${redactSecrets(
6554
+ `[ModuleRail] runId=${context.runId} composition artifacts rollback restore failed for ${redactSecrets(path5)}: ${redactSecrets(
6620
6555
  rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
6621
6556
  )}`
6622
6557
  );
6623
6558
  }
6624
6559
  }
6625
- for (const { path: path3, previous } of [...appliedWrites].reverse()) {
6560
+ for (const { path: path5, previous } of [...appliedWrites].reverse()) {
6626
6561
  try {
6627
6562
  if (previous == null) {
6628
- await deleteSandboxFile(context, path3);
6563
+ await deleteSandboxFile(context, path5);
6629
6564
  } else {
6630
- await writeSandboxFileAtomic(context, path3, previous);
6565
+ await writeSandboxFileAtomic(context, path5, previous);
6631
6566
  }
6632
6567
  } catch (rollbackError) {
6633
6568
  console.log(
6634
- `[ModuleRail] runId=${context.runId} composition artifacts rollback write failed for ${redactSecrets(path3)}: ${redactSecrets(
6569
+ `[ModuleRail] runId=${context.runId} composition artifacts rollback write failed for ${redactSecrets(path5)}: ${redactSecrets(
6635
6570
  rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
6636
6571
  )}`
6637
6572
  );
@@ -6665,39 +6600,39 @@ async function listExistingGeneratedStubPaths(context) {
6665
6600
  COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN
6666
6601
  );
6667
6602
  const presentPaths = [];
6668
- for (const path3 of candidates) {
6603
+ for (const path5 of candidates) {
6669
6604
  const presence = assertRealCompositionArtifactFilePresence(
6670
- path3,
6671
- kinds.get(path3) ?? "unreadable"
6605
+ path5,
6606
+ kinds.get(path5) ?? "unreadable"
6672
6607
  );
6673
- if (presence === "file") presentPaths.push(path3);
6608
+ if (presence === "file") presentPaths.push(path5);
6674
6609
  }
6675
6610
  let contents;
6676
6611
  try {
6677
6612
  contents = await readFilesMany(context, presentPaths);
6678
6613
  } catch (error) {
6679
- const path3 = readFailurePath(error);
6614
+ const path5 = readFailurePath(error);
6680
6615
  throw new Error(
6681
- `[ModuleRail] composition artifacts: cannot read candidate stub ${redactSecrets(path3)}: ${redactSecrets(
6616
+ `[ModuleRail] composition artifacts: cannot read candidate stub ${redactSecrets(path5)}: ${redactSecrets(
6682
6617
  error instanceof Error ? error.message : String(error)
6683
6618
  )}`
6684
6619
  );
6685
6620
  }
6686
6621
  const owned = [];
6687
- for (const path3 of presentPaths) {
6688
- const content = contents.get(path3);
6622
+ for (const path5 of presentPaths) {
6623
+ const content = contents.get(path5);
6689
6624
  const marker = parseCompositionArtifactMarker(content);
6690
- if (parseLegacyApiAdapter(path3, content) || parseLegacyRouteAdapter(path3, content)) {
6691
- owned.push(path3);
6625
+ if (parseLegacyApiAdapter(path5, content) || parseLegacyRouteAdapter(path5, content)) {
6626
+ owned.push(path5);
6692
6627
  continue;
6693
6628
  }
6694
6629
  if (marker == null) continue;
6695
- if (!markerCongruentWithPath(path3, marker)) {
6630
+ if (!markerCongruentWithPath(path5, marker)) {
6696
6631
  throw new Error(
6697
- `[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)`
6632
+ `[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)`
6698
6633
  );
6699
6634
  }
6700
- owned.push(path3);
6635
+ owned.push(path5);
6701
6636
  }
6702
6637
  return owned;
6703
6638
  }
@@ -6830,7 +6765,7 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6830
6765
  { raiseOnError: false }
6831
6766
  );
6832
6767
  if (result.exitCode !== 0) {
6833
- for (const path3 of chunk) kinds.set(path3, "unreadable");
6768
+ for (const path5 of chunk) kinds.set(path5, "unreadable");
6834
6769
  continue;
6835
6770
  }
6836
6771
  const lines = result.output.split("\n").filter(Boolean);
@@ -6841,31 +6776,31 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6841
6776
  }
6842
6777
  } else {
6843
6778
  for (const line of lines) {
6844
- const [rawKind, path3] = line.split(" ", 2);
6779
+ const [rawKind, path5] = line.split(" ", 2);
6845
6780
  const kind = rawKind?.replace(/^entry-kind:/, "");
6846
- if (path3 && chunk.includes(path3) && CONVENTIONAL_ENTRY_PRESENCE_KINDS.includes(kind ?? "")) {
6847
- kinds.set(path3, kind);
6781
+ if (path5 && chunk.includes(path5) && CONVENTIONAL_ENTRY_PRESENCE_KINDS.includes(kind ?? "")) {
6782
+ kinds.set(path5, kind);
6848
6783
  }
6849
6784
  }
6850
6785
  }
6851
- for (const path3 of chunk) {
6852
- if (!kinds.has(path3)) kinds.set(path3, "unreadable");
6786
+ for (const path5 of chunk) {
6787
+ if (!kinds.has(path5)) kinds.set(path5, "unreadable");
6853
6788
  }
6854
6789
  }
6855
6790
  return kinds;
6856
6791
  }
6857
- async function classifySandboxPathPresenceKind(context, path3, probeToken) {
6858
- return (await classifySandboxPathPresenceKindMany(context, [path3], probeToken)).get(path3);
6792
+ async function classifySandboxPathPresenceKind(context, path5, probeToken) {
6793
+ return (await classifySandboxPathPresenceKindMany(context, [path5], probeToken)).get(path5);
6859
6794
  }
6860
6795
  var LOCALE_JSON_KIND_PROBE_TOKEN = "locale-json-kind";
6861
- function assertRealLocaleJsonFilePresence(path3, presence) {
6796
+ function assertRealLocaleJsonFilePresence(path5, presence) {
6862
6797
  if (presence === "file") return;
6863
6798
  if (presence === "absent") {
6864
- throw new Error(`[ModuleRail] composition artifacts: locale file missing at ${path3}`);
6799
+ throw new Error(`[ModuleRail] composition artifacts: locale file missing at ${path5}`);
6865
6800
  }
6866
- const presenceError = formatConventionalEntryPresenceError(path3, presence);
6801
+ const presenceError = formatConventionalEntryPresenceError(path5, presence);
6867
6802
  throw new Error(
6868
- `[ModuleRail] composition artifacts: ${presenceError ?? `${path3} is not a real locale JSON file`}`
6803
+ `[ModuleRail] composition artifacts: ${presenceError ?? `${path5} is not a real locale JSON file`}`
6869
6804
  );
6870
6805
  }
6871
6806
  function splitTaggedLine(line) {
@@ -6940,11 +6875,11 @@ async function listLocaleBasenamesIfDirMany(context, dirs) {
6940
6875
  }
6941
6876
  async function readFilesMany(context, paths) {
6942
6877
  const sortedPaths = [...paths].sort((a, b) => a.localeCompare(b));
6943
- const settled = await mapConcurrent(sortedPaths, SANDBOX_READ_CONCURRENCY, async (path3) => {
6878
+ const settled = await mapConcurrent(sortedPaths, SANDBOX_READ_CONCURRENCY, async (path5) => {
6944
6879
  try {
6945
- return { path: path3, value: await context.sandbox.readFile(path3) };
6880
+ return { path: path5, value: await context.sandbox.readFile(path5) };
6946
6881
  } catch (error) {
6947
- return { path: path3, error };
6882
+ return { path: path5, error };
6948
6883
  }
6949
6884
  });
6950
6885
  const files = /* @__PURE__ */ new Map();
@@ -6967,58 +6902,58 @@ function readFailurePath(error) {
6967
6902
  var CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN = "conventional-entry-kind";
6968
6903
  var SERVER_REGISTRATIONS_FILE = "server/registrations.ts";
6969
6904
  var COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN = "composition-artifact-kind";
6970
- async function classifyCompositionArtifactPathKind(context, path3) {
6971
- return classifySandboxPathPresenceKind(context, path3, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
6905
+ async function classifyCompositionArtifactPathKind(context, path5) {
6906
+ return classifySandboxPathPresenceKind(context, path5, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
6972
6907
  }
6973
- function assertRealCompositionArtifactFilePresence(path3, presence) {
6908
+ function assertRealCompositionArtifactFilePresence(path5, presence) {
6974
6909
  if (presence === "file") return "file";
6975
6910
  if (presence === "absent") return "absent";
6976
- const presenceError = formatConventionalEntryPresenceError(path3, presence);
6911
+ const presenceError = formatConventionalEntryPresenceError(path5, presence);
6977
6912
  throw new Error(
6978
- `[ModuleRail] composition artifacts: ${presenceError ?? `${path3} is not a real composition artifact file`}`
6913
+ `[ModuleRail] composition artifacts: ${presenceError ?? `${path5} is not a real composition artifact file`}`
6979
6914
  );
6980
6915
  }
6981
- async function readConventionalEntryIfRealFile(path3, kind, presences, files) {
6982
- const presence = presences.get(path3) ?? "unreadable";
6983
- const presenceError = formatConventionalEntryPresenceError(path3, presence);
6916
+ async function readConventionalEntryIfRealFile(path5, kind, presences, files) {
6917
+ const presence = presences.get(path5) ?? "unreadable";
6918
+ const presenceError = formatConventionalEntryPresenceError(path5, presence);
6984
6919
  if (presenceError) {
6985
6920
  throw new Error(`[ModuleRail] composition artifacts: ${presenceError}`);
6986
6921
  }
6987
6922
  if (presence === "absent") {
6988
6923
  return null;
6989
6924
  }
6990
- const source = files.get(path3);
6925
+ const source = files.get(path5);
6991
6926
  switch (kind) {
6992
6927
  case "contributions":
6993
- assertContributionsExport(source, path3);
6994
- assertClientSafeConventionalEntryImports(source, path3, "contributions");
6928
+ assertContributionsExport(source, path5);
6929
+ assertClientSafeConventionalEntryImports(source, path5, "contributions");
6995
6930
  break;
6996
6931
  case "slotCatalogs":
6997
- assertSlotCatalogsExport(source, path3);
6998
- assertClientSafeConventionalEntryImports(source, path3, "slotCatalogs");
6932
+ assertSlotCatalogsExport(source, path5);
6933
+ assertClientSafeConventionalEntryImports(source, path5, "slotCatalogs");
6999
6934
  break;
7000
6935
  case "initializeEnhancement":
7001
- assertInitializeEnhancementExport(source, path3);
6936
+ assertInitializeEnhancementExport(source, path5);
7002
6937
  break;
7003
6938
  }
7004
6939
  return source;
7005
6940
  }
7006
6941
  function hasRealServerRegistrations(modulePath, presences) {
7007
- const path3 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
7008
- return assertRealCompositionArtifactFilePresence(path3, presences.get(path3) ?? "unreadable") === "file";
6942
+ const path5 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
6943
+ return assertRealCompositionArtifactFilePresence(path5, presences.get(path5) ?? "unreadable") === "file";
7009
6944
  }
7010
- async function readJsonObjectFile(path3, files) {
7011
- const raw = files.get(path3);
6945
+ async function readJsonObjectFile(path5, files) {
6946
+ const raw = files.get(path5);
7012
6947
  let parsed;
7013
6948
  try {
7014
6949
  parsed = JSON.parse(raw);
7015
6950
  } catch (error) {
7016
6951
  throw new Error(
7017
- `[ModuleRail] composition artifacts: invalid JSON at ${path3}: ${error instanceof Error ? error.message : String(error)}`
6952
+ `[ModuleRail] composition artifacts: invalid JSON at ${path5}: ${error instanceof Error ? error.message : String(error)}`
7018
6953
  );
7019
6954
  }
7020
6955
  if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
7021
- throw new Error(`[ModuleRail] composition artifacts: ${path3} root must be an object`);
6956
+ throw new Error(`[ModuleRail] composition artifacts: ${path5} root must be an object`);
7022
6957
  }
7023
6958
  return parsed;
7024
6959
  }
@@ -7067,11 +7002,11 @@ async function collectCompositionEntryFacts(context, registry) {
7067
7002
  classifySandboxPathPresenceKindMany(context, localePaths, LOCALE_JSON_KIND_PROBE_TOKEN)
7068
7003
  ]);
7069
7004
  const presentConventionalPaths = [];
7070
- for (const path3 of conventionalPaths) {
7071
- const presence = conventionalPresences.get(path3) ?? "unreadable";
7072
- if (presence === "file") presentConventionalPaths.push(path3);
7005
+ for (const path5 of conventionalPaths) {
7006
+ const presence = conventionalPresences.get(path5) ?? "unreadable";
7007
+ if (presence === "file") presentConventionalPaths.push(path5);
7073
7008
  }
7074
- const presentLocalePaths = localePaths.filter((path3) => localePresences.get(path3) === "file");
7009
+ const presentLocalePaths = localePaths.filter((path5) => localePresences.get(path5) === "file");
7075
7010
  const files = await readFilesMany(context, [...presentConventionalPaths, ...presentLocalePaths]);
7076
7011
  const substrateLocales = [];
7077
7012
  for (const locale of substrateNames) {
@@ -7169,8 +7104,8 @@ async function collectCompositionEntryFacts(context, registry) {
7169
7104
  });
7170
7105
  if (rootLocales.length > 0) {
7171
7106
  for (const locale of rootLocales) {
7172
- const path3 = `${modulePath}/i18n/${locale}.json`;
7173
- assertRealLocaleJsonFilePresence(path3, localePresences.get(path3) ?? "unreadable");
7107
+ const path5 = `${modulePath}/i18n/${locale}.json`;
7108
+ assertRealLocaleJsonFilePresence(path5, localePresences.get(path5) ?? "unreadable");
7174
7109
  }
7175
7110
  const enMessages = await readJsonObjectFile(`${modulePath}/i18n/en.json`, files);
7176
7111
  const localeMessages = { en: enMessages };
@@ -7194,8 +7129,8 @@ async function collectCompositionEntryFacts(context, registry) {
7194
7129
  if (!enh.active || enh.locales.length === 0) continue;
7195
7130
  const peerDir = `${modulePath}/enhancements/${enh.peer}`;
7196
7131
  for (const locale of enh.locales) {
7197
- const path3 = `${peerDir}/i18n/${locale}.json`;
7198
- assertRealLocaleJsonFilePresence(path3, localePresences.get(path3) ?? "unreadable");
7132
+ const path5 = `${peerDir}/i18n/${locale}.json`;
7133
+ assertRealLocaleJsonFilePresence(path5, localePresences.get(path5) ?? "unreadable");
7199
7134
  }
7200
7135
  const enMessages = await readJsonObjectFile(`${peerDir}/i18n/en.json`, files);
7201
7136
  const localeMessages = { en: enMessages };
@@ -7396,16 +7331,16 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7396
7331
  for (const stub of probePlan.plan.routeStubs) candidatePaths.add(stub.path);
7397
7332
  const existingFiles = {};
7398
7333
  const remainingCandidatePaths = [];
7399
- for (const path3 of [...candidatePaths].sort((a, b) => a.localeCompare(b))) {
7400
- if (Object.prototype.hasOwnProperty.call(existingAppPages, path3)) {
7401
- existingFiles[path3] = existingAppPages[path3];
7334
+ for (const path5 of [...candidatePaths].sort((a, b) => a.localeCompare(b))) {
7335
+ if (Object.prototype.hasOwnProperty.call(existingAppPages, path5)) {
7336
+ existingFiles[path5] = existingAppPages[path5];
7402
7337
  continue;
7403
7338
  }
7404
- if (Object.prototype.hasOwnProperty.call(existingAppRoutes, path3)) {
7405
- existingFiles[path3] = existingAppRoutes[path3];
7339
+ if (Object.prototype.hasOwnProperty.call(existingAppRoutes, path5)) {
7340
+ existingFiles[path5] = existingAppRoutes[path5];
7406
7341
  continue;
7407
7342
  }
7408
- remainingCandidatePaths.push(path3);
7343
+ remainingCandidatePaths.push(path5);
7409
7344
  }
7410
7345
  const candidateKinds = await classifySandboxPathPresenceKindMany(
7411
7346
  context,
@@ -7413,12 +7348,12 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7413
7348
  COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN
7414
7349
  );
7415
7350
  const presentCandidatePaths = [];
7416
- for (const path3 of remainingCandidatePaths) {
7351
+ for (const path5 of remainingCandidatePaths) {
7417
7352
  const presence = assertRealCompositionArtifactFilePresence(
7418
- path3,
7419
- candidateKinds.get(path3) ?? "unreadable"
7353
+ path5,
7354
+ candidateKinds.get(path5) ?? "unreadable"
7420
7355
  );
7421
- if (presence === "file") presentCandidatePaths.push(path3);
7356
+ if (presence === "file") presentCandidatePaths.push(path5);
7422
7357
  }
7423
7358
  try {
7424
7359
  Object.assign(
@@ -7426,9 +7361,9 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7426
7361
  Object.fromEntries(await readFilesMany(context, presentCandidatePaths))
7427
7362
  );
7428
7363
  } catch (error) {
7429
- const path3 = readFailurePath(error);
7364
+ const path5 = readFailurePath(error);
7430
7365
  throw new Error(
7431
- `[ModuleRail] composition artifacts: cannot read ${redactSecrets(path3)} during preflight: ${redactSecrets(
7366
+ `[ModuleRail] composition artifacts: cannot read ${redactSecrets(path5)} during preflight: ${redactSecrets(
7432
7367
  error instanceof Error ? error.message : String(error)
7433
7368
  )}`
7434
7369
  );
@@ -7497,14 +7432,142 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7497
7432
  var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
7498
7433
  var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
7499
7434
 
7435
+ // src/index.ts
7436
+ var import_typescript6 = __toESM(require("typescript"));
7437
+
7438
+ // src/module-boundaries.ts
7439
+ var import_node_fs2 = __toESM(require("fs"));
7440
+ var import_node_path2 = __toESM(require("path"));
7441
+ var import_typescript5 = __toESM(require("typescript"));
7442
+ var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
7443
+ var RESOLVE_SUFFIXES = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
7444
+ var DATA_FILE = /\.(?:sql|json)$/;
7445
+ function dataPathsIn(fileName, source) {
7446
+ if (!/\.(?:sql|json)["'`]/.test(source)) return [];
7447
+ const paths = [];
7448
+ const visit = (node) => {
7449
+ if ((import_typescript5.default.isStringLiteralLike(node) || import_typescript5.default.isTemplateTail(node)) && DATA_FILE.test(node.text)) {
7450
+ paths.push(node.text);
7451
+ }
7452
+ import_typescript5.default.forEachChild(node, visit);
7453
+ };
7454
+ visit(import_typescript5.default.createSourceFile(fileName, source, import_typescript5.default.ScriptTarget.Latest, false));
7455
+ return paths;
7456
+ }
7457
+ function scanModule(modulesDir, module2) {
7458
+ const scanned = /* @__PURE__ */ new Map();
7459
+ const moduleDir = import_node_path2.default.join(modulesDir, module2);
7460
+ if (!import_node_fs2.default.existsSync(moduleDir)) return scanned;
7461
+ const files = import_node_fs2.default.readdirSync(moduleDir, { recursive: true, encoding: "utf8" }).filter((relative) => SOURCE_FILE.test(relative) && !relative.includes("node_modules"));
7462
+ for (const relative of files) {
7463
+ const absolute = import_node_path2.default.join(moduleDir, relative);
7464
+ if (!import_node_fs2.default.statSync(absolute).isFile()) continue;
7465
+ const source = import_node_fs2.default.readFileSync(absolute, "utf8");
7466
+ const specifiers = import_typescript5.default.preProcessFile(source, true, true).importedFiles.map((f) => f.fileName);
7467
+ const dataPaths = dataPathsIn(absolute, source);
7468
+ const targets = [...specifiers, ...dataPaths].flatMap((spec) => {
7469
+ if (spec.startsWith("@/modules/"))
7470
+ return [import_node_path2.default.join(modulesDir, spec.slice("@/modules/".length))];
7471
+ if (spec.startsWith("./") || spec.startsWith("../")) {
7472
+ return [import_node_path2.default.resolve(import_node_path2.default.dirname(absolute), spec)];
7473
+ }
7474
+ return [];
7475
+ });
7476
+ scanned.set(absolute, {
7477
+ module: module2,
7478
+ relative: relative.split(import_node_path2.default.sep).join("/"),
7479
+ targets,
7480
+ dataPaths
7481
+ });
7482
+ }
7483
+ return scanned;
7484
+ }
7485
+ function findModuleBoundaryViolations(modulesDir, peersByModule, { pruning = false } = {}) {
7486
+ const files = /* @__PURE__ */ new Map();
7487
+ for (const [module2, peers] of Object.entries(peersByModule)) {
7488
+ if (peers.length === 0) continue;
7489
+ for (const [absolute, file] of scanModule(modulesDir, module2)) files.set(absolute, file);
7490
+ }
7491
+ const exempt = (file, peer) => (pruning ? peersByModule[file.module] ?? [] : [peer]).some(
7492
+ (q) => file.relative.startsWith(`enhancements/${q}/`)
7493
+ );
7494
+ const goneWith = (target, peer) => {
7495
+ const relative = import_node_path2.default.relative(modulesDir, target).split(import_node_path2.default.sep).join("/");
7496
+ return relative === peer || relative.startsWith(`${peer}/`) || new RegExp(`^[^/]+/enhancements/${peer}(/|$)`).test(relative);
7497
+ };
7498
+ const tainted = /* @__PURE__ */ new Map();
7499
+ const queue = [];
7500
+ const taint = (absolute, peer) => {
7501
+ const peers = tainted.get(absolute) ?? /* @__PURE__ */ new Set();
7502
+ if (peers.has(peer)) return;
7503
+ peers.add(peer);
7504
+ tainted.set(absolute, peers);
7505
+ queue.push([absolute, peer]);
7506
+ };
7507
+ const importers = /* @__PURE__ */ new Map();
7508
+ for (const [absolute, file] of files) {
7509
+ for (const target of file.targets) {
7510
+ const hit = RESOLVE_SUFFIXES.map((suffix) => target + suffix).find((c) => files.has(c));
7511
+ if (hit) importers.set(hit, [...importers.get(hit) ?? [], absolute]);
7512
+ }
7513
+ for (const peer of peersByModule[file.module] ?? []) {
7514
+ if (exempt(file, peer)) continue;
7515
+ if (file.targets.some((target) => goneWith(target, peer)) || file.dataPaths.some(
7516
+ (p) => p.startsWith(`modules/${peer}/`) || p.includes(`/modules/${peer}/`)
7517
+ )) {
7518
+ taint(absolute, peer);
7519
+ }
7520
+ }
7521
+ }
7522
+ for (let next = queue.shift(); next; next = queue.shift()) {
7523
+ const [target, peer] = next;
7524
+ for (const importer of importers.get(target) ?? []) {
7525
+ const file = files.get(importer);
7526
+ if ((peersByModule[file.module] ?? []).includes(peer) && !exempt(file, peer)) {
7527
+ taint(importer, peer);
7528
+ }
7529
+ }
7530
+ }
7531
+ const violations = [];
7532
+ for (const [absolute, peers] of tainted) {
7533
+ const file = files.get(absolute);
7534
+ for (const peer of peers) {
7535
+ violations.push({ file: `${file.module}/${file.relative}`, module: file.module, peer });
7536
+ }
7537
+ }
7538
+ return violations.sort((a, b) => a.file.localeCompare(b.file) || a.peer.localeCompare(b.peer));
7539
+ }
7540
+ function findOptionalPeerViolations(modulesDir) {
7541
+ if (!import_node_fs2.default.existsSync(modulesDir)) return [];
7542
+ const manifests = {};
7543
+ for (const name of import_node_fs2.default.readdirSync(modulesDir)) {
7544
+ const manifestPath = import_node_path2.default.join(modulesDir, name, "module.json");
7545
+ if (!import_node_fs2.default.existsSync(manifestPath)) continue;
7546
+ manifests[name] = JSON.parse(import_node_fs2.default.readFileSync(manifestPath, "utf8"));
7547
+ }
7548
+ const features = Object.keys(manifests).filter((name) => manifests[name].kind === "feature");
7549
+ const peersByModule = {};
7550
+ for (const [name, manifest] of Object.entries(manifests)) {
7551
+ const uses = new Set(manifest.uses ?? []);
7552
+ peersByModule[name] = features.filter((peer) => peer !== name && !uses.has(peer));
7553
+ }
7554
+ return findModuleBoundaryViolations(modulesDir, peersByModule);
7555
+ }
7556
+ function formatBoundaryViolations(modulesDirLabel, violations) {
7557
+ const files = new Set(violations.map((v) => v.file)).size;
7558
+ return `${files} Module file(s) import a peer Module that may be absent (not in the Module's \`uses\`, or excluded by --enabled).
7559
+ \`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).
7560
+ ` + violations.map((v) => `${modulesDirLabel}/${v.file}: ${v.peer}`).join("\n");
7561
+ }
7562
+
7500
7563
  // src/index.ts
7501
7564
  var MODULES_DIR2 = MODULES_SANDBOX_DIR;
7502
7565
  function readComposeInputs(cwd) {
7503
- const file = import_node_path2.default.join(cwd, COMPOSE_INPUTS_PATH);
7504
- if (!import_node_fs2.default.existsSync(file)) return { connectedStores: [], installs: {} };
7566
+ const file = import_node_path3.default.join(cwd, COMPOSE_INPUTS_PATH);
7567
+ if (!import_node_fs3.default.existsSync(file)) return { connectedStores: [], installs: {} };
7505
7568
  let raw;
7506
7569
  try {
7507
- raw = JSON.parse(import_node_fs2.default.readFileSync(file, "utf8"));
7570
+ raw = JSON.parse(import_node_fs3.default.readFileSync(file, "utf8"));
7508
7571
  } catch (error) {
7509
7572
  throw new Error(
7510
7573
  `[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
@@ -7546,18 +7609,18 @@ function stubGitignoreEntries(artifactPaths) {
7546
7609
  function writeStubGitignore(cwd, artifactPaths) {
7547
7610
  const entries = stubGitignoreEntries(artifactPaths);
7548
7611
  const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
7549
- const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
7550
- const existing = import_node_fs2.default.existsSync(file) ? import_node_fs2.default.readFileSync(file, "utf8") : null;
7612
+ const file = import_node_path3.default.join(cwd, STUB_GITIGNORE_PATH);
7613
+ const existing = import_node_fs3.default.existsSync(file) ? import_node_fs3.default.readFileSync(file, "utf8") : null;
7551
7614
  if (existing !== content) {
7552
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
7553
- import_node_fs2.default.writeFileSync(file, content);
7615
+ import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(file), { recursive: true });
7616
+ import_node_fs3.default.writeFileSync(file, content);
7554
7617
  }
7555
7618
  return entries.length;
7556
7619
  }
7557
7620
  function assertStubGitignoreOwned(cwd) {
7558
- const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
7559
- if (!import_node_fs2.default.existsSync(file)) return;
7560
- const firstLine = import_node_fs2.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
7621
+ const file = import_node_path3.default.join(cwd, STUB_GITIGNORE_PATH);
7622
+ if (!import_node_fs3.default.existsSync(file)) return;
7623
+ const firstLine = import_node_fs3.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
7561
7624
  if (firstLine !== STUB_GITIGNORE_HEADER) {
7562
7625
  throw new Error(
7563
7626
  `[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
@@ -7565,8 +7628,8 @@ function assertStubGitignoreOwned(cwd) {
7565
7628
  }
7566
7629
  }
7567
7630
  function assertAppRoot(cwd) {
7568
- const appPath = import_node_path2.default.join(cwd, APP_PACKAGE_JSON);
7569
- if (!import_node_fs2.default.existsSync(appPath)) {
7631
+ const appPath = import_node_path3.default.join(cwd, APP_PACKAGE_JSON);
7632
+ if (!import_node_fs3.default.existsSync(appPath)) {
7570
7633
  throw new Error(
7571
7634
  `[compose] ${appPath} not found \u2014 run this from the repo root of a Stardeck app`
7572
7635
  );
@@ -7612,9 +7675,9 @@ ${porcelain.trim()}`
7612
7675
  }
7613
7676
  var APP_SRC_DIR = "apps/web/src";
7614
7677
  function walkSources(dir, skip, out) {
7615
- if (!import_node_fs2.default.existsSync(dir)) return;
7616
- for (const entry of import_node_fs2.default.readdirSync(dir, { withFileTypes: true })) {
7617
- const full = import_node_path2.default.join(dir, entry.name);
7678
+ if (!import_node_fs3.default.existsSync(dir)) return;
7679
+ for (const entry of import_node_fs3.default.readdirSync(dir, { withFileTypes: true })) {
7680
+ const full = import_node_path3.default.join(dir, entry.name);
7618
7681
  if (full === skip) continue;
7619
7682
  if (entry.isDirectory()) walkSources(full, skip, out);
7620
7683
  else if (/\.[cm]?[jt]sx?$/.test(entry.name)) out.push(full);
@@ -7623,13 +7686,13 @@ function walkSources(dir, skip, out) {
7623
7686
  function assertNoAppLayerImporters(cwd, excluded) {
7624
7687
  if (excluded.length === 0) return;
7625
7688
  const files = [];
7626
- walkSources(import_node_path2.default.join(cwd, APP_SRC_DIR), import_node_path2.default.join(cwd, MODULES_DIR2), files);
7689
+ walkSources(import_node_path3.default.join(cwd, APP_SRC_DIR), import_node_path3.default.join(cwd, MODULES_DIR2), files);
7627
7690
  const offenders = [];
7628
7691
  for (const file of files) {
7629
- const source = import_node_fs2.default.readFileSync(file, "utf8");
7692
+ const source = import_node_fs3.default.readFileSync(file, "utf8");
7630
7693
  for (const name of excluded) {
7631
7694
  if (new RegExp(`@/modules/${name}(?=["'\`/])`).test(source)) {
7632
- offenders.push(`${import_node_path2.default.relative(cwd, file)}: ${name}`);
7695
+ offenders.push(`${import_node_path3.default.relative(cwd, file)}: ${name}`);
7633
7696
  }
7634
7697
  }
7635
7698
  }
@@ -7641,9 +7704,44 @@ function assertNoAppLayerImporters(cwd, excluded) {
7641
7704
  );
7642
7705
  }
7643
7706
  }
7707
+ var TSCONFIG_PATH = "apps/web/tsconfig.json";
7708
+ var BUILD_TSCONFIG_PATH = "apps/web/tsconfig.build.gen.json";
7709
+ var BUILD_TSCONFIG_HEADER = "// Generated by stardeck-compose \u2014 the tsconfig `next build` type-checks. Do not edit or commit.";
7710
+ function writeBuildTsconfig(cwd, excluded) {
7711
+ const baseFile = import_node_path3.default.join(cwd, TSCONFIG_PATH);
7712
+ if (!import_node_fs3.default.existsSync(import_node_path3.default.join(cwd, MODULES_DIR2)) || !import_node_fs3.default.existsSync(baseFile)) return;
7713
+ const base = import_typescript6.default.readConfigFile(baseFile, import_typescript6.default.sys.readFile);
7714
+ if (base.error) {
7715
+ throw new Error(
7716
+ `[compose] cannot read ${TSCONFIG_PATH}: ${import_typescript6.default.flattenDiagnosticMessageText(base.error.messageText, "\n")}`
7717
+ );
7718
+ }
7719
+ const baseExclude = base.config?.exclude ?? ["node_modules"];
7720
+ const modulesDir = import_node_path3.default.posix.relative("apps/web", MODULES_DIR2);
7721
+ const config = {
7722
+ extends: "./tsconfig.json",
7723
+ exclude: [
7724
+ ...baseExclude,
7725
+ ...excluded.flatMap((name) => [
7726
+ `${modulesDir}/${name}`,
7727
+ `${modulesDir}/*/enhancements/${name}`
7728
+ ])
7729
+ ]
7730
+ };
7731
+ const content = `${BUILD_TSCONFIG_HEADER}
7732
+ ${JSON.stringify(config, null, 2)}
7733
+ `;
7734
+ const file = import_node_path3.default.join(cwd, BUILD_TSCONFIG_PATH);
7735
+ if (!import_node_fs3.default.existsSync(file) || import_node_fs3.default.readFileSync(file, "utf8") !== content) {
7736
+ import_node_fs3.default.writeFileSync(file, content);
7737
+ }
7738
+ console.log(
7739
+ `[compose] ${BUILD_TSCONFIG_PATH} excludes ${excluded.length} Module(s): ${excluded.join(",") || "none"}`
7740
+ );
7741
+ }
7644
7742
  function removeDir(absolute) {
7645
- if (!import_node_fs2.default.existsSync(absolute)) return false;
7646
- import_node_fs2.default.rmSync(absolute, { recursive: true, force: true });
7743
+ if (!import_node_fs3.default.existsSync(absolute)) return false;
7744
+ import_node_fs3.default.rmSync(absolute, { recursive: true, force: true });
7647
7745
  return true;
7648
7746
  }
7649
7747
  async function compose(options) {
@@ -7670,6 +7768,16 @@ async function compose(options) {
7670
7768
  dryRun: true
7671
7769
  });
7672
7770
  assertNoAppLayerImporters(options.cwd, preview.excludedModules);
7771
+ const violations = findModuleBoundaryViolations(
7772
+ import_node_path3.default.join(options.cwd, MODULES_DIR2),
7773
+ Object.fromEntries(preview.composedModules.map((name) => [name, preview.excludedModules])),
7774
+ { pruning: true }
7775
+ );
7776
+ if (violations.length > 0) {
7777
+ throw new Error(
7778
+ `[compose] refusing --prune: ${formatBoundaryViolations(MODULES_DIR2, violations)}`
7779
+ );
7780
+ }
7673
7781
  }
7674
7782
  const result = await reconcileCompositionArtifacts(context, {
7675
7783
  enabledModulesRaw: options.enabled,
@@ -7678,14 +7786,15 @@ async function compose(options) {
7678
7786
  skipComposeInputs: true
7679
7787
  });
7680
7788
  const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
7789
+ writeBuildTsconfig(options.cwd, result.excludedModules);
7681
7790
  let pruned = 0;
7682
7791
  if (options.prune) {
7683
7792
  for (const name of result.excludedModules) {
7684
- if (removeDir(import_node_path2.default.join(options.cwd, MODULES_DIR2, name))) pruned++;
7793
+ if (removeDir(import_node_path3.default.join(options.cwd, MODULES_DIR2, name))) pruned++;
7685
7794
  }
7686
7795
  for (const owner of result.composedModules) {
7687
7796
  for (const peer of result.excludedModules) {
7688
- if (removeDir(import_node_path2.default.join(options.cwd, MODULES_DIR2, owner, "enhancements", peer))) pruned++;
7797
+ if (removeDir(import_node_path3.default.join(options.cwd, MODULES_DIR2, owner, "enhancements", peer))) pruned++;
7689
7798
  }
7690
7799
  }
7691
7800
  }
@@ -7701,15 +7810,28 @@ async function compose(options) {
7701
7810
 
7702
7811
  // src/cli.ts
7703
7812
  async function main() {
7704
- assertSupportedTypescript(import_typescript5.default.versionMajorMinor);
7813
+ assertSupportedTypescript(import_typescript7.default.versionMajorMinor);
7705
7814
  const { values } = (0, import_node_util.parseArgs)({
7706
7815
  options: {
7707
7816
  enabled: { type: "string" },
7708
7817
  prune: { type: "boolean", default: false },
7709
- cwd: { type: "string" }
7818
+ cwd: { type: "string" },
7819
+ "check-boundaries": { type: "boolean", default: false }
7710
7820
  },
7711
7821
  strict: true
7712
7822
  });
7823
+ if (values["check-boundaries"]) {
7824
+ const violations = findOptionalPeerViolations(
7825
+ import_node_path4.default.join(values.cwd ?? process.cwd(), MODULES_SANDBOX_DIR)
7826
+ );
7827
+ if (violations.length > 0) {
7828
+ throw new Error(
7829
+ `[compose] --check-boundaries: ${formatBoundaryViolations(MODULES_SANDBOX_DIR, violations)}`
7830
+ );
7831
+ }
7832
+ console.log("[compose] --check-boundaries: no Module file reaches a peer outside its `uses`");
7833
+ return;
7834
+ }
7713
7835
  const enabled = values.enabled ?? process.env[ENABLED_MODULES_ENV];
7714
7836
  if (values.prune && !enabled) {
7715
7837
  throw new Error(