@stardeck-customer-apps/compose 0.14.0 → 0.15.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
@@ -1145,16 +1145,16 @@ var require_range = __commonJS({
1145
1145
  };
1146
1146
  var replaceTilde = (comp, options) => {
1147
1147
  const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE];
1148
- const z10 = options.includePrerelease ? "-0" : "";
1148
+ const z11 = options.includePrerelease ? "-0" : "";
1149
1149
  return comp.replace(r, (_, M, m, p, pr) => {
1150
1150
  debug("tilde", comp, _, M, m, p, pr);
1151
1151
  let ret;
1152
1152
  if (isX(M)) {
1153
1153
  ret = "";
1154
1154
  } else if (isX(m)) {
1155
- ret = `>=${M}.0.0${z10} <${+M + 1}.0.0-0`;
1155
+ ret = `>=${M}.0.0${z11} <${+M + 1}.0.0-0`;
1156
1156
  } else if (isX(p)) {
1157
- ret = `>=${M}.${m}.0${z10} <${M}.${+m + 1}.0-0`;
1157
+ ret = `>=${M}.${m}.0${z11} <${M}.${+m + 1}.0-0`;
1158
1158
  } else if (pr) {
1159
1159
  debug("replaceTilde pr", pr);
1160
1160
  ret = `>=${M}.${m}.${p}-${pr} <${M}.${+m + 1}.0-0`;
@@ -1171,19 +1171,19 @@ var require_range = __commonJS({
1171
1171
  var replaceCaret = (comp, options) => {
1172
1172
  debug("caret", comp, options);
1173
1173
  const r = options.loose ? re[t.CARETLOOSE] : re[t.CARET];
1174
- const z10 = options.includePrerelease ? "-0" : "";
1174
+ const z11 = options.includePrerelease ? "-0" : "";
1175
1175
  return comp.replace(r, (_, M, m, p, pr) => {
1176
1176
  debug("caret", comp, _, M, m, p, pr);
1177
1177
  let ret;
1178
1178
  if (isX(M)) {
1179
1179
  ret = "";
1180
1180
  } else if (isX(m)) {
1181
- ret = `>=${M}.0.0${z10} <${+M + 1}.0.0-0`;
1181
+ ret = `>=${M}.0.0${z11} <${+M + 1}.0.0-0`;
1182
1182
  } else if (isX(p)) {
1183
1183
  if (M === "0") {
1184
- ret = `>=${M}.${m}.0${z10} <${M}.${+m + 1}.0-0`;
1184
+ ret = `>=${M}.${m}.0${z11} <${M}.${+m + 1}.0-0`;
1185
1185
  } else {
1186
- ret = `>=${M}.${m}.0${z10} <${+M + 1}.0.0-0`;
1186
+ ret = `>=${M}.${m}.0${z11} <${+M + 1}.0.0-0`;
1187
1187
  }
1188
1188
  } else if (pr) {
1189
1189
  debug("replaceCaret pr", pr);
@@ -2021,8 +2021,13 @@ var require_semver2 = __commonJS({
2021
2021
  });
2022
2022
 
2023
2023
  // src/cli.ts
2024
+ var import_node_child_process3 = require("child_process");
2024
2025
  var import_node_util = require("util");
2025
2026
 
2027
+ // ../../packages/lib/src/server/module-rail/ownership-guard.ts
2028
+ var import_node_fs = __toESM(require("fs"));
2029
+ var import_node_path = __toESM(require("path"));
2030
+
2026
2031
  // ../../packages/module-contract/src/module-manifest.ts
2027
2032
  var import_zod = require("zod");
2028
2033
 
@@ -2053,6 +2058,13 @@ function validateSlug(slug) {
2053
2058
  var MODULES_DIR = "src/modules";
2054
2059
  var APP_PACKAGE_DIR = "apps/web";
2055
2060
  var MODULES_SANDBOX_DIR = `${APP_PACKAGE_DIR}/${MODULES_DIR}`;
2061
+ var MODULE_MANIFEST_FILENAME = "module.json";
2062
+ var MODULE_UI_SUBPATHS = [
2063
+ "components/",
2064
+ "routes/",
2065
+ "enhancements/*/components/",
2066
+ "enhancements/*/routes/"
2067
+ ];
2056
2068
  var KEBAB_SLUG_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2057
2069
  var SNAKE_TABLE_REGEX = /^[a-z][a-z0-9_]*$/;
2058
2070
  var ROUTE_GROUP_REGEX = /^\([a-z0-9-]+\)$/;
@@ -2066,23 +2078,23 @@ function isValidModuleName(value) {
2066
2078
  var projectSlugSchema = import_zod.z.string().refine((value) => validateSlug(value).valid, "must be a valid project slug");
2067
2079
  var moduleKindSchema = import_zod.z.enum(["feature", "library", "contract"]);
2068
2080
  var endpointMethodSchema = import_zod.z.enum(ENDPOINT_METHODS);
2069
- function endpointPathValidationError(path5) {
2070
- if (!path5.startsWith("/api/")) {
2081
+ function endpointPathValidationError(path6) {
2082
+ if (!path6.startsWith("/api/")) {
2071
2083
  return 'must begin with "/api/"';
2072
2084
  }
2073
- if (path5.endsWith("/")) {
2085
+ if (path6.endsWith("/")) {
2074
2086
  return "must not end with a trailing slash";
2075
2087
  }
2076
- if (path5.includes("?") || path5.includes("#")) {
2088
+ if (path6.includes("?") || path6.includes("#")) {
2077
2089
  return "must not include query or hash";
2078
2090
  }
2079
- if (path5.includes("\\")) {
2091
+ if (path6.includes("\\")) {
2080
2092
  return "must not include backslashes";
2081
2093
  }
2082
- if (path5.includes("%")) {
2094
+ if (path6.includes("%")) {
2083
2095
  return "must not include percent-encoded segments";
2084
2096
  }
2085
- const parts = path5.split("/");
2097
+ const parts = path6.split("/");
2086
2098
  if (parts.length < 3 || parts[0] !== "" || parts[1] !== "api") {
2087
2099
  return 'must begin with "/api/"';
2088
2100
  }
@@ -2132,12 +2144,12 @@ function endpointPathValidationError(path5) {
2132
2144
  }
2133
2145
  return null;
2134
2146
  }
2135
- function normalizeEndpointPathForCollision(path5) {
2136
- const error = endpointPathValidationError(path5);
2147
+ function normalizeEndpointPathForCollision(path6) {
2148
+ const error = endpointPathValidationError(path6);
2137
2149
  if (error != null) {
2138
2150
  throw new Error(`invalid endpoint path: ${error}`);
2139
2151
  }
2140
- return path5.split("/").map((segment, index) => {
2152
+ return path6.split("/").map((segment, index) => {
2141
2153
  if (index < 2) {
2142
2154
  return segment;
2143
2155
  }
@@ -2150,8 +2162,8 @@ function normalizeEndpointPathForCollision(path5) {
2150
2162
  return segment;
2151
2163
  }).join("/");
2152
2164
  }
2153
- var endpointPathSchema = import_zod.z.string().superRefine((path5, ctx) => {
2154
- const error = endpointPathValidationError(path5);
2165
+ var endpointPathSchema = import_zod.z.string().superRefine((path6, ctx) => {
2166
+ const error = endpointPathValidationError(path6);
2155
2167
  if (error != null) {
2156
2168
  ctx.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: error });
2157
2169
  }
@@ -2259,10 +2271,12 @@ var modulePermissionClaimSchema = import_zod.z.object({
2259
2271
  /** Human label for the deployment auth config row. */
2260
2272
  description: descriptionSchema,
2261
2273
  /**
2262
- * The default organization roles that hold this key when the platform
2263
- * first registers it on an app (a Blueprint fork, a Module install). Admin
2264
- * holds every key without being listed; External Viewer never holds one.
2265
- * Changing a role's grants afterwards is the organization's business.
2274
+ * The standard organization roles that hold this key. The platform sets them on every app
2275
+ * (a Blueprint fork, a Module install or uninstall, every Blueprint update), so this list is
2276
+ * the truth: a role added here gains the key, a role removed loses it, and nobody can edit a
2277
+ * standard role's grants on a Module key. Admin holds every key without being listed;
2278
+ * External Viewer never holds one. An organization that wants different access duplicates the
2279
+ * standard role and edits the copy.
2266
2280
  */
2267
2281
  roles: import_zod.z.array(import_zod.z.enum(MODULE_PERMISSION_DEFAULT_ROLES)).optional()
2268
2282
  }).strict();
@@ -2412,16 +2426,16 @@ var moduleManifestSchema = import_zod.z.object({
2412
2426
  }
2413
2427
  }
2414
2428
  const seenWorkflows = /* @__PURE__ */ new Map();
2415
- const noteWorkflow = (name, path5) => {
2429
+ const noteWorkflow = (name, path6) => {
2416
2430
  const existing = seenWorkflows.get(name);
2417
2431
  if (existing != null) {
2418
2432
  ctx.addIssue({
2419
2433
  code: import_zod.z.ZodIssueCode.custom,
2420
2434
  message: `duplicate workflow name "${name}" (also at ${existing})`,
2421
- path: path5
2435
+ path: path6
2422
2436
  });
2423
2437
  } else {
2424
- seenWorkflows.set(name, path5.join("."));
2438
+ seenWorkflows.set(name, path6.join("."));
2425
2439
  }
2426
2440
  };
2427
2441
  for (let i = 0; i < manifest.workflows.length; i += 1) {
@@ -2433,7 +2447,7 @@ var moduleManifestSchema = import_zod.z.object({
2433
2447
  }
2434
2448
  }
2435
2449
  const seenEndpointKeys = /* @__PURE__ */ new Map();
2436
- const noteEndpoint = (claim, path5) => {
2450
+ const noteEndpoint = (claim, path6) => {
2437
2451
  let key;
2438
2452
  try {
2439
2453
  key = normalizeEndpointPathForCollision(claim.path);
@@ -2445,10 +2459,10 @@ var moduleManifestSchema = import_zod.z.object({
2445
2459
  ctx.addIssue({
2446
2460
  code: import_zod.z.ZodIssueCode.custom,
2447
2461
  message: `duplicate normalized endpoint path "${key}" (also at ${existing})`,
2448
- path: path5
2462
+ path: path6
2449
2463
  });
2450
2464
  } else {
2451
- seenEndpointKeys.set(key, path5.join("."));
2465
+ seenEndpointKeys.set(key, path6.join("."));
2452
2466
  }
2453
2467
  };
2454
2468
  for (let i = 0; i < manifest.endpoints.length; i += 1) {
@@ -2549,8 +2563,8 @@ var moduleManifestSchema = import_zod.z.object({
2549
2563
  }).transform(({ version: _legacyVersion, ...manifest }) => manifest);
2550
2564
  function formatZodErrors(error) {
2551
2565
  return error.issues.map((issue) => {
2552
- const path5 = issue.path.length > 0 ? issue.path.join(".") : "root";
2553
- return `${path5}: ${issue.message}`;
2566
+ const path6 = issue.path.length > 0 ? issue.path.join(".") : "root";
2567
+ return `${path6}: ${issue.message}`;
2554
2568
  });
2555
2569
  }
2556
2570
  function parseModuleManifest(raw) {
@@ -2950,16 +2964,16 @@ var collectedE2eFactsSchema = import_zod3.z.object({
2950
2964
  function validateRouteExportEvidenceCongruence(input) {
2951
2965
  const entrySet = new Set(input.routeEntries);
2952
2966
  const exportPaths = input.routePageExports.map((e) => e.nextRelativePath);
2953
- for (const path5 of exportPaths) {
2954
- if (!entrySet.has(path5)) {
2955
- input.onIssue(`routePageExports path "${path5}" is not listed in routeEntries`);
2967
+ for (const path6 of exportPaths) {
2968
+ if (!entrySet.has(path6)) {
2969
+ input.onIssue(`routePageExports path "${path6}" is not listed in routeEntries`);
2956
2970
  }
2957
2971
  }
2958
2972
  if (input.routeCollectionErrors.length === 0) {
2959
2973
  const exportSet = new Set(exportPaths);
2960
- for (const path5 of input.routeEntries) {
2961
- if (!exportSet.has(path5)) {
2962
- input.onIssue(`routeEntries path "${path5}" is missing from routePageExports`);
2974
+ for (const path6 of input.routeEntries) {
2975
+ if (!exportSet.has(path6)) {
2976
+ input.onIssue(`routeEntries path "${path6}" is missing from routePageExports`);
2963
2977
  }
2964
2978
  }
2965
2979
  }
@@ -3904,6 +3918,50 @@ function analyzeClientSafeEntryImports(sourceText, fileName) {
3904
3918
  return analyzeClientSafeEntryImportsWithTs(import_typescript3.default, sourceText, fileName);
3905
3919
  }
3906
3920
 
3921
+ // ../../packages/lib/src/shared/ownership-record.ts
3922
+ var import_zod6 = require("zod");
3923
+ var OWNERSHIP_RECORD_PATH = "stardeck.ownership.json";
3924
+ var OWNERSHIP_RECORD_VERSION = 1;
3925
+ var moduleList = import_zod6.z.array(import_zod6.z.string().refine(isValidModuleName, "not a Module name"));
3926
+ var ownershipRecordSchema = import_zod6.z.object({
3927
+ version: import_zod6.z.literal(OWNERSHIP_RECORD_VERSION),
3928
+ /** False once the app stopped all Stardeck updates: then every file is the app's. */
3929
+ stardeckUpdates: import_zod6.z.boolean(),
3930
+ /** projects.ejected_modules: updates stopped for these Modules, so their folders are the app's. */
3931
+ updatesStoppedModules: moduleList,
3932
+ /** projects.ui_ejected_modules: Customize UI, so their screens are the app's. */
3933
+ customizedUiModules: moduleList,
3934
+ /** projects.external_modules: installed from another app, so the app owns the copy. */
3935
+ externalModules: moduleList,
3936
+ /** projects.removed_modules names: uninstalled on purpose. */
3937
+ uninstalledModules: moduleList
3938
+ });
3939
+ function parseOwnershipRecord(content) {
3940
+ let json;
3941
+ try {
3942
+ json = JSON.parse(content);
3943
+ } catch (error) {
3944
+ return { ok: false, error: `${OWNERSHIP_RECORD_PATH} is not valid JSON: ${String(error)}` };
3945
+ }
3946
+ const parsed = ownershipRecordSchema.safeParse(json);
3947
+ if (!parsed.success) {
3948
+ return {
3949
+ ok: false,
3950
+ error: `${OWNERSHIP_RECORD_PATH} is not a record Stardeck wrote: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`
3951
+ };
3952
+ }
3953
+ return { ok: true, record: parsed.data };
3954
+ }
3955
+ var BLUEPRINT_RELEASE_COMMITTER_EMAIL = "ci@stardeck.ai";
3956
+ var BLUEPRINT_RELEASE_SUBJECT_PREFIX = "publish: ";
3957
+ var STARDECK_OWNED_ALTERNATIVES = [
3958
+ "If the Module has a setting for this, change the setting.",
3959
+ "If a slot contribution can do it, write it in app code outside the Module folder (`apps/web/src/lib/modules/app-extensions.ts` registers it).",
3960
+ "To rework a Module's screens, the app owner can choose **Customize UI** in the dashboard's Modules tab (run **Sync** there first if it is refused). The app then owns that Module's `components/` and `routes/`, and its logic keeps updating.",
3961
+ "If the Module's logic is wrong for every app, report it to Stardeck so every app gets the fix.",
3962
+ "If the app cannot wait, the app owner can stop Stardeck updates for this Module, or for the whole app, in the Modules tab. Both are permanent."
3963
+ ];
3964
+
3907
3965
  // ../../packages/lib/src/shared/platform-owned-paths.ts
3908
3966
  var APP_WEB_PREFIX = "apps/web/";
3909
3967
  var BLUEPRINT_LINT_MARKER = `${APP_WEB_PREFIX}eslint-rules/module-boundaries.mjs`;
@@ -3917,20 +3975,217 @@ var MODULE_INIT_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-init.server.gen.t
3917
3975
  var MODULE_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-server.gen.ts`;
3918
3976
  var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
3919
3977
  var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
3978
+ var APP_OWNED_PATHS = [APP_EXTENSIONS_PATH];
3979
+ function platformOwnedSet(input) {
3980
+ const externalModules = input.externalModules ?? [];
3981
+ const removedModules = input.removedModules ?? [];
3982
+ const moduleNames = [
3983
+ ...input.moduleNames,
3984
+ ...input.ejectedModules,
3985
+ ...input.uiEjectedModules,
3986
+ ...externalModules,
3987
+ ...removedModules
3988
+ ];
3989
+ for (const moduleName of moduleNames) {
3990
+ if (!isValidModuleName(moduleName)) {
3991
+ throw new Error(`Invalid Module name: ${moduleName}`);
3992
+ }
3993
+ }
3994
+ const appOwned = /* @__PURE__ */ new Set([...input.ejectedModules, ...externalModules]);
3995
+ const activeModules = input.moduleNames.filter((name) => !appOwned.has(name));
3996
+ const uiEjectedModules = new Set(input.uiEjectedModules);
3997
+ return {
3998
+ prefixes: [
3999
+ ...activeModules.map((name) => `${APP_WEB_PREFIX}src/modules/${name}/`),
4000
+ `${APP_WEB_PREFIX}src/lib/modules/`
4001
+ ].sort(),
4002
+ // Build outputs, not source: compose regenerates them from Module source on
4003
+ // every dev/build/lint/typecheck/test run and git ignores them, so there is
4004
+ // nothing for the write refusal or the Blueprint updater to protect.
4005
+ files: [],
4006
+ // Detach dominates: a detached Module has no prefix, so it needs no carve-out.
4007
+ carveOuts: activeModules.filter((name) => uiEjectedModules.has(name)).flatMap(
4008
+ (name) => MODULE_UI_SUBPATHS.map((subpath) => `${APP_WEB_PREFIX}src/modules/${name}/${subpath}`)
4009
+ ).sort(),
4010
+ externalModules: [...externalModules].sort(),
4011
+ removedModules: [...removedModules].sort()
4012
+ };
4013
+ }
4014
+ function appOwnedExcludePathspecs(set) {
4015
+ return [
4016
+ ...APP_OWNED_PATHS.map((file) => `:(exclude)${file}`),
4017
+ ...set.carveOuts.map((carveOut) => `:(glob,exclude)${carveOut}**`)
4018
+ ];
4019
+ }
4020
+ function platformOwnedDiffPathspecs(set) {
4021
+ return [...set.prefixes, ...set.files, ...appOwnedExcludePathspecs(set)];
4022
+ }
4023
+
4024
+ // ../../packages/lib/src/server/module-rail/ownership-guard.ts
4025
+ var STRICT_RECORD = {
4026
+ version: 1,
4027
+ stardeckUpdates: true,
4028
+ updatesStoppedModules: [],
4029
+ customizedUiModules: [],
4030
+ externalModules: [],
4031
+ uninstalledModules: []
4032
+ };
4033
+ var MANIFEST_PATH = new RegExp(
4034
+ `^${MODULES_SANDBOX_DIR.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/([^/]+)/${MODULE_MANIFEST_FILENAME.replace(".", "\\.")}$`
4035
+ );
4036
+ function realpath(dir) {
4037
+ try {
4038
+ return import_node_fs.default.realpathSync(dir);
4039
+ } catch {
4040
+ return import_node_path.default.resolve(dir);
4041
+ }
4042
+ }
4043
+ function zList(stdout) {
4044
+ return stdout.split("\0").filter(Boolean);
4045
+ }
4046
+ async function findRelease(git) {
4047
+ const found = await git([
4048
+ "rev-list",
4049
+ "-n1",
4050
+ "--topo-order",
4051
+ `--committer=${BLUEPRINT_RELEASE_COMMITTER_EMAIL.replace(/\./g, "\\.")}`,
4052
+ `--grep=^${BLUEPRINT_RELEASE_SUBJECT_PREFIX}`,
4053
+ "HEAD"
4054
+ ]);
4055
+ const sha = found.status === 0 ? found.stdout.trim() : "";
4056
+ return sha || null;
4057
+ }
4058
+ async function noReleaseReason(git) {
4059
+ const shallow = await git(["rev-parse", "--is-shallow-repository"]);
4060
+ if (shallow.status === 0 && shallow.stdout.trim() === "true") {
4061
+ return {
4062
+ status: "skipped",
4063
+ warning: true,
4064
+ reason: "this is a shallow clone, so Stardeck's version of the Module files is not in its history. Run `git fetch --unshallow` and lint again to check that no Stardeck-owned file changed."
4065
+ };
4066
+ }
4067
+ return {
4068
+ status: "skipped",
4069
+ warning: false,
4070
+ reason: "no Stardeck release in this repo's history, so it holds no Stardeck-owned files"
4071
+ };
4072
+ }
4073
+ function readRecord(repoRoot) {
4074
+ let content;
4075
+ try {
4076
+ content = import_node_fs.default.readFileSync(import_node_path.default.join(repoRoot, OWNERSHIP_RECORD_PATH), "utf8");
4077
+ } catch (error) {
4078
+ if (error.code === "ENOENT") {
4079
+ return { ok: true, record: STRICT_RECORD, found: false };
4080
+ }
4081
+ throw error;
4082
+ }
4083
+ const parsed = parseOwnershipRecord(content);
4084
+ return parsed.ok ? { ok: true, record: parsed.record, found: true } : parsed;
4085
+ }
4086
+ async function releaseModules(git, release, record) {
4087
+ const listed = await git([
4088
+ "ls-tree",
4089
+ "-r",
4090
+ "-z",
4091
+ "--name-only",
4092
+ release,
4093
+ "--",
4094
+ MODULES_SANDBOX_DIR
4095
+ ]);
4096
+ if (listed.status !== 0) throw new Error(`git ls-tree ${release} failed`);
4097
+ const uninstalled = new Set(record.uninstalledModules);
4098
+ return zList(listed.stdout).map((file) => MANIFEST_PATH.exec(file)?.[1]).filter((name) => !!name && !uninstalled.has(name));
4099
+ }
4100
+ async function checkStardeckOwnedFiles(input) {
4101
+ const { git, repoRoot } = input;
4102
+ const top = await git(["rev-parse", "--show-toplevel"]);
4103
+ if (top.status !== 0) {
4104
+ return { status: "skipped", warning: false, reason: "not a git checkout" };
4105
+ }
4106
+ if (realpath(top.stdout.trim()) !== realpath(repoRoot)) {
4107
+ return {
4108
+ status: "skipped",
4109
+ warning: false,
4110
+ reason: `${repoRoot} is not the root of its git repo, so it is not an app's own repo`
4111
+ };
4112
+ }
4113
+ const release = await findRelease(git);
4114
+ if (!release) return noReleaseReason(git);
4115
+ const read = readRecord(repoRoot);
4116
+ if (!read.ok) return { status: "invalid-record", error: read.error };
4117
+ const { record } = read;
4118
+ if (!record.stardeckUpdates) {
4119
+ return {
4120
+ status: "skipped",
4121
+ warning: false,
4122
+ reason: "this app stopped all Stardeck updates, so every file is the app's"
4123
+ };
4124
+ }
4125
+ const moduleNames = await releaseModules(git, release, record);
4126
+ const set = platformOwnedSet({
4127
+ moduleNames,
4128
+ ejectedModules: record.updatesStoppedModules,
4129
+ uiEjectedModules: record.customizedUiModules,
4130
+ externalModules: record.externalModules,
4131
+ removedModules: record.uninstalledModules
4132
+ });
4133
+ const pathspecs = [
4134
+ ...platformOwnedDiffPathspecs(set),
4135
+ ...record.uninstalledModules.map(
4136
+ (name) => `:(glob,exclude)${MODULES_SANDBOX_DIR}/*/enhancements/${name}/**`
4137
+ )
4138
+ ];
4139
+ const [changed, added] = await Promise.all([
4140
+ git([
4141
+ "diff",
4142
+ "--name-only",
4143
+ "-z",
4144
+ "--no-renames",
4145
+ "--no-ext-diff",
4146
+ release,
4147
+ "--",
4148
+ ...pathspecs
4149
+ ]),
4150
+ git(["ls-files", "-z", "--others", "--exclude-standard", "--", ...pathspecs])
4151
+ ]);
4152
+ if (changed.status !== 0 || added.status !== 0) {
4153
+ throw new Error(`git could not compare the checkout with Stardeck's version ${release}`);
4154
+ }
4155
+ const paths = [.../* @__PURE__ */ new Set([...zList(changed.stdout), ...zList(added.stdout)])].sort();
4156
+ console.log(
4157
+ `[OwnershipGuard] release=${release.slice(0, 12)} record=${read.found ? "found" : "missing"} modules=${moduleNames.length} carveOuts=${set.carveOuts.length} changed=${paths.length}`
4158
+ );
4159
+ return paths.length > 0 ? { status: "changed", release, paths } : { status: "clean", release };
4160
+ }
4161
+ function formatOwnershipGuardFailure(result) {
4162
+ if (result.status === "invalid-record") {
4163
+ return `${result.error}
4164
+ Restore it with \`git checkout origin/HEAD -- ${OWNERSHIP_RECORD_PATH}\`. Stardeck writes this file; ownership changes in the dashboard's Modules tab.`;
4165
+ }
4166
+ const count = `${result.paths.length} Stardeck-owned ${result.paths.length === 1 ? "file" : "files"}`;
4167
+ return [
4168
+ `${count} changed from Stardeck's version (${result.release.slice(0, 12)}). The next Stardeck update replaces them, so these changes would be lost:`,
4169
+ ...result.paths.map((file) => ` - ${file}`),
4170
+ "",
4171
+ `Undo them (\`git checkout ${result.release.slice(0, 12)} -- <path>\`, or delete a new file) and do one of these instead:`,
4172
+ ...STARDECK_OWNED_ALTERNATIVES.map((line) => ` - ${line.replace(/\*\*/g, "")}`)
4173
+ ].join("\n");
4174
+ }
3920
4175
 
3921
4176
  // src/cli.ts
3922
4177
  var import_typescript7 = __toESM(require("typescript"));
3923
- var import_node_path4 = __toESM(require("path"));
4178
+ var import_node_path5 = __toESM(require("path"));
3924
4179
 
3925
4180
  // src/index.ts
3926
4181
  var import_node_child_process2 = require("child_process");
3927
- var import_node_fs3 = __toESM(require("fs"));
3928
- var import_node_path3 = __toESM(require("path"));
4182
+ var import_node_fs4 = __toESM(require("fs"));
4183
+ var import_node_path4 = __toESM(require("path"));
3929
4184
 
3930
4185
  // ../../packages/lib/src/server/module-rail/local-fs-sandbox.ts
3931
4186
  var import_node_child_process = require("child_process");
3932
- var import_node_fs = __toESM(require("fs"));
3933
- var import_node_path = __toESM(require("path"));
4187
+ var import_node_fs2 = __toESM(require("fs"));
4188
+ var import_node_path2 = __toESM(require("path"));
3934
4189
  function localFsSandbox(root) {
3935
4190
  return {
3936
4191
  async exec(command, options) {
@@ -3954,13 +4209,13 @@ ${result.error}`);
3954
4209
  },
3955
4210
  async fileExists(relative) {
3956
4211
  try {
3957
- return import_node_fs.default.statSync(import_node_path.default.join(root, relative)).isFile();
4212
+ return import_node_fs2.default.statSync(import_node_path2.default.join(root, relative)).isFile();
3958
4213
  } catch {
3959
4214
  return false;
3960
4215
  }
3961
4216
  },
3962
4217
  async readFile(relative) {
3963
- return import_node_fs.default.readFileSync(import_node_path.default.join(root, relative), "utf8");
4218
+ return import_node_fs2.default.readFileSync(import_node_path2.default.join(root, relative), "utf8");
3964
4219
  },
3965
4220
  async fetchRemoteRef() {
3966
4221
  throw new Error("[ModuleRail] local filesystem sandbox cannot reach the network");
@@ -3982,6 +4237,7 @@ var JSON_SECRET_PAIR = new RegExp(
3982
4237
  "g"
3983
4238
  );
3984
4239
  var SECRET_QUOTE_OPENER = new RegExp(String.raw`\b${SECRET_NAME}\s*=\s*(['"])`, "g");
4240
+ var SHELL_QUOTE_CONTINUATION = String.raw`\''`;
3985
4241
  function redactSecrets(text) {
3986
4242
  return text.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@").replace(/https:\/\/(?!x-access-token:)[^/\s:@]+:[^/\s@]+@/gi, "https://***:***@").replace(/\b(gh[pousr]_)[A-Za-z0-9]+/g, "$1***").replace(/\b(github_pat_)[A-Za-z0-9_]+/g, "$1***").replace(/\b(postgres(?:ql)?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1$2:***@").replace(/\b(npm_[A-Za-z0-9]+)/g, "npm_***").replace(/(_authToken=)([^\s&]+)/gi, "$1***").replace(SECRET_ASSIGNMENT, "$1=***").replace(JSON_SECRET_PAIR, '"$1"$2"***"').replace(/\b(Authorization:\s*(?:Bearer|Basic)\s+)\S+/gi, "$1***");
3987
4243
  }
@@ -3992,28 +4248,28 @@ function shellQuote(value) {
3992
4248
  }
3993
4249
 
3994
4250
  // ../../packages/lib/src/shared/compose-inputs.ts
3995
- var import_zod6 = require("zod");
4251
+ var import_zod7 = require("zod");
3996
4252
  var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
3997
- var composeInputsSchema = import_zod6.z.strictObject({
3998
- connectedStores: import_zod6.z.array(
3999
- import_zod6.z.strictObject({
4000
- storeId: import_zod6.z.string().min(1),
4001
- slug: import_zod6.z.string().min(1),
4253
+ var composeInputsSchema = import_zod7.z.strictObject({
4254
+ connectedStores: import_zod7.z.array(
4255
+ import_zod7.z.strictObject({
4256
+ storeId: import_zod7.z.string().min(1),
4257
+ slug: import_zod7.z.string().min(1),
4002
4258
  // A row with an empty binding key must not fail a deploy: the rail
4003
4259
  // treats falsy as "no binding key", so normalise rather than reject.
4004
- bindingKey: import_zod6.z.string().nullish().transform((value) => value || null),
4005
- accessLevel: import_zod6.z.enum(["read", "write", "admin"]),
4260
+ bindingKey: import_zod7.z.string().nullish().transform((value) => value || null),
4261
+ accessLevel: import_zod7.z.enum(["read", "write", "admin"]),
4006
4262
  // Defaulted so a file written before the platform emitted store types
4007
4263
  // (database rows only) still parses. The platform starts writing
4008
4264
  // storage rows, with `type` set, once forks run a compose that reads it.
4009
- type: import_zod6.z.enum(["database", "storage"]).default("database")
4265
+ type: import_zod7.z.enum(["database", "storage"]).default("database")
4010
4266
  })
4011
4267
  ).default([]),
4012
- installs: import_zod6.z.record(
4013
- import_zod6.z.string(),
4014
- import_zod6.z.strictObject({
4015
- installedVersion: import_zod6.z.string().min(1),
4016
- datastoreBindings: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).nullish()
4268
+ installs: import_zod7.z.record(
4269
+ import_zod7.z.string(),
4270
+ import_zod7.z.strictObject({
4271
+ installedVersion: import_zod7.z.string().min(1),
4272
+ datastoreBindings: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).nullish()
4017
4273
  })
4018
4274
  ).default({})
4019
4275
  });
@@ -4173,13 +4429,13 @@ function assertClientSafeConventionalEntryImports(source, pathLabel, kind) {
4173
4429
  function createMessageTree() {
4174
4430
  return /* @__PURE__ */ Object.create(null);
4175
4431
  }
4176
- function assertMessageTree(value, path5) {
4432
+ function assertMessageTree(value, path6) {
4177
4433
  if (typeof value === "string") return;
4178
4434
  if (value === null || Array.isArray(value) || typeof value !== "object") {
4179
- throw new Error(`Invalid message at ${path5}: expected string or nested object`);
4435
+ throw new Error(`Invalid message at ${path6}: expected string or nested object`);
4180
4436
  }
4181
4437
  for (const [key, child] of Object.entries(value)) {
4182
- assertMessageTree(child, `${path5}.${key}`);
4438
+ assertMessageTree(child, `${path6}.${key}`);
4183
4439
  }
4184
4440
  }
4185
4441
  function cloneTree(value) {
@@ -4189,17 +4445,17 @@ function cloneTree(value) {
4189
4445
  }
4190
4446
  return clone;
4191
4447
  }
4192
- function collectLeafPaths(value, path5, out) {
4448
+ function collectLeafPaths(value, path6, out) {
4193
4449
  if (typeof value === "string") {
4194
- out.push(path5);
4450
+ out.push(path6);
4195
4451
  return;
4196
4452
  }
4197
- assertMessageTree(value, path5);
4198
- for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path5}.${key}`, out);
4453
+ assertMessageTree(value, path6);
4454
+ for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path6}.${key}`, out);
4199
4455
  }
4200
- function overrideInto(target, source, owner, path5, missing) {
4456
+ function overrideInto(target, source, owner, path6, missing) {
4201
4457
  for (const [key, value] of Object.entries(source)) {
4202
- const nextPath = path5 ? `${path5}.${key}` : key;
4458
+ const nextPath = path6 ? `${path6}.${key}` : key;
4203
4459
  if (!Object.prototype.hasOwnProperty.call(target, key)) {
4204
4460
  collectLeafPaths(value, nextPath, missing);
4205
4461
  continue;
@@ -4226,9 +4482,9 @@ function overrideInto(target, source, owner, path5, missing) {
4226
4482
  function composeLocaleMessagesForPlan(layers) {
4227
4483
  const result = createMessageTree();
4228
4484
  const leafOwners = /* @__PURE__ */ new Map();
4229
- function mergeInto(target, source, owner, path5) {
4485
+ function mergeInto(target, source, owner, path6) {
4230
4486
  for (const [key, value] of Object.entries(source)) {
4231
- const nextPath = path5 ? `${path5}.${key}` : key;
4487
+ const nextPath = path6 ? `${path6}.${key}` : key;
4232
4488
  const hasExisting = Object.prototype.hasOwnProperty.call(target, key);
4233
4489
  const existing = hasExisting ? target[key] : void 0;
4234
4490
  if (typeof value === "string") {
@@ -4789,8 +5045,8 @@ var METHOD_ORDER = {
4789
5045
  function compareNames2(a, b) {
4790
5046
  return a < b ? -1 : a > b ? 1 : 0;
4791
5047
  }
4792
- function endpointPairKey(method, path5) {
4793
- return `${method} ${path5}`;
5048
+ function endpointPairKey(method, path6) {
5049
+ return `${method} ${path6}`;
4794
5050
  }
4795
5051
  function apiPathToRouteFile(apiPath) {
4796
5052
  if (!apiPath.startsWith("/api/")) {
@@ -4804,11 +5060,11 @@ function routeEntryToAppPageFile(nextRelativePath) {
4804
5060
  }
4805
5061
  var APP_ROUTE_FILE_PREFIX = `${APP_PACKAGE_DIR}/src/app/`;
4806
5062
  var API_ROUTE_FILE_SUFFIXES = ["/route.ts", "/route.tsx", "/route.jsx", "/route.js"];
4807
- function appRouteFileToNextRelativePath(path5) {
4808
- const pagePath = appPageFileToNextRelativePath(path5, APP_PACKAGE_DIR);
5063
+ function appRouteFileToNextRelativePath(path6) {
5064
+ const pagePath = appPageFileToNextRelativePath(path6, APP_PACKAGE_DIR);
4809
5065
  if (pagePath != null) return pagePath;
4810
- if (!path5.startsWith(APP_ROUTE_FILE_PREFIX)) return null;
4811
- const relative = path5.slice(APP_ROUTE_FILE_PREFIX.length);
5066
+ if (!path6.startsWith(APP_ROUTE_FILE_PREFIX)) return null;
5067
+ const relative = path6.slice(APP_ROUTE_FILE_PREFIX.length);
4812
5068
  if (!relative.startsWith("api/")) return null;
4813
5069
  const suffix = API_ROUTE_FILE_SUFFIXES.find((candidate) => relative.endsWith(candidate));
4814
5070
  if (!suffix) return null;
@@ -4827,6 +5083,9 @@ function enhancementImport(owner, peer) {
4827
5083
  function scopeLabel(scope) {
4828
5084
  return scope.kind === "root" ? "root" : `enhancement:${scope.peer}`;
4829
5085
  }
5086
+ function endpointMarkerOwnsStub(marker, stub) {
5087
+ return stub != null && marker?.kind === "endpoint" && marker.owner === stub.owner;
5088
+ }
4830
5089
  function formatEndpointMarker(owner, scope) {
4831
5090
  return `// ${MODULE_STUB_MARKER} owner=${owner} scope=${scopeLabel(scope)}`;
4832
5091
  }
@@ -4900,9 +5159,9 @@ function legacyHandlerPropertyMatchesPath(property, method, physicalPath, bindin
4900
5159
  const expectedTokens = knownAliases[`${binding} ${method} ${physicalPath}`] ?? pathTokens;
4901
5160
  return handlerTokens.length === expectedTokens.length && handlerTokens.every((token, index) => token === expectedTokens[index]);
4902
5161
  }
4903
- function parseLegacyApiAdapter(path5, source) {
4904
- const physicalPath = appRouteFileToNextRelativePath(path5);
4905
- if (expectedMarkerKindForPath(path5) !== "endpoint" || physicalPath == null) return null;
5162
+ function parseLegacyApiAdapter(path6, source) {
5163
+ const physicalPath = appRouteFileToNextRelativePath(path6);
5164
+ if (expectedMarkerKindForPath(path6) !== "endpoint" || physicalPath == null) return null;
4906
5165
  const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
4907
5166
  const match = firstLine.match(
4908
5167
  new RegExp(
@@ -4912,7 +5171,7 @@ function parseLegacyApiAdapter(path5, source) {
4912
5171
  if (!match || match[2] !== physicalPath) return null;
4913
5172
  const owner = match[1];
4914
5173
  if (!isValidModuleName(owner)) return null;
4915
- const file = import_typescript4.default.createSourceFile(path5, source, import_typescript4.default.ScriptTarget.Latest, false, import_typescript4.default.ScriptKind.TS);
5174
+ const file = import_typescript4.default.createSourceFile(path6, source, import_typescript4.default.ScriptTarget.Latest, false, import_typescript4.default.ScriptKind.TS);
4916
5175
  if (file.parseDiagnostics.length > 0) {
4917
5176
  return null;
4918
5177
  }
@@ -4954,9 +5213,9 @@ function parseLegacyApiAdapter(path5, source) {
4954
5213
  }
4955
5214
  return handlerBinding != null && methods.size > 0 ? { owner, methods: [...methods] } : null;
4956
5215
  }
4957
- function parseLegacyRouteAdapter(path5, source) {
4958
- const physicalPath = appRouteFileToNextRelativePath(path5);
4959
- if (expectedMarkerKindForPath(path5) !== "route" || physicalPath == null) return null;
5216
+ function parseLegacyRouteAdapter(path6, source) {
5217
+ const physicalPath = appRouteFileToNextRelativePath(path6);
5218
+ if (expectedMarkerKindForPath(path6) !== "route" || physicalPath == null) return null;
4960
5219
  const normalized = source.replace(/\r\n/g, "\n");
4961
5220
  const match = normalized.match(
4962
5221
  new RegExp(
@@ -4968,30 +5227,30 @@ function parseLegacyRouteAdapter(path5, source) {
4968
5227
  const nextRelativePath = match[2];
4969
5228
  return isValidModuleName(owner) && nextRelativePath === physicalPath ? { owner, nextRelativePath } : null;
4970
5229
  }
4971
- function legacyRouteAdapterMatchesStub(path5, source, stub) {
4972
- const legacy = parseLegacyRouteAdapter(path5, source);
5230
+ function legacyRouteAdapterMatchesStub(path6, source, stub) {
5231
+ const legacy = parseLegacyRouteAdapter(path6, source);
4973
5232
  return legacy != null && legacy.owner === stub.owner;
4974
5233
  }
4975
5234
  var API_ROUTE_DIR_PREFIX = `${APP_PACKAGE_DIR}/src/app/api/`;
4976
5235
  var APP_DIR_PREFIX = `${APP_PACKAGE_DIR}/src/app/`;
4977
5236
  var PAGE_FILE_SUFFIXES = ["/page.tsx", "/page.ts", "/page.jsx", "/page.js"];
4978
- function expectedMarkerKindForPath(path5) {
4979
- if (path5 === MODULES_GEN_PATH) return "registry";
4980
- if (path5 === MODULE_CONTRIBUTIONS_GEN_PATH) return "contributions";
4981
- if (path5 === MODULE_DATASTORES_GEN_PATH) return "datastores";
4982
- if (path5 === MODULE_I18N_GEN_PATH) return "i18n";
4983
- if (path5 === MODULE_INIT_SERVER_GEN_PATH) return "init";
4984
- if (path5 === MODULE_SERVER_GEN_PATH) return "server";
4985
- if (path5.startsWith(API_ROUTE_DIR_PREFIX) && path5.endsWith("/route.ts")) return "endpoint";
4986
- if (path5.startsWith(APP_DIR_PREFIX) && !path5.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path5.endsWith(suffix))) {
5237
+ function expectedMarkerKindForPath(path6) {
5238
+ if (path6 === MODULES_GEN_PATH) return "registry";
5239
+ if (path6 === MODULE_CONTRIBUTIONS_GEN_PATH) return "contributions";
5240
+ if (path6 === MODULE_DATASTORES_GEN_PATH) return "datastores";
5241
+ if (path6 === MODULE_I18N_GEN_PATH) return "i18n";
5242
+ if (path6 === MODULE_INIT_SERVER_GEN_PATH) return "init";
5243
+ if (path6 === MODULE_SERVER_GEN_PATH) return "server";
5244
+ if (path6.startsWith(API_ROUTE_DIR_PREFIX) && path6.endsWith("/route.ts")) return "endpoint";
5245
+ if (path6.startsWith(APP_DIR_PREFIX) && !path6.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path6.endsWith(suffix))) {
4987
5246
  return "route";
4988
5247
  }
4989
5248
  return null;
4990
5249
  }
4991
- function markerCongruentWithPath(path5, marker) {
4992
- if (expectedMarkerKindForPath(path5) !== marker.kind) return false;
5250
+ function markerCongruentWithPath(path6, marker) {
5251
+ if (expectedMarkerKindForPath(path6) !== marker.kind) return false;
4993
5252
  if (marker.kind === "route") {
4994
- const derived = appPageFileToNextRelativePath(path5, APP_PACKAGE_DIR);
5253
+ const derived = appPageFileToNextRelativePath(path6, APP_PACKAGE_DIR);
4995
5254
  return derived != null && derived === marker.nextRelativePath;
4996
5255
  }
4997
5256
  return true;
@@ -5267,8 +5526,8 @@ function buildGeneratedModuleRegistry(input) {
5267
5526
  function activeEnhancementPeers(registryModule) {
5268
5527
  return new Set(registryModule.enhancements.filter((e) => e.active).map((e) => e.peer));
5269
5528
  }
5270
- function endpointBucketKey(path5, owner, scope) {
5271
- return `${path5}\0${owner}\0${scopeLabel(scope)}`;
5529
+ function endpointBucketKey(path6, owner, scope) {
5530
+ return `${path6}\0${owner}\0${scopeLabel(scope)}`;
5272
5531
  }
5273
5532
  function planRouteStubs(input) {
5274
5533
  const endpointBuckets = /* @__PURE__ */ new Map();
@@ -5392,7 +5651,7 @@ function planRouteStubs(input) {
5392
5651
  for (const entry of entries) {
5393
5652
  const nextRelativePath = entry.nextRelativePath;
5394
5653
  const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
5395
- const path5 = routeEntryToAppPageFile(nextRelativePath);
5654
+ const path6 = routeEntryToAppPageFile(nextRelativePath);
5396
5655
  const existing = routeDestinationOwners.get(destination);
5397
5656
  if (existing) {
5398
5657
  throw new Error(
@@ -5402,13 +5661,13 @@ function planRouteStubs(input) {
5402
5661
  routeDestinationOwners.set(destination, {
5403
5662
  owner: regMod.name,
5404
5663
  nextRelativePath,
5405
- path: path5
5664
+ path: path6
5406
5665
  });
5407
5666
  const importModule = routeEntryImport(regMod.name, nextRelativePath);
5408
5667
  const analyzed = analyzeRouteEntryPath(nextRelativePath);
5409
5668
  const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
5410
5669
  routeStubs.push({
5411
- path: path5,
5670
+ path: path6,
5412
5671
  owner: regMod.name,
5413
5672
  nextRelativePath,
5414
5673
  importModule,
@@ -5469,16 +5728,16 @@ function planCompositionArtifacts(input) {
5469
5728
  ...stubs.routeStubs.map((stub) => stub.path)
5470
5729
  ]);
5471
5730
  const existingGeneratedPathSet = new Set(input.existingGeneratedPaths);
5472
- const isProvenStaleDeletionCandidate = (path5, content) => {
5473
- if (path5 === MODULES_GEN_PATH) return false;
5474
- if (COMPOSITION_RUNTIME_GEN_PATHS.includes(path5)) return false;
5475
- if (desiredPaths.has(path5)) return false;
5476
- if (!existingGeneratedPathSet.has(path5)) return false;
5477
- const legacy = parseLegacyApiAdapter(path5, content);
5731
+ const isProvenStaleDeletionCandidate = (path6, content) => {
5732
+ if (path6 === MODULES_GEN_PATH) return false;
5733
+ if (COMPOSITION_RUNTIME_GEN_PATHS.includes(path6)) return false;
5734
+ if (desiredPaths.has(path6)) return false;
5735
+ if (!existingGeneratedPathSet.has(path6)) return false;
5736
+ const legacy = parseLegacyApiAdapter(path6, content);
5478
5737
  if (legacy) return true;
5479
- if (parseLegacyRouteAdapter(path5, content)) return true;
5738
+ if (parseLegacyRouteAdapter(path6, content)) return true;
5480
5739
  const marker = parseCompositionArtifactMarker(content);
5481
- return marker != null && markerCongruentWithPath(path5, marker);
5740
+ return marker != null && markerCongruentWithPath(path6, marker);
5482
5741
  };
5483
5742
  const plannedByDestination = /* @__PURE__ */ new Map();
5484
5743
  for (const stub of stubs.routeStubs) {
@@ -5528,7 +5787,7 @@ function planCompositionArtifacts(input) {
5528
5787
  const legacy = parseLegacyApiAdapter(pagePath, content);
5529
5788
  const plannedRoute = plannedRouteByPath.get(pagePath);
5530
5789
  const plannedEndpoint = plannedEndpointByPath.get(pagePath);
5531
- const isExactPlannedMatch = plannedRoute != null && marker != null && marker.kind === "route" && marker.owner === plannedRoute.owner && marker.nextRelativePath === plannedRoute.nextRelativePath || plannedEndpoint != null && (marker != null && marker.kind === "endpoint" && marker.owner === plannedEndpoint.owner && scopeLabel(marker.scope) === scopeLabel(plannedEndpoint.scope) || legacy != null) || plannedRoute != null && legacyRouteAdapterMatchesStub(pagePath, content, plannedRoute);
5790
+ const isExactPlannedMatch = plannedRoute != null && marker != null && marker.kind === "route" && marker.owner === plannedRoute.owner && marker.nextRelativePath === plannedRoute.nextRelativePath || plannedEndpoint != null && (endpointMarkerOwnsStub(marker, plannedEndpoint) || legacy != null) || plannedRoute != null && legacyRouteAdapterMatchesStub(pagePath, content, plannedRoute);
5532
5791
  if (isExactPlannedMatch) {
5533
5792
  continue;
5534
5793
  }
@@ -5632,13 +5891,13 @@ function planCompositionArtifacts(input) {
5632
5891
  for (const stub of stubs.routeStubs) desired.set(stub.path, stub.content);
5633
5892
  const endpointStubByPath = new Map(stubs.endpointStubs.map((stub) => [stub.path, stub]));
5634
5893
  const routeStubByPath = new Map(stubs.routeStubs.map((stub) => [stub.path, stub]));
5635
- for (const [path5] of desired) {
5636
- const existing = input.existingFiles[path5];
5894
+ for (const [path6] of desired) {
5895
+ const existing = input.existingFiles[path6];
5637
5896
  if (existing == null) continue;
5638
5897
  let marker = parseCompositionArtifactMarker(existing);
5639
- const plannedEndpoint = endpointStubByPath.get(path5);
5640
- const plannedRoute = routeStubByPath.get(path5);
5641
- const legacy = parseLegacyApiAdapter(path5, existing);
5898
+ const plannedEndpoint = endpointStubByPath.get(path6);
5899
+ const plannedRoute = routeStubByPath.get(path6);
5900
+ const legacy = parseLegacyApiAdapter(path6, existing);
5642
5901
  if (legacy && plannedEndpoint) {
5643
5902
  marker = {
5644
5903
  kind: "endpoint",
@@ -5646,66 +5905,64 @@ function planCompositionArtifacts(input) {
5646
5905
  scope: plannedEndpoint.scope
5647
5906
  };
5648
5907
  }
5649
- if (!marker && plannedRoute && legacyRouteAdapterMatchesStub(path5, existing, plannedRoute)) {
5908
+ if (!marker && plannedRoute && legacyRouteAdapterMatchesStub(path6, existing, plannedRoute)) {
5650
5909
  marker = {
5651
5910
  kind: "route",
5652
5911
  owner: plannedRoute.owner,
5653
5912
  nextRelativePath: plannedRoute.nextRelativePath
5654
5913
  };
5655
5914
  }
5656
- const isGenRuntimePath = COMPOSITION_RUNTIME_GEN_PATHS.includes(path5);
5915
+ const isGenRuntimePath = COMPOSITION_RUNTIME_GEN_PATHS.includes(path6);
5657
5916
  if (!marker) {
5658
5917
  return {
5659
5918
  ok: false,
5660
- 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}`,
5661
- path: path5
5919
+ error: path6 === MODULES_GEN_PATH ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged registry at ${path6}` : isGenRuntimePath ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged generated file at ${path6}` : `[ModuleRail] composition artifacts: refusing to overwrite unmanaged route at ${path6}`,
5920
+ path: path6
5662
5921
  };
5663
5922
  }
5664
- const expectedKind = expectedMarkerKindForPath(path5);
5923
+ const expectedKind = expectedMarkerKindForPath(path6);
5665
5924
  if (marker.kind !== expectedKind) {
5666
5925
  return {
5667
5926
  ok: false,
5668
- 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)`,
5669
- path: path5
5927
+ error: path6 === MODULES_GEN_PATH ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged registry at ${path6} (incongruent marker ownership: found ${marker.kind} marker)` : isGenRuntimePath ? `[ModuleRail] composition artifacts: refusing to overwrite unmanaged generated file at ${path6} (incongruent marker ownership: found ${marker.kind} marker)` : `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path6} (expected ${expectedKind ?? "no"} marker, found ${marker.kind} marker)`,
5928
+ path: path6
5670
5929
  };
5671
5930
  }
5672
5931
  if (marker.kind === "endpoint") {
5673
- const stub = endpointStubByPath.get(path5);
5674
- const ownerMatches = stub != null && marker.owner === stub.owner;
5675
- const scopeMatches = stub != null && scopeLabel(marker.scope) === scopeLabel(stub.scope);
5676
- if (!ownerMatches || !scopeMatches) {
5932
+ const stub = endpointStubByPath.get(path6);
5933
+ if (!endpointMarkerOwnsStub(marker, stub)) {
5677
5934
  return {
5678
5935
  ok: false,
5679
- 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) : "?"})`,
5680
- path: path5,
5936
+ error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path6} (existing owner=${marker.owner} scope=${scopeLabel(marker.scope)} vs planned owner=${stub?.owner ?? "?"} scope=${stub ? scopeLabel(stub.scope) : "?"})`,
5937
+ path: path6,
5681
5938
  owner: stub?.owner
5682
5939
  };
5683
5940
  }
5684
5941
  } else if (marker.kind === "route") {
5685
- const stub = routeStubByPath.get(path5);
5942
+ const stub = routeStubByPath.get(path6);
5686
5943
  const ownerMatches = stub != null && marker.owner === stub.owner;
5687
5944
  const pathMatches = stub != null && marker.nextRelativePath === stub.nextRelativePath;
5688
5945
  if (!ownerMatches || !pathMatches) {
5689
5946
  return {
5690
5947
  ok: false,
5691
- 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 ?? "?"})`,
5692
- path: path5,
5948
+ error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path6} (existing owner=${marker.owner} path=${marker.nextRelativePath} vs planned owner=${stub?.owner ?? "?"} path=${stub?.nextRelativePath ?? "?"})`,
5949
+ path: path6,
5693
5950
  owner: stub?.owner
5694
5951
  };
5695
5952
  }
5696
5953
  }
5697
5954
  }
5698
5955
  const deletes = [];
5699
- for (const path5 of input.existingGeneratedPaths) {
5700
- const existing = input.existingFiles[path5];
5956
+ for (const path6 of input.existingGeneratedPaths) {
5957
+ const existing = input.existingFiles[path6];
5701
5958
  if (existing == null) continue;
5702
- if (!isProvenStaleDeletionCandidate(path5, existing)) {
5959
+ if (!isProvenStaleDeletionCandidate(path6, existing)) {
5703
5960
  continue;
5704
5961
  }
5705
- deletes.push(path5);
5962
+ deletes.push(path6);
5706
5963
  }
5707
5964
  deletes.sort(compareNames2);
5708
- 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));
5965
+ const writes = [...desired.entries()].filter(([path6, content]) => input.existingFiles[path6] !== content).map(([path6, content]) => ({ path: path6, content })).sort((a, b) => compareNames2(a.path, b.path));
5709
5966
  return {
5710
5967
  ok: true,
5711
5968
  plan: {
@@ -5976,14 +6233,14 @@ function rejectPseudoModuleEntries(entries, parentDir, label) {
5976
6233
  }
5977
6234
 
5978
6235
  // ../../packages/lib/src/shared/module-composition.ts
5979
- var import_zod7 = require("zod");
5980
- var creationSourceBlankSchema = import_zod7.z.object({
5981
- type: import_zod7.z.literal("blank")
6236
+ var import_zod8 = require("zod");
6237
+ var creationSourceBlankSchema = import_zod8.z.object({
6238
+ type: import_zod8.z.literal("blank")
5982
6239
  });
5983
- var creationSourceBlueprintSchema = import_zod7.z.object({
5984
- type: import_zod7.z.literal("blueprint"),
6240
+ var creationSourceBlueprintSchema = import_zod8.z.object({
6241
+ type: import_zod8.z.literal("blueprint"),
5985
6242
  /** Internal template/fork project id — keep template naming in identifiers. */
5986
- blueprintProjectId: import_zod7.z.string().uuid(),
6243
+ blueprintProjectId: import_zod8.z.string().uuid(),
5987
6244
  /**
5988
6245
  * `null`/absent means every feature Module: the platform stores that as the
5989
6246
  * explicit list of feature Modules in the published Blueprint version the fork
@@ -5991,192 +6248,197 @@ var creationSourceBlueprintSchema = import_zod7.z.object({
5991
6248
  * a custom app: no feature Module, Library Modules only (`resolvePlatformEnv`
5992
6249
  * emits it as a set value naming no Module).
5993
6250
  */
5994
- enabledModules: import_zod7.z.array(moduleNameSchema).max(100).nullable().optional()
6251
+ enabledModules: import_zod8.z.array(moduleNameSchema).max(100).nullable().optional(),
6252
+ /**
6253
+ * The kind of business (a preset id the published version defines). Its Modules are built
6254
+ * and `enabledModules` adds extras on top; for the general kind `enabledModules` is the pick.
6255
+ */
6256
+ businessKind: import_zod8.z.string().regex(/^[a-z][a-z0-9-]{0,39}$/).optional()
5995
6257
  });
5996
- var creationSourceSchema = import_zod7.z.discriminatedUnion("type", [
6258
+ var creationSourceSchema = import_zod8.z.discriminatedUnion("type", [
5997
6259
  creationSourceBlankSchema,
5998
6260
  creationSourceBlueprintSchema
5999
6261
  ]);
6000
- var moduleInstallHandoffSchema = import_zod7.z.object({
6001
- moduleName: import_zod7.z.string().min(1),
6002
- homeProjectId: import_zod7.z.string().uuid(),
6003
- installedVersion: import_zod7.z.string().min(1),
6004
- baselineSha: import_zod7.z.string().min(1),
6005
- mounts: import_zod7.z.array(
6006
- import_zod7.z.object({
6007
- group: import_zod7.z.string(),
6008
- segment: import_zod7.z.string()
6262
+ var moduleInstallHandoffSchema = import_zod8.z.object({
6263
+ moduleName: import_zod8.z.string().min(1),
6264
+ homeProjectId: import_zod8.z.string().uuid(),
6265
+ installedVersion: import_zod8.z.string().min(1),
6266
+ baselineSha: import_zod8.z.string().min(1),
6267
+ mounts: import_zod8.z.array(
6268
+ import_zod8.z.object({
6269
+ group: import_zod8.z.string(),
6270
+ segment: import_zod8.z.string()
6009
6271
  })
6010
6272
  ),
6011
- integration: import_zod7.z.array(import_zod7.z.string()),
6012
- workflows: import_zod7.z.array(import_zod7.z.string()),
6013
- prunedEnhancements: import_zod7.z.array(import_zod7.z.string()),
6014
- restoredEnhancements: import_zod7.z.array(import_zod7.z.string()).optional(),
6015
- permissions: import_zod7.z.array(
6016
- import_zod7.z.object({
6017
- key: import_zod7.z.string().min(1),
6018
- description: import_zod7.z.string().min(1)
6273
+ integration: import_zod8.z.array(import_zod8.z.string()),
6274
+ workflows: import_zod8.z.array(import_zod8.z.string()),
6275
+ prunedEnhancements: import_zod8.z.array(import_zod8.z.string()),
6276
+ restoredEnhancements: import_zod8.z.array(import_zod8.z.string()).optional(),
6277
+ permissions: import_zod8.z.array(
6278
+ import_zod8.z.object({
6279
+ key: import_zod8.z.string().min(1),
6280
+ description: import_zod8.z.string().min(1)
6019
6281
  })
6020
6282
  ).optional()
6021
6283
  });
6022
6284
 
6023
6285
  // ../../packages/lib/src/shared/schema-ops.ts
6024
- var import_zod8 = require("zod");
6025
- var columnSpecSchema = import_zod8.z.object({
6026
- name: import_zod8.z.string().min(1).max(63),
6027
- pgType: import_zod8.z.string().min(1),
6028
- nullable: import_zod8.z.boolean(),
6286
+ var import_zod9 = require("zod");
6287
+ var columnSpecSchema = import_zod9.z.object({
6288
+ name: import_zod9.z.string().min(1).max(63),
6289
+ pgType: import_zod9.z.string().min(1),
6290
+ nullable: import_zod9.z.boolean(),
6029
6291
  /**
6030
6292
  * A SQL expression used as the column default, as a raw string (e.g. `'t'`,
6031
6293
  * `0`, `now()`, `gen_random_uuid()`). `null` or omitted means no default.
6032
6294
  * NOT a JS literal — must already be a valid SQL expression.
6033
6295
  */
6034
- default: import_zod8.z.string().nullable().optional(),
6035
- fieldType: import_zod8.z.string().optional(),
6036
- fieldConfig: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.unknown()).optional()
6296
+ default: import_zod9.z.string().nullable().optional(),
6297
+ fieldType: import_zod9.z.string().optional(),
6298
+ fieldConfig: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.unknown()).optional()
6037
6299
  });
6038
- var referentialActionSchema = import_zod8.z.enum([
6300
+ var referentialActionSchema = import_zod9.z.enum([
6039
6301
  "cascade",
6040
6302
  "set null",
6041
6303
  "set default",
6042
6304
  "restrict",
6043
6305
  "no action"
6044
6306
  ]);
6045
- var schemaOpSchema = import_zod8.z.discriminatedUnion("type", [
6307
+ var schemaOpSchema = import_zod9.z.discriminatedUnion("type", [
6046
6308
  // Tables
6047
- import_zod8.z.object({
6048
- type: import_zod8.z.literal("createTable"),
6049
- table: import_zod8.z.string().min(1).max(63),
6050
- columns: import_zod8.z.array(columnSpecSchema).min(1),
6051
- primaryKey: import_zod8.z.array(import_zod8.z.string()).optional(),
6052
- ifNotExists: import_zod8.z.boolean().optional()
6309
+ import_zod9.z.object({
6310
+ type: import_zod9.z.literal("createTable"),
6311
+ table: import_zod9.z.string().min(1).max(63),
6312
+ columns: import_zod9.z.array(columnSpecSchema).min(1),
6313
+ primaryKey: import_zod9.z.array(import_zod9.z.string()).optional(),
6314
+ ifNotExists: import_zod9.z.boolean().optional()
6053
6315
  }),
6054
- import_zod8.z.object({
6055
- type: import_zod8.z.literal("dropTable"),
6056
- table: import_zod8.z.string().min(1).max(63),
6057
- cascade: import_zod8.z.boolean().optional(),
6058
- ifExists: import_zod8.z.boolean().optional()
6316
+ import_zod9.z.object({
6317
+ type: import_zod9.z.literal("dropTable"),
6318
+ table: import_zod9.z.string().min(1).max(63),
6319
+ cascade: import_zod9.z.boolean().optional(),
6320
+ ifExists: import_zod9.z.boolean().optional()
6059
6321
  }),
6060
- import_zod8.z.object({
6061
- type: import_zod8.z.literal("renameTable"),
6062
- from: import_zod8.z.string().min(1).max(63),
6063
- to: import_zod8.z.string().min(1).max(63)
6322
+ import_zod9.z.object({
6323
+ type: import_zod9.z.literal("renameTable"),
6324
+ from: import_zod9.z.string().min(1).max(63),
6325
+ to: import_zod9.z.string().min(1).max(63)
6064
6326
  }),
6065
6327
  // Columns
6066
- import_zod8.z.object({
6067
- type: import_zod8.z.literal("addColumn"),
6068
- table: import_zod8.z.string().min(1).max(63),
6328
+ import_zod9.z.object({
6329
+ type: import_zod9.z.literal("addColumn"),
6330
+ table: import_zod9.z.string().min(1).max(63),
6069
6331
  column: columnSpecSchema,
6070
- ifNotExists: import_zod8.z.boolean().optional()
6332
+ ifNotExists: import_zod9.z.boolean().optional()
6071
6333
  }),
6072
- import_zod8.z.object({
6073
- type: import_zod8.z.literal("dropColumn"),
6074
- table: import_zod8.z.string().min(1).max(63),
6075
- column: import_zod8.z.string().min(1).max(63),
6076
- cascade: import_zod8.z.boolean().optional(),
6077
- ifExists: import_zod8.z.boolean().optional()
6334
+ import_zod9.z.object({
6335
+ type: import_zod9.z.literal("dropColumn"),
6336
+ table: import_zod9.z.string().min(1).max(63),
6337
+ column: import_zod9.z.string().min(1).max(63),
6338
+ cascade: import_zod9.z.boolean().optional(),
6339
+ ifExists: import_zod9.z.boolean().optional()
6078
6340
  }),
6079
- import_zod8.z.object({
6080
- type: import_zod8.z.literal("renameColumn"),
6081
- table: import_zod8.z.string().min(1).max(63),
6082
- from: import_zod8.z.string().min(1).max(63),
6083
- to: import_zod8.z.string().min(1).max(63)
6341
+ import_zod9.z.object({
6342
+ type: import_zod9.z.literal("renameColumn"),
6343
+ table: import_zod9.z.string().min(1).max(63),
6344
+ from: import_zod9.z.string().min(1).max(63),
6345
+ to: import_zod9.z.string().min(1).max(63)
6084
6346
  }),
6085
- import_zod8.z.object({
6086
- type: import_zod8.z.literal("alterColumnType"),
6087
- table: import_zod8.z.string().min(1).max(63),
6088
- column: import_zod8.z.string().min(1).max(63),
6089
- newType: import_zod8.z.string().min(1),
6347
+ import_zod9.z.object({
6348
+ type: import_zod9.z.literal("alterColumnType"),
6349
+ table: import_zod9.z.string().min(1).max(63),
6350
+ column: import_zod9.z.string().min(1).max(63),
6351
+ newType: import_zod9.z.string().min(1),
6090
6352
  /** Optional USING expression for potentially-lossy casts */
6091
- using: import_zod8.z.string().optional()
6353
+ using: import_zod9.z.string().optional()
6092
6354
  }),
6093
- import_zod8.z.object({
6094
- type: import_zod8.z.literal("alterColumnNullable"),
6095
- table: import_zod8.z.string().min(1).max(63),
6096
- column: import_zod8.z.string().min(1).max(63),
6097
- nullable: import_zod8.z.boolean()
6355
+ import_zod9.z.object({
6356
+ type: import_zod9.z.literal("alterColumnNullable"),
6357
+ table: import_zod9.z.string().min(1).max(63),
6358
+ column: import_zod9.z.string().min(1).max(63),
6359
+ nullable: import_zod9.z.boolean()
6098
6360
  }),
6099
- import_zod8.z.object({
6100
- type: import_zod8.z.literal("alterColumnDefault"),
6101
- table: import_zod8.z.string().min(1).max(63),
6102
- column: import_zod8.z.string().min(1).max(63),
6361
+ import_zod9.z.object({
6362
+ type: import_zod9.z.literal("alterColumnDefault"),
6363
+ table: import_zod9.z.string().min(1).max(63),
6364
+ column: import_zod9.z.string().min(1).max(63),
6103
6365
  /** null means drop the default; otherwise a SQL expression string. */
6104
- default: import_zod8.z.string().nullable()
6366
+ default: import_zod9.z.string().nullable()
6105
6367
  }),
6106
6368
  // Indexes
6107
- import_zod8.z.object({
6108
- type: import_zod8.z.literal("addIndex"),
6109
- table: import_zod8.z.string().min(1).max(63),
6110
- name: import_zod8.z.string().min(1).max(63),
6111
- columns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1),
6112
- unique: import_zod8.z.boolean().optional(),
6369
+ import_zod9.z.object({
6370
+ type: import_zod9.z.literal("addIndex"),
6371
+ table: import_zod9.z.string().min(1).max(63),
6372
+ name: import_zod9.z.string().min(1).max(63),
6373
+ columns: import_zod9.z.array(import_zod9.z.string().min(1)).min(1),
6374
+ unique: import_zod9.z.boolean().optional(),
6113
6375
  /** Partial-index WHERE clause, as a raw SQL expression */
6114
- where: import_zod8.z.string().optional(),
6115
- ifNotExists: import_zod8.z.boolean().optional()
6376
+ where: import_zod9.z.string().optional(),
6377
+ ifNotExists: import_zod9.z.boolean().optional()
6116
6378
  }),
6117
- import_zod8.z.object({
6118
- type: import_zod8.z.literal("dropIndex"),
6119
- name: import_zod8.z.string().min(1).max(63),
6120
- cascade: import_zod8.z.boolean().optional(),
6121
- ifExists: import_zod8.z.boolean().optional()
6379
+ import_zod9.z.object({
6380
+ type: import_zod9.z.literal("dropIndex"),
6381
+ name: import_zod9.z.string().min(1).max(63),
6382
+ cascade: import_zod9.z.boolean().optional(),
6383
+ ifExists: import_zod9.z.boolean().optional()
6122
6384
  }),
6123
6385
  // Constraints
6124
- import_zod8.z.object({
6125
- type: import_zod8.z.literal("addForeignKey"),
6126
- table: import_zod8.z.string().min(1).max(63),
6127
- name: import_zod8.z.string().min(1).max(63),
6128
- columns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1),
6129
- refTable: import_zod8.z.string().min(1).max(63),
6130
- refColumns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1),
6386
+ import_zod9.z.object({
6387
+ type: import_zod9.z.literal("addForeignKey"),
6388
+ table: import_zod9.z.string().min(1).max(63),
6389
+ name: import_zod9.z.string().min(1).max(63),
6390
+ columns: import_zod9.z.array(import_zod9.z.string().min(1)).min(1),
6391
+ refTable: import_zod9.z.string().min(1).max(63),
6392
+ refColumns: import_zod9.z.array(import_zod9.z.string().min(1)).min(1),
6131
6393
  onDelete: referentialActionSchema.optional(),
6132
6394
  onUpdate: referentialActionSchema.optional()
6133
6395
  }),
6134
- import_zod8.z.object({
6135
- type: import_zod8.z.literal("addUniqueConstraint"),
6136
- table: import_zod8.z.string().min(1).max(63),
6137
- name: import_zod8.z.string().min(1).max(63),
6138
- columns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1)
6396
+ import_zod9.z.object({
6397
+ type: import_zod9.z.literal("addUniqueConstraint"),
6398
+ table: import_zod9.z.string().min(1).max(63),
6399
+ name: import_zod9.z.string().min(1).max(63),
6400
+ columns: import_zod9.z.array(import_zod9.z.string().min(1)).min(1)
6139
6401
  }),
6140
- import_zod8.z.object({
6141
- type: import_zod8.z.literal("addCheckConstraint"),
6142
- table: import_zod8.z.string().min(1).max(63),
6143
- name: import_zod8.z.string().min(1).max(63),
6144
- expression: import_zod8.z.string().min(1)
6402
+ import_zod9.z.object({
6403
+ type: import_zod9.z.literal("addCheckConstraint"),
6404
+ table: import_zod9.z.string().min(1).max(63),
6405
+ name: import_zod9.z.string().min(1).max(63),
6406
+ expression: import_zod9.z.string().min(1)
6145
6407
  }),
6146
- import_zod8.z.object({
6147
- type: import_zod8.z.literal("dropConstraint"),
6148
- table: import_zod8.z.string().min(1).max(63),
6149
- name: import_zod8.z.string().min(1).max(63),
6150
- cascade: import_zod8.z.boolean().optional(),
6151
- ifExists: import_zod8.z.boolean().optional()
6408
+ import_zod9.z.object({
6409
+ type: import_zod9.z.literal("dropConstraint"),
6410
+ table: import_zod9.z.string().min(1).max(63),
6411
+ name: import_zod9.z.string().min(1).max(63),
6412
+ cascade: import_zod9.z.boolean().optional(),
6413
+ ifExists: import_zod9.z.boolean().optional()
6152
6414
  }),
6153
6415
  // Enum types
6154
- import_zod8.z.object({
6155
- type: import_zod8.z.literal("createEnum"),
6156
- name: import_zod8.z.string().min(1).max(63),
6157
- values: import_zod8.z.array(import_zod8.z.string().min(1)).min(1)
6416
+ import_zod9.z.object({
6417
+ type: import_zod9.z.literal("createEnum"),
6418
+ name: import_zod9.z.string().min(1).max(63),
6419
+ values: import_zod9.z.array(import_zod9.z.string().min(1)).min(1)
6158
6420
  }),
6159
- import_zod8.z.object({
6160
- type: import_zod8.z.literal("alterEnumAddValue"),
6161
- name: import_zod8.z.string().min(1).max(63),
6162
- value: import_zod8.z.string().min(1),
6421
+ import_zod9.z.object({
6422
+ type: import_zod9.z.literal("alterEnumAddValue"),
6423
+ name: import_zod9.z.string().min(1).max(63),
6424
+ value: import_zod9.z.string().min(1),
6163
6425
  /** Position the new value before an existing one (mutually exclusive with `after`) */
6164
- before: import_zod8.z.string().optional(),
6426
+ before: import_zod9.z.string().optional(),
6165
6427
  /** Position the new value after an existing one (mutually exclusive with `before`) */
6166
- after: import_zod8.z.string().optional(),
6167
- ifNotExists: import_zod8.z.boolean().optional()
6428
+ after: import_zod9.z.string().optional(),
6429
+ ifNotExists: import_zod9.z.boolean().optional()
6168
6430
  }),
6169
- import_zod8.z.object({
6170
- type: import_zod8.z.literal("dropEnum"),
6171
- name: import_zod8.z.string().min(1).max(63),
6172
- cascade: import_zod8.z.boolean().optional(),
6173
- ifExists: import_zod8.z.boolean().optional()
6431
+ import_zod9.z.object({
6432
+ type: import_zod9.z.literal("dropEnum"),
6433
+ name: import_zod9.z.string().min(1).max(63),
6434
+ cascade: import_zod9.z.boolean().optional(),
6435
+ ifExists: import_zod9.z.boolean().optional()
6174
6436
  }),
6175
6437
  // Extensions
6176
- import_zod8.z.object({
6177
- type: import_zod8.z.literal("createExtension"),
6178
- name: import_zod8.z.string().min(1),
6179
- ifNotExists: import_zod8.z.boolean().optional()
6438
+ import_zod9.z.object({
6439
+ type: import_zod9.z.literal("createExtension"),
6440
+ name: import_zod9.z.string().min(1),
6441
+ ifNotExists: import_zod9.z.boolean().optional()
6180
6442
  }),
6181
6443
  // Data operations
6182
6444
  /**
@@ -6184,26 +6446,26 @@ var schemaOpSchema = import_zod8.z.discriminatedUnion("type", [
6184
6446
  * destructive — requires explicit confirm to replay on prod. The
6185
6447
  * `description` is what the agent/user sees at confirm time.
6186
6448
  */
6187
- import_zod8.z.object({
6188
- type: import_zod8.z.literal("backfill"),
6189
- description: import_zod8.z.string().min(1),
6190
- sql: import_zod8.z.string().min(1)
6449
+ import_zod9.z.object({
6450
+ type: import_zod9.z.literal("backfill"),
6451
+ description: import_zod9.z.string().min(1),
6452
+ sql: import_zod9.z.string().min(1)
6191
6453
  }),
6192
6454
  /**
6193
6455
  * Escape hatch for DDL the structured ops can't express. `destructive`
6194
6456
  * forces classification — use false only for genuinely additive-only SQL
6195
6457
  * (e.g. CREATE EXTENSION variants the structured op doesn't cover).
6196
6458
  */
6197
- import_zod8.z.object({
6198
- type: import_zod8.z.literal("rawSql"),
6199
- description: import_zod8.z.string().min(1),
6200
- sql: import_zod8.z.string().min(1),
6201
- destructive: import_zod8.z.boolean()
6459
+ import_zod9.z.object({
6460
+ type: import_zod9.z.literal("rawSql"),
6461
+ description: import_zod9.z.string().min(1),
6462
+ sql: import_zod9.z.string().min(1),
6463
+ destructive: import_zod9.z.boolean()
6202
6464
  })
6203
6465
  ]);
6204
6466
 
6205
6467
  // ../../packages/lib/src/server/module-rail/ops.ts
6206
- var import_zod9 = require("zod");
6468
+ var import_zod10 = require("zod");
6207
6469
 
6208
6470
  // ../../packages/lib/src/server/module-rail/reconcile.ts
6209
6471
  var SANDBOX_BATCH_SIZE = 200;
@@ -6233,11 +6495,11 @@ function countedCompositionContext(context) {
6233
6495
  counts.sandboxExecs += 1;
6234
6496
  return context.sandbox.exec(...args);
6235
6497
  },
6236
- readFile: (path5) => {
6498
+ readFile: (path6) => {
6237
6499
  counts.sandboxReads += 1;
6238
- return context.sandbox.readFile(path5);
6500
+ return context.sandbox.readFile(path6);
6239
6501
  },
6240
- fileExists: (path5) => context.sandbox.fileExists(path5),
6502
+ fileExists: (path6) => context.sandbox.fileExists(path6),
6241
6503
  fetchRemoteRef: (...args) => context.sandbox.fetchRemoteRef(...args),
6242
6504
  addDependencies: (specs) => context.sandbox.addDependencies(specs)
6243
6505
  }
@@ -6316,39 +6578,24 @@ async function assertNoRootRoutesWhenMountsEmpty(context, moduleName) {
6316
6578
  );
6317
6579
  }
6318
6580
  }
6319
- async function assertNoEnhancementRoutes(context, moduleName) {
6320
- const enhancementsDir = `${MODULES_SANDBOX_DIR}/${moduleName}/enhancements`;
6321
- const enhancementEntries = await listImmediateEntries(context.sandbox, enhancementsDir, {
6322
- optional: true,
6323
- label: `enhancements for ${moduleName}`
6324
- });
6325
- for (const entry of enhancementEntries) {
6326
- if (entry.kind !== "dir") continue;
6327
- const sliceRoutes = `${enhancementsDir}/${entry.name}/routes`;
6328
- const sliceProbe = await context.sandbox.exec(
6329
- [
6330
- `p=${shellQuote(sliceRoutes)}`,
6331
- `if test -L "$p" || test -e "$p"; then exit 2; else exit 0; fi`
6332
- ].join("\n"),
6333
- { raiseOnError: false }
6334
- );
6335
- if (sliceProbe.exitCode === 2) {
6581
+ async function assertNoEnhancementRoutePaths(context, paths) {
6582
+ const kinds = await classifySandboxPathPresenceKindMany(context, paths, "enhancement-route-kind");
6583
+ for (const sliceRoutes of paths) {
6584
+ const kind = kinds.get(sliceRoutes);
6585
+ if (kind === "absent") continue;
6586
+ if (kind !== "unreadable" && kind !== void 0) {
6336
6587
  throw new Error(
6337
6588
  `[ModuleRail] composition artifacts: enhancement slice routes/ are forbidden at ${sliceRoutes}`
6338
6589
  );
6339
6590
  }
6340
- if (sliceProbe.exitCode !== 0) {
6341
- const detail = typeof sliceProbe.error === "string" && sliceProbe.error.trim() || sliceProbe.output.trim() || `exit ${sliceProbe.exitCode}`;
6342
- throw new Error(
6343
- `[ModuleRail] composition artifacts: cannot probe ${sliceRoutes}: ${redactSecrets(detail)}`
6344
- );
6345
- }
6591
+ throw new Error(
6592
+ `[ModuleRail] composition artifacts: cannot probe ${sliceRoutes}: entry-kind:${kind ?? "missing"}`
6593
+ );
6346
6594
  }
6347
6595
  }
6348
- async function discoverModuleRouteEntries(context, moduleName) {
6596
+ async function discoverRootRouteEntries(context, moduleName) {
6349
6597
  const modulePath = `${MODULES_SANDBOX_DIR}/${moduleName}`;
6350
6598
  const routesDir = `${modulePath}/routes`;
6351
- await assertNoEnhancementRoutes(context, moduleName);
6352
6599
  const quotedRoutes = shellQuote(routesDir);
6353
6600
  const listResult = await context.sandbox.exec(
6354
6601
  [
@@ -6449,14 +6696,14 @@ async function discoverModuleRouteEntries(context, moduleName) {
6449
6696
  }
6450
6697
  return pages.sort((a, b) => a.nextRelativePath.localeCompare(b.nextRelativePath));
6451
6698
  }
6452
- async function writeSandboxFileAtomic(target, path5, content) {
6453
- const dir = dirnamePosix(path5);
6454
- const tmp = `${path5}.tmp.${target.runId}`;
6699
+ async function writeSandboxFileAtomic(target, path6, content) {
6700
+ const dir = dirnamePosix(path6);
6701
+ const tmp = `${path6}.tmp.${target.runId}`;
6455
6702
  const b64 = Buffer.from(content, "utf8").toString("base64");
6456
6703
  const cmd = [
6457
6704
  `mkdir -p ${shellQuote(dir)}`,
6458
6705
  `printf '%s' ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)}`,
6459
- `mv -f ${shellQuote(tmp)} ${shellQuote(path5)}`
6706
+ `mv -f ${shellQuote(tmp)} ${shellQuote(path6)}`
6460
6707
  ].join(" && ");
6461
6708
  const result = await target.sandbox.exec(cmd, { raiseOnError: false });
6462
6709
  if (result.exitCode !== 0) {
@@ -6471,14 +6718,14 @@ async function writeSandboxFileAtomic(target, path5, content) {
6471
6718
  }
6472
6719
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
6473
6720
  throw new Error(
6474
- `[ModuleRail] composition artifacts: failed to write ${redactSecrets(path5)}: ${redactSecrets(detail)}`
6721
+ `[ModuleRail] composition artifacts: failed to write ${redactSecrets(path6)}: ${redactSecrets(detail)}`
6475
6722
  );
6476
6723
  }
6477
6724
  }
6478
6725
  async function collectAbsentWriteParentDirs(context, writePaths) {
6479
6726
  const absent = /* @__PURE__ */ new Set();
6480
- for (const path5 of writePaths) {
6481
- let dir = dirnamePosix(path5);
6727
+ for (const path6 of writePaths) {
6728
+ let dir = dirnamePosix(path6);
6482
6729
  while (dir !== "." && dir !== "") {
6483
6730
  if (absent.has(dir)) {
6484
6731
  dir = dirnamePosix(dir);
@@ -6515,12 +6762,12 @@ async function removeAbsentWriteParentDirsOnRollback(context, dirsDeepestFirst)
6515
6762
  );
6516
6763
  }
6517
6764
  }
6518
- async function deleteSandboxFile(context, path5) {
6519
- const result = await context.sandbox.exec(`rm -f ${shellQuote(path5)}`, { raiseOnError: false });
6765
+ async function deleteSandboxFile(context, path6) {
6766
+ const result = await context.sandbox.exec(`rm -f ${shellQuote(path6)}`, { raiseOnError: false });
6520
6767
  if (result.exitCode !== 0) {
6521
6768
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
6522
6769
  throw new Error(
6523
- `[ModuleRail] composition artifacts: failed to delete ${redactSecrets(path5)}: ${redactSecrets(detail)}`
6770
+ `[ModuleRail] composition artifacts: failed to delete ${redactSecrets(path6)}: ${redactSecrets(detail)}`
6524
6771
  );
6525
6772
  }
6526
6773
  }
@@ -6587,37 +6834,37 @@ async function applyCompositionArtifactMutations(context, plan, existingFiles) {
6587
6834
  await writeSandboxFileAtomic(context, write.path, write.content);
6588
6835
  appliedWrites.push({ path: write.path, previous });
6589
6836
  }
6590
- for (const path5 of plan.deletes) {
6591
- const previous = existingFiles[path5];
6837
+ for (const path6 of plan.deletes) {
6838
+ const previous = existingFiles[path6];
6592
6839
  if (previous == null) continue;
6593
- await deleteSandboxFile(context, path5);
6594
- appliedDeletes.push({ path: path5, previous });
6840
+ await deleteSandboxFile(context, path6);
6841
+ appliedDeletes.push({ path: path6, previous });
6595
6842
  }
6596
6843
  } catch (error) {
6597
6844
  console.log(
6598
6845
  `[ModuleRail] runId=${context.runId} composition artifacts mid-apply failure; rolling back writes=${appliedWrites.length} deletes=${appliedDeletes.length} newDirs=${newlyCreatedParentDirs.length}`
6599
6846
  );
6600
- for (const { path: path5, previous } of [...appliedDeletes].reverse()) {
6847
+ for (const { path: path6, previous } of [...appliedDeletes].reverse()) {
6601
6848
  try {
6602
- await writeSandboxFileAtomic(context, path5, previous);
6849
+ await writeSandboxFileAtomic(context, path6, previous);
6603
6850
  } catch (rollbackError) {
6604
6851
  console.log(
6605
- `[ModuleRail] runId=${context.runId} composition artifacts rollback restore failed for ${redactSecrets(path5)}: ${redactSecrets(
6852
+ `[ModuleRail] runId=${context.runId} composition artifacts rollback restore failed for ${redactSecrets(path6)}: ${redactSecrets(
6606
6853
  rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
6607
6854
  )}`
6608
6855
  );
6609
6856
  }
6610
6857
  }
6611
- for (const { path: path5, previous } of [...appliedWrites].reverse()) {
6858
+ for (const { path: path6, previous } of [...appliedWrites].reverse()) {
6612
6859
  try {
6613
6860
  if (previous == null) {
6614
- await deleteSandboxFile(context, path5);
6861
+ await deleteSandboxFile(context, path6);
6615
6862
  } else {
6616
- await writeSandboxFileAtomic(context, path5, previous);
6863
+ await writeSandboxFileAtomic(context, path6, previous);
6617
6864
  }
6618
6865
  } catch (rollbackError) {
6619
6866
  console.log(
6620
- `[ModuleRail] runId=${context.runId} composition artifacts rollback write failed for ${redactSecrets(path5)}: ${redactSecrets(
6867
+ `[ModuleRail] runId=${context.runId} composition artifacts rollback write failed for ${redactSecrets(path6)}: ${redactSecrets(
6621
6868
  rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
6622
6869
  )}`
6623
6870
  );
@@ -6651,39 +6898,39 @@ async function listExistingGeneratedStubPaths(context) {
6651
6898
  COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN
6652
6899
  );
6653
6900
  const presentPaths = [];
6654
- for (const path5 of candidates) {
6901
+ for (const path6 of candidates) {
6655
6902
  const presence = assertRealCompositionArtifactFilePresence(
6656
- path5,
6657
- kinds.get(path5) ?? "unreadable"
6903
+ path6,
6904
+ kinds.get(path6) ?? "unreadable"
6658
6905
  );
6659
- if (presence === "file") presentPaths.push(path5);
6906
+ if (presence === "file") presentPaths.push(path6);
6660
6907
  }
6661
6908
  let contents;
6662
6909
  try {
6663
6910
  contents = await readFilesMany(context, presentPaths);
6664
6911
  } catch (error) {
6665
- const path5 = readFailurePath(error);
6912
+ const path6 = readFailurePath(error);
6666
6913
  throw new Error(
6667
- `[ModuleRail] composition artifacts: cannot read candidate stub ${redactSecrets(path5)}: ${redactSecrets(
6914
+ `[ModuleRail] composition artifacts: cannot read candidate stub ${redactSecrets(path6)}: ${redactSecrets(
6668
6915
  error instanceof Error ? error.message : String(error)
6669
6916
  )}`
6670
6917
  );
6671
6918
  }
6672
6919
  const owned = [];
6673
- for (const path5 of presentPaths) {
6674
- const content = contents.get(path5);
6920
+ for (const path6 of presentPaths) {
6921
+ const content = contents.get(path6);
6675
6922
  const marker = parseCompositionArtifactMarker(content);
6676
- if (parseLegacyApiAdapter(path5, content) || parseLegacyRouteAdapter(path5, content)) {
6677
- owned.push(path5);
6923
+ if (parseLegacyApiAdapter(path6, content) || parseLegacyRouteAdapter(path6, content)) {
6924
+ owned.push(path6);
6678
6925
  continue;
6679
6926
  }
6680
6927
  if (marker == null) continue;
6681
- if (!markerCongruentWithPath(path5, marker)) {
6928
+ if (!markerCongruentWithPath(path6, marker)) {
6682
6929
  throw new Error(
6683
- `[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)`
6930
+ `[ModuleRail] composition artifacts: incongruent marker at ${redactSecrets(path6)} (found ${marker.kind} marker on a path shaped for ${expectedMarkerKindForPath(path6) ?? "no"} artifacts; refusing to reconcile a swapped marker)`
6684
6931
  );
6685
6932
  }
6686
- owned.push(path5);
6933
+ owned.push(path6);
6687
6934
  }
6688
6935
  return owned;
6689
6936
  }
@@ -6802,7 +7049,8 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6802
7049
  `# Leaf is missing. A missing intermediate parent means the path is simply`,
6803
7050
  `# absent (new nested writes). Only an existing-but-inaccessible parent`,
6804
7051
  `# is unreadable \u2014 never conflate ENOENT parents with EACCES.`,
6805
- `parent=$(dirname -- "$p")`,
7052
+ // Probe paths are nested repo-relative paths; do not spawn dirname per missing file.
7053
+ `parent=\${p%/*}`,
6806
7054
  `if test ! -e "$parent" && test ! -L "$parent"; then`,
6807
7055
  ` printf 'entry-kind:absent\\t%s\\n' "$p"`,
6808
7056
  `elif ls -ld -- "$parent" >/dev/null 2>&1; then`,
@@ -6816,7 +7064,7 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6816
7064
  { raiseOnError: false }
6817
7065
  );
6818
7066
  if (result.exitCode !== 0) {
6819
- for (const path5 of chunk) kinds.set(path5, "unreadable");
7067
+ for (const path6 of chunk) kinds.set(path6, "unreadable");
6820
7068
  continue;
6821
7069
  }
6822
7070
  const lines = result.output.split("\n").filter(Boolean);
@@ -6827,31 +7075,31 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6827
7075
  }
6828
7076
  } else {
6829
7077
  for (const line of lines) {
6830
- const [rawKind, path5] = line.split(" ", 2);
7078
+ const [rawKind, path6] = line.split(" ", 2);
6831
7079
  const kind = rawKind?.replace(/^entry-kind:/, "");
6832
- if (path5 && chunk.includes(path5) && CONVENTIONAL_ENTRY_PRESENCE_KINDS.includes(kind ?? "")) {
6833
- kinds.set(path5, kind);
7080
+ if (path6 && chunk.includes(path6) && CONVENTIONAL_ENTRY_PRESENCE_KINDS.includes(kind ?? "")) {
7081
+ kinds.set(path6, kind);
6834
7082
  }
6835
7083
  }
6836
7084
  }
6837
- for (const path5 of chunk) {
6838
- if (!kinds.has(path5)) kinds.set(path5, "unreadable");
7085
+ for (const path6 of chunk) {
7086
+ if (!kinds.has(path6)) kinds.set(path6, "unreadable");
6839
7087
  }
6840
7088
  }
6841
7089
  return kinds;
6842
7090
  }
6843
- async function classifySandboxPathPresenceKind(context, path5, probeToken) {
6844
- return (await classifySandboxPathPresenceKindMany(context, [path5], probeToken)).get(path5);
7091
+ async function classifySandboxPathPresenceKind(context, path6, probeToken) {
7092
+ return (await classifySandboxPathPresenceKindMany(context, [path6], probeToken)).get(path6);
6845
7093
  }
6846
7094
  var LOCALE_JSON_KIND_PROBE_TOKEN = "locale-json-kind";
6847
- function assertRealLocaleJsonFilePresence(path5, presence) {
7095
+ function assertRealLocaleJsonFilePresence(path6, presence) {
6848
7096
  if (presence === "file") return;
6849
7097
  if (presence === "absent") {
6850
- throw new Error(`[ModuleRail] composition artifacts: locale file missing at ${path5}`);
7098
+ throw new Error(`[ModuleRail] composition artifacts: locale file missing at ${path6}`);
6851
7099
  }
6852
- const presenceError = formatConventionalEntryPresenceError(path5, presence);
7100
+ const presenceError = formatConventionalEntryPresenceError(path6, presence);
6853
7101
  throw new Error(
6854
- `[ModuleRail] composition artifacts: ${presenceError ?? `${path5} is not a real locale JSON file`}`
7102
+ `[ModuleRail] composition artifacts: ${presenceError ?? `${path6} is not a real locale JSON file`}`
6855
7103
  );
6856
7104
  }
6857
7105
  function splitTaggedLine(line) {
@@ -6926,11 +7174,11 @@ async function listLocaleBasenamesIfDirMany(context, dirs) {
6926
7174
  }
6927
7175
  async function readFilesMany(context, paths) {
6928
7176
  const sortedPaths = [...paths].sort((a, b) => a.localeCompare(b));
6929
- const settled = await mapConcurrent(sortedPaths, SANDBOX_READ_CONCURRENCY, async (path5) => {
7177
+ const settled = await mapConcurrent(sortedPaths, SANDBOX_READ_CONCURRENCY, async (path6) => {
6930
7178
  try {
6931
- return { path: path5, value: await context.sandbox.readFile(path5) };
7179
+ return { path: path6, value: await context.sandbox.readFile(path6) };
6932
7180
  } catch (error) {
6933
- return { path: path5, error };
7181
+ return { path: path6, error };
6934
7182
  }
6935
7183
  });
6936
7184
  const files = /* @__PURE__ */ new Map();
@@ -6953,61 +7201,61 @@ function readFailurePath(error) {
6953
7201
  var CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN = "conventional-entry-kind";
6954
7202
  var SERVER_REGISTRATIONS_FILE = "server/registrations.ts";
6955
7203
  var COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN = "composition-artifact-kind";
6956
- async function classifyCompositionArtifactPathKind(context, path5) {
6957
- return classifySandboxPathPresenceKind(context, path5, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
7204
+ async function classifyCompositionArtifactPathKind(context, path6) {
7205
+ return classifySandboxPathPresenceKind(context, path6, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
6958
7206
  }
6959
- function assertRealCompositionArtifactFilePresence(path5, presence) {
7207
+ function assertRealCompositionArtifactFilePresence(path6, presence) {
6960
7208
  if (presence === "file") return "file";
6961
7209
  if (presence === "absent") return "absent";
6962
- const presenceError = formatConventionalEntryPresenceError(path5, presence);
7210
+ const presenceError = formatConventionalEntryPresenceError(path6, presence);
6963
7211
  throw new Error(
6964
- `[ModuleRail] composition artifacts: ${presenceError ?? `${path5} is not a real composition artifact file`}`
7212
+ `[ModuleRail] composition artifacts: ${presenceError ?? `${path6} is not a real composition artifact file`}`
6965
7213
  );
6966
7214
  }
6967
- async function readConventionalEntryIfRealFile(path5, kind, presences, files) {
6968
- const presence = presences.get(path5) ?? "unreadable";
6969
- const presenceError = formatConventionalEntryPresenceError(path5, presence);
7215
+ async function readConventionalEntryIfRealFile(path6, kind, presences, files) {
7216
+ const presence = presences.get(path6) ?? "unreadable";
7217
+ const presenceError = formatConventionalEntryPresenceError(path6, presence);
6970
7218
  if (presenceError) {
6971
7219
  throw new Error(`[ModuleRail] composition artifacts: ${presenceError}`);
6972
7220
  }
6973
7221
  if (presence === "absent") {
6974
7222
  return null;
6975
7223
  }
6976
- const source = files.get(path5);
7224
+ const source = files.get(path6);
6977
7225
  switch (kind) {
6978
7226
  case "contributions":
6979
- assertContributionsExport(source, path5);
6980
- assertClientSafeConventionalEntryImports(source, path5, "contributions");
7227
+ assertContributionsExport(source, path6);
7228
+ assertClientSafeConventionalEntryImports(source, path6, "contributions");
6981
7229
  break;
6982
7230
  case "slotCatalogs":
6983
- assertSlotCatalogsExport(source, path5);
6984
- assertClientSafeConventionalEntryImports(source, path5, "slotCatalogs");
7231
+ assertSlotCatalogsExport(source, path6);
7232
+ assertClientSafeConventionalEntryImports(source, path6, "slotCatalogs");
6985
7233
  break;
6986
7234
  case "initializeEnhancement":
6987
- assertInitializeEnhancementExport(source, path5);
7235
+ assertInitializeEnhancementExport(source, path6);
6988
7236
  break;
6989
7237
  case "initializeModule":
6990
- assertInitializeModuleExport(source, path5);
7238
+ assertInitializeModuleExport(source, path6);
6991
7239
  break;
6992
7240
  }
6993
7241
  return source;
6994
7242
  }
6995
7243
  function hasRealServerRegistrations(modulePath, presences) {
6996
- const path5 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
6997
- return assertRealCompositionArtifactFilePresence(path5, presences.get(path5) ?? "unreadable") === "file";
7244
+ const path6 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
7245
+ return assertRealCompositionArtifactFilePresence(path6, presences.get(path6) ?? "unreadable") === "file";
6998
7246
  }
6999
- async function readJsonObjectFile(path5, files) {
7000
- const raw = files.get(path5);
7247
+ async function readJsonObjectFile(path6, files) {
7248
+ const raw = files.get(path6);
7001
7249
  let parsed;
7002
7250
  try {
7003
7251
  parsed = JSON.parse(raw);
7004
7252
  } catch (error) {
7005
7253
  throw new Error(
7006
- `[ModuleRail] composition artifacts: invalid JSON at ${path5}: ${error instanceof Error ? error.message : String(error)}`
7254
+ `[ModuleRail] composition artifacts: invalid JSON at ${path6}: ${error instanceof Error ? error.message : String(error)}`
7007
7255
  );
7008
7256
  }
7009
7257
  if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
7010
- throw new Error(`[ModuleRail] composition artifacts: ${path5} root must be an object`);
7258
+ throw new Error(`[ModuleRail] composition artifacts: ${path6} root must be an object`);
7011
7259
  }
7012
7260
  return parsed;
7013
7261
  }
@@ -7060,11 +7308,11 @@ async function collectCompositionEntryFacts(context, registry) {
7060
7308
  classifySandboxPathPresenceKindMany(context, localePaths, LOCALE_JSON_KIND_PROBE_TOKEN)
7061
7309
  ]);
7062
7310
  const presentConventionalPaths = [];
7063
- for (const path5 of conventionalPaths) {
7064
- const presence = conventionalPresences.get(path5) ?? "unreadable";
7065
- if (presence === "file") presentConventionalPaths.push(path5);
7311
+ for (const path6 of conventionalPaths) {
7312
+ const presence = conventionalPresences.get(path6) ?? "unreadable";
7313
+ if (presence === "file") presentConventionalPaths.push(path6);
7066
7314
  }
7067
- const presentLocalePaths = localePaths.filter((path5) => localePresences.get(path5) === "file");
7315
+ const presentLocalePaths = localePaths.filter((path6) => localePresences.get(path6) === "file");
7068
7316
  const files = await readFilesMany(context, [...presentConventionalPaths, ...presentLocalePaths]);
7069
7317
  const substrateLocales = [];
7070
7318
  for (const locale of substrateNames) {
@@ -7174,8 +7422,8 @@ async function collectCompositionEntryFacts(context, registry) {
7174
7422
  });
7175
7423
  if (rootLocales.length > 0) {
7176
7424
  for (const locale of rootLocales) {
7177
- const path5 = `${modulePath}/i18n/${locale}.json`;
7178
- assertRealLocaleJsonFilePresence(path5, localePresences.get(path5) ?? "unreadable");
7425
+ const path6 = `${modulePath}/i18n/${locale}.json`;
7426
+ assertRealLocaleJsonFilePresence(path6, localePresences.get(path6) ?? "unreadable");
7179
7427
  }
7180
7428
  const enMessages = await readJsonObjectFile(`${modulePath}/i18n/en.json`, files);
7181
7429
  const localeMessages = { en: enMessages };
@@ -7199,8 +7447,8 @@ async function collectCompositionEntryFacts(context, registry) {
7199
7447
  if (!enh.active || enh.locales.length === 0) continue;
7200
7448
  const peerDir = `${modulePath}/enhancements/${enh.peer}`;
7201
7449
  for (const locale of enh.locales) {
7202
- const path5 = `${peerDir}/i18n/${locale}.json`;
7203
- assertRealLocaleJsonFilePresence(path5, localePresences.get(path5) ?? "unreadable");
7450
+ const path6 = `${peerDir}/i18n/${locale}.json`;
7451
+ assertRealLocaleJsonFilePresence(path6, localePresences.get(path6) ?? "unreadable");
7204
7452
  }
7205
7453
  const enMessages = await readJsonObjectFile(`${peerDir}/i18n/en.json`, files);
7206
7454
  const localeMessages = { en: enMessages };
@@ -7347,14 +7595,21 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7347
7595
  manifestsByName
7348
7596
  );
7349
7597
  const routeEntriesByOwner = {};
7598
+ await assertNoEnhancementRoutePaths(
7599
+ context,
7600
+ moduleRuntimeFacts.flatMap(
7601
+ ({ moduleName, onDiskEnhancementDirs }) => onDiskEnhancementDirs.map(
7602
+ (peer) => `${MODULES_SANDBOX_DIR}/${moduleName}/enhancements/${peer}/routes`
7603
+ )
7604
+ )
7605
+ );
7350
7606
  for (const moduleName of composedNames) {
7351
7607
  const manifest = manifestsByName[moduleName];
7352
- await assertNoEnhancementRoutes(context, moduleName);
7353
7608
  if (manifest.mounts.length === 0) {
7354
7609
  await assertNoRootRoutesWhenMountsEmpty(context, moduleName);
7355
7610
  routeEntriesByOwner[moduleName] = [];
7356
7611
  } else {
7357
- routeEntriesByOwner[moduleName] = await discoverModuleRouteEntries(context, moduleName);
7612
+ routeEntriesByOwner[moduleName] = await discoverRootRouteEntries(context, moduleName);
7358
7613
  }
7359
7614
  }
7360
7615
  const existingGeneratedPaths = await listExistingGeneratedStubPaths(context);
@@ -7401,16 +7656,16 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7401
7656
  for (const stub of probePlan.plan.routeStubs) candidatePaths.add(stub.path);
7402
7657
  const existingFiles = {};
7403
7658
  const remainingCandidatePaths = [];
7404
- for (const path5 of [...candidatePaths].sort((a, b) => a.localeCompare(b))) {
7405
- if (Object.prototype.hasOwnProperty.call(existingAppPages, path5)) {
7406
- existingFiles[path5] = existingAppPages[path5];
7659
+ for (const path6 of [...candidatePaths].sort((a, b) => a.localeCompare(b))) {
7660
+ if (Object.prototype.hasOwnProperty.call(existingAppPages, path6)) {
7661
+ existingFiles[path6] = existingAppPages[path6];
7407
7662
  continue;
7408
7663
  }
7409
- if (Object.prototype.hasOwnProperty.call(existingAppRoutes, path5)) {
7410
- existingFiles[path5] = existingAppRoutes[path5];
7664
+ if (Object.prototype.hasOwnProperty.call(existingAppRoutes, path6)) {
7665
+ existingFiles[path6] = existingAppRoutes[path6];
7411
7666
  continue;
7412
7667
  }
7413
- remainingCandidatePaths.push(path5);
7668
+ remainingCandidatePaths.push(path6);
7414
7669
  }
7415
7670
  const candidateKinds = await classifySandboxPathPresenceKindMany(
7416
7671
  context,
@@ -7418,12 +7673,12 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7418
7673
  COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN
7419
7674
  );
7420
7675
  const presentCandidatePaths = [];
7421
- for (const path5 of remainingCandidatePaths) {
7676
+ for (const path6 of remainingCandidatePaths) {
7422
7677
  const presence = assertRealCompositionArtifactFilePresence(
7423
- path5,
7424
- candidateKinds.get(path5) ?? "unreadable"
7678
+ path6,
7679
+ candidateKinds.get(path6) ?? "unreadable"
7425
7680
  );
7426
- if (presence === "file") presentCandidatePaths.push(path5);
7681
+ if (presence === "file") presentCandidatePaths.push(path6);
7427
7682
  }
7428
7683
  try {
7429
7684
  Object.assign(
@@ -7431,9 +7686,9 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7431
7686
  Object.fromEntries(await readFilesMany(context, presentCandidatePaths))
7432
7687
  );
7433
7688
  } catch (error) {
7434
- const path5 = readFailurePath(error);
7689
+ const path6 = readFailurePath(error);
7435
7690
  throw new Error(
7436
- `[ModuleRail] composition artifacts: cannot read ${redactSecrets(path5)} during preflight: ${redactSecrets(
7691
+ `[ModuleRail] composition artifacts: cannot read ${redactSecrets(path6)} during preflight: ${redactSecrets(
7437
7692
  error instanceof Error ? error.message : String(error)
7438
7693
  )}`
7439
7694
  );
@@ -7506,8 +7761,8 @@ var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
7506
7761
  var import_typescript6 = __toESM(require("typescript"));
7507
7762
 
7508
7763
  // src/module-boundaries.ts
7509
- var import_node_fs2 = __toESM(require("fs"));
7510
- var import_node_path2 = __toESM(require("path"));
7764
+ var import_node_fs3 = __toESM(require("fs"));
7765
+ var import_node_path3 = __toESM(require("path"));
7511
7766
  var import_typescript5 = __toESM(require("typescript"));
7512
7767
  var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
7513
7768
  var RESOLVE_SUFFIXES = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
@@ -7526,26 +7781,26 @@ function dataPathsIn(fileName, source) {
7526
7781
  }
7527
7782
  function scanModule(modulesDir, module2) {
7528
7783
  const scanned = /* @__PURE__ */ new Map();
7529
- const moduleDir = import_node_path2.default.join(modulesDir, module2);
7530
- if (!import_node_fs2.default.existsSync(moduleDir)) return scanned;
7531
- const files = import_node_fs2.default.readdirSync(moduleDir, { recursive: true, encoding: "utf8" }).filter((relative) => SOURCE_FILE.test(relative) && !relative.includes("node_modules"));
7784
+ const moduleDir = import_node_path3.default.join(modulesDir, module2);
7785
+ if (!import_node_fs3.default.existsSync(moduleDir)) return scanned;
7786
+ const files = import_node_fs3.default.readdirSync(moduleDir, { recursive: true, encoding: "utf8" }).filter((relative) => SOURCE_FILE.test(relative) && !relative.includes("node_modules"));
7532
7787
  for (const relative of files) {
7533
- const absolute = import_node_path2.default.join(moduleDir, relative);
7534
- if (!import_node_fs2.default.statSync(absolute).isFile()) continue;
7535
- const source = import_node_fs2.default.readFileSync(absolute, "utf8");
7788
+ const absolute = import_node_path3.default.join(moduleDir, relative);
7789
+ if (!import_node_fs3.default.statSync(absolute).isFile()) continue;
7790
+ const source = import_node_fs3.default.readFileSync(absolute, "utf8");
7536
7791
  const specifiers = import_typescript5.default.preProcessFile(source, true, true).importedFiles.map((f) => f.fileName);
7537
7792
  const dataPaths = dataPathsIn(absolute, source);
7538
7793
  const targets = [...specifiers, ...dataPaths].flatMap((spec) => {
7539
7794
  if (spec.startsWith("@/modules/"))
7540
- return [import_node_path2.default.join(modulesDir, spec.slice("@/modules/".length))];
7795
+ return [import_node_path3.default.join(modulesDir, spec.slice("@/modules/".length))];
7541
7796
  if (spec.startsWith("./") || spec.startsWith("../")) {
7542
- return [import_node_path2.default.resolve(import_node_path2.default.dirname(absolute), spec)];
7797
+ return [import_node_path3.default.resolve(import_node_path3.default.dirname(absolute), spec)];
7543
7798
  }
7544
7799
  return [];
7545
7800
  });
7546
7801
  scanned.set(absolute, {
7547
7802
  module: module2,
7548
- relative: relative.split(import_node_path2.default.sep).join("/"),
7803
+ relative: relative.split(import_node_path3.default.sep).join("/"),
7549
7804
  targets,
7550
7805
  dataPaths
7551
7806
  });
@@ -7562,7 +7817,7 @@ function findModuleBoundaryViolations(modulesDir, peersByModule, { pruning = fal
7562
7817
  (q) => file.relative.startsWith(`enhancements/${q}/`)
7563
7818
  );
7564
7819
  const goneWith = (target, peer) => {
7565
- const relative = import_node_path2.default.relative(modulesDir, target).split(import_node_path2.default.sep).join("/");
7820
+ const relative = import_node_path3.default.relative(modulesDir, target).split(import_node_path3.default.sep).join("/");
7566
7821
  return relative === peer || relative.startsWith(`${peer}/`) || new RegExp(`^[^/]+/enhancements/${peer}(/|$)`).test(relative);
7567
7822
  };
7568
7823
  const tainted = /* @__PURE__ */ new Map();
@@ -7608,12 +7863,12 @@ function findModuleBoundaryViolations(modulesDir, peersByModule, { pruning = fal
7608
7863
  return violations.sort((a, b) => a.file.localeCompare(b.file) || a.peer.localeCompare(b.peer));
7609
7864
  }
7610
7865
  function findOptionalPeerViolations(modulesDir) {
7611
- if (!import_node_fs2.default.existsSync(modulesDir)) return [];
7866
+ if (!import_node_fs3.default.existsSync(modulesDir)) return [];
7612
7867
  const manifests = {};
7613
- for (const name of import_node_fs2.default.readdirSync(modulesDir)) {
7614
- const manifestPath = import_node_path2.default.join(modulesDir, name, "module.json");
7615
- if (!import_node_fs2.default.existsSync(manifestPath)) continue;
7616
- manifests[name] = JSON.parse(import_node_fs2.default.readFileSync(manifestPath, "utf8"));
7868
+ for (const name of import_node_fs3.default.readdirSync(modulesDir)) {
7869
+ const manifestPath = import_node_path3.default.join(modulesDir, name, "module.json");
7870
+ if (!import_node_fs3.default.existsSync(manifestPath)) continue;
7871
+ manifests[name] = JSON.parse(import_node_fs3.default.readFileSync(manifestPath, "utf8"));
7617
7872
  }
7618
7873
  const features = Object.keys(manifests).filter((name) => manifests[name].kind === "feature");
7619
7874
  const peersByModule = {};
@@ -7633,11 +7888,11 @@ function formatBoundaryViolations(modulesDirLabel, violations) {
7633
7888
  // src/index.ts
7634
7889
  var MODULES_DIR2 = MODULES_SANDBOX_DIR;
7635
7890
  function readComposeInputs(cwd) {
7636
- const file = import_node_path3.default.join(cwd, COMPOSE_INPUTS_PATH);
7637
- if (!import_node_fs3.default.existsSync(file)) return { connectedStores: [], installs: {} };
7891
+ const file = import_node_path4.default.join(cwd, COMPOSE_INPUTS_PATH);
7892
+ if (!import_node_fs4.default.existsSync(file)) return { connectedStores: [], installs: {} };
7638
7893
  let raw;
7639
7894
  try {
7640
- raw = JSON.parse(import_node_fs3.default.readFileSync(file, "utf8"));
7895
+ raw = JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"));
7641
7896
  } catch (error) {
7642
7897
  throw new Error(
7643
7898
  `[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
@@ -7679,18 +7934,18 @@ function stubGitignoreEntries(artifactPaths) {
7679
7934
  function writeStubGitignore(cwd, artifactPaths) {
7680
7935
  const entries = stubGitignoreEntries(artifactPaths);
7681
7936
  const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
7682
- const file = import_node_path3.default.join(cwd, STUB_GITIGNORE_PATH);
7683
- const existing = import_node_fs3.default.existsSync(file) ? import_node_fs3.default.readFileSync(file, "utf8") : null;
7937
+ const file = import_node_path4.default.join(cwd, STUB_GITIGNORE_PATH);
7938
+ const existing = import_node_fs4.default.existsSync(file) ? import_node_fs4.default.readFileSync(file, "utf8") : null;
7684
7939
  if (existing !== content) {
7685
- import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(file), { recursive: true });
7686
- import_node_fs3.default.writeFileSync(file, content);
7940
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
7941
+ import_node_fs4.default.writeFileSync(file, content);
7687
7942
  }
7688
7943
  return entries.length;
7689
7944
  }
7690
7945
  function assertStubGitignoreOwned(cwd) {
7691
- const file = import_node_path3.default.join(cwd, STUB_GITIGNORE_PATH);
7692
- if (!import_node_fs3.default.existsSync(file)) return;
7693
- const firstLine = import_node_fs3.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
7946
+ const file = import_node_path4.default.join(cwd, STUB_GITIGNORE_PATH);
7947
+ if (!import_node_fs4.default.existsSync(file)) return;
7948
+ const firstLine = import_node_fs4.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
7694
7949
  if (firstLine !== STUB_GITIGNORE_HEADER) {
7695
7950
  throw new Error(
7696
7951
  `[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
@@ -7698,8 +7953,8 @@ function assertStubGitignoreOwned(cwd) {
7698
7953
  }
7699
7954
  }
7700
7955
  function assertAppRoot(cwd) {
7701
- const appPath = import_node_path3.default.join(cwd, APP_PACKAGE_JSON);
7702
- if (!import_node_fs3.default.existsSync(appPath)) {
7956
+ const appPath = import_node_path4.default.join(cwd, APP_PACKAGE_JSON);
7957
+ if (!import_node_fs4.default.existsSync(appPath)) {
7703
7958
  throw new Error(
7704
7959
  `[compose] ${appPath} not found \u2014 run this from the repo root of a Stardeck app`
7705
7960
  );
@@ -7745,9 +8000,9 @@ ${porcelain.trim()}`
7745
8000
  }
7746
8001
  var APP_SRC_DIR = "apps/web/src";
7747
8002
  function walkSources(dir, skip, out) {
7748
- if (!import_node_fs3.default.existsSync(dir)) return;
7749
- for (const entry of import_node_fs3.default.readdirSync(dir, { withFileTypes: true })) {
7750
- const full = import_node_path3.default.join(dir, entry.name);
8003
+ if (!import_node_fs4.default.existsSync(dir)) return;
8004
+ for (const entry of import_node_fs4.default.readdirSync(dir, { withFileTypes: true })) {
8005
+ const full = import_node_path4.default.join(dir, entry.name);
7751
8006
  if (full === skip) continue;
7752
8007
  if (entry.isDirectory()) walkSources(full, skip, out);
7753
8008
  else if (/\.[cm]?[jt]sx?$/.test(entry.name)) out.push(full);
@@ -7756,13 +8011,13 @@ function walkSources(dir, skip, out) {
7756
8011
  function assertNoAppLayerImporters(cwd, excluded) {
7757
8012
  if (excluded.length === 0) return;
7758
8013
  const files = [];
7759
- walkSources(import_node_path3.default.join(cwd, APP_SRC_DIR), import_node_path3.default.join(cwd, MODULES_DIR2), files);
8014
+ walkSources(import_node_path4.default.join(cwd, APP_SRC_DIR), import_node_path4.default.join(cwd, MODULES_DIR2), files);
7760
8015
  const offenders = [];
7761
8016
  for (const file of files) {
7762
- const source = import_node_fs3.default.readFileSync(file, "utf8");
8017
+ const source = import_node_fs4.default.readFileSync(file, "utf8");
7763
8018
  for (const name of excluded) {
7764
8019
  if (new RegExp(`@/modules/${name}(?=["'\`/])`).test(source)) {
7765
- offenders.push(`${import_node_path3.default.relative(cwd, file)}: ${name}`);
8020
+ offenders.push(`${import_node_path4.default.relative(cwd, file)}: ${name}`);
7766
8021
  }
7767
8022
  }
7768
8023
  }
@@ -7796,8 +8051,8 @@ var BUILD_TYPECHECK_SKIP = [
7796
8051
  "playwright.config.ts"
7797
8052
  ];
7798
8053
  function writeBuildTsconfig(cwd, excluded) {
7799
- const baseFile = import_node_path3.default.join(cwd, TSCONFIG_PATH);
7800
- if (!import_node_fs3.default.existsSync(import_node_path3.default.join(cwd, MODULES_DIR2)) || !import_node_fs3.default.existsSync(baseFile)) return;
8054
+ const baseFile = import_node_path4.default.join(cwd, TSCONFIG_PATH);
8055
+ if (!import_node_fs4.default.existsSync(import_node_path4.default.join(cwd, MODULES_DIR2)) || !import_node_fs4.default.existsSync(baseFile)) return;
7801
8056
  const base = import_typescript6.default.readConfigFile(baseFile, import_typescript6.default.sys.readFile);
7802
8057
  if (base.error) {
7803
8058
  throw new Error(
@@ -7805,7 +8060,7 @@ function writeBuildTsconfig(cwd, excluded) {
7805
8060
  );
7806
8061
  }
7807
8062
  const baseExclude = base.config?.exclude ?? ["node_modules"];
7808
- const modulesDir = import_node_path3.default.posix.relative("apps/web", MODULES_DIR2);
8063
+ const modulesDir = import_node_path4.default.posix.relative("apps/web", MODULES_DIR2);
7809
8064
  const config = {
7810
8065
  extends: "./tsconfig.json",
7811
8066
  exclude: [
@@ -7820,17 +8075,17 @@ function writeBuildTsconfig(cwd, excluded) {
7820
8075
  const content = `${BUILD_TSCONFIG_HEADER}
7821
8076
  ${JSON.stringify(config, null, 2)}
7822
8077
  `;
7823
- const file = import_node_path3.default.join(cwd, BUILD_TSCONFIG_PATH);
7824
- if (!import_node_fs3.default.existsSync(file) || import_node_fs3.default.readFileSync(file, "utf8") !== content) {
7825
- import_node_fs3.default.writeFileSync(file, content);
8078
+ const file = import_node_path4.default.join(cwd, BUILD_TSCONFIG_PATH);
8079
+ if (!import_node_fs4.default.existsSync(file) || import_node_fs4.default.readFileSync(file, "utf8") !== content) {
8080
+ import_node_fs4.default.writeFileSync(file, content);
7826
8081
  }
7827
8082
  console.log(
7828
8083
  `[compose] ${BUILD_TSCONFIG_PATH} excludes ${excluded.length} Module(s): ${excluded.join(",") || "none"}`
7829
8084
  );
7830
8085
  }
7831
8086
  function removeDir(absolute) {
7832
- if (!import_node_fs3.default.existsSync(absolute)) return false;
7833
- import_node_fs3.default.rmSync(absolute, { recursive: true, force: true });
8087
+ if (!import_node_fs4.default.existsSync(absolute)) return false;
8088
+ import_node_fs4.default.rmSync(absolute, { recursive: true, force: true });
7834
8089
  return true;
7835
8090
  }
7836
8091
  async function compose(options) {
@@ -7858,7 +8113,7 @@ async function compose(options) {
7858
8113
  });
7859
8114
  assertNoAppLayerImporters(options.cwd, preview.excludedModules);
7860
8115
  const violations = findModuleBoundaryViolations(
7861
- import_node_path3.default.join(options.cwd, MODULES_DIR2),
8116
+ import_node_path4.default.join(options.cwd, MODULES_DIR2),
7862
8117
  Object.fromEntries(preview.composedModules.map((name) => [name, preview.excludedModules])),
7863
8118
  { pruning: true }
7864
8119
  );
@@ -7879,11 +8134,11 @@ async function compose(options) {
7879
8134
  let pruned = 0;
7880
8135
  if (options.prune) {
7881
8136
  for (const name of result.excludedModules) {
7882
- if (removeDir(import_node_path3.default.join(options.cwd, MODULES_DIR2, name))) pruned++;
8137
+ if (removeDir(import_node_path4.default.join(options.cwd, MODULES_DIR2, name))) pruned++;
7883
8138
  }
7884
8139
  for (const owner of result.composedModules) {
7885
8140
  for (const peer of result.excludedModules) {
7886
- if (removeDir(import_node_path3.default.join(options.cwd, MODULES_DIR2, owner, "enhancements", peer))) pruned++;
8141
+ if (removeDir(import_node_path4.default.join(options.cwd, MODULES_DIR2, owner, "enhancements", peer))) pruned++;
7887
8142
  }
7888
8143
  }
7889
8144
  }
@@ -7905,13 +8160,18 @@ async function main() {
7905
8160
  enabled: { type: "string" },
7906
8161
  prune: { type: "boolean", default: false },
7907
8162
  cwd: { type: "string" },
7908
- "check-boundaries": { type: "boolean", default: false }
8163
+ "check-boundaries": { type: "boolean", default: false },
8164
+ "check-ownership": { type: "boolean", default: false }
7909
8165
  },
7910
8166
  strict: true
7911
8167
  });
8168
+ if (values["check-ownership"]) {
8169
+ await checkOwnership(values.cwd ?? process.cwd());
8170
+ return;
8171
+ }
7912
8172
  if (values["check-boundaries"]) {
7913
8173
  const violations = findOptionalPeerViolations(
7914
- import_node_path4.default.join(values.cwd ?? process.cwd(), MODULES_SANDBOX_DIR)
8174
+ import_node_path5.default.join(values.cwd ?? process.cwd(), MODULES_SANDBOX_DIR)
7915
8175
  );
7916
8176
  if (violations.length > 0) {
7917
8177
  throw new Error(
@@ -7936,6 +8196,36 @@ async function main() {
7936
8196
  `[compose] modules=${result.composedModules.join(",") || "none"} excluded=${result.excludedModules.join(",") || "none"} writes=${result.writes} deletes=${result.deletes} pruned=${result.pruned} gitignore=${result.gitignoreEntries ?? "kept"} stub paths`
7937
8197
  );
7938
8198
  }
8199
+ function gitAt(cwd) {
8200
+ return (args) => new Promise((resolve, reject) => {
8201
+ const child = (0, import_node_child_process3.spawn)("git", ["-c", "core.quotePath=false", ...args], {
8202
+ cwd,
8203
+ stdio: ["ignore", "pipe", "pipe"]
8204
+ });
8205
+ let stdout = "";
8206
+ child.stdout.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
8207
+ child.on("error", reject);
8208
+ child.on("close", (status) => resolve({ status: status ?? 1, stdout }));
8209
+ });
8210
+ }
8211
+ async function checkOwnership(cwd) {
8212
+ const repoRoot = import_node_path5.default.resolve(cwd);
8213
+ const result = await checkStardeckOwnedFiles({ repoRoot, git: gitAt(repoRoot) });
8214
+ switch (result.status) {
8215
+ case "skipped":
8216
+ if (result.warning)
8217
+ console.warn(`[compose] --check-ownership: not checked: ${result.reason}`);
8218
+ else console.log(`[compose] --check-ownership: skipped: ${result.reason}`);
8219
+ return;
8220
+ case "clean":
8221
+ console.log(
8222
+ `[compose] --check-ownership: no Stardeck-owned file changed from Stardeck's version (${result.release.slice(0, 12)})`
8223
+ );
8224
+ return;
8225
+ default:
8226
+ throw new Error(`[compose] --check-ownership: ${formatOwnershipGuardFailure(result)}`);
8227
+ }
8228
+ }
7939
8229
  main().catch((error) => {
7940
8230
  console.error(error instanceof Error ? error.message : String(error));
7941
8231
  process.exit(1);