@jskit-ai/jskit-cli 0.2.72 → 0.2.74

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-cli",
3
- "version": "0.2.72",
3
+ "version": "0.2.74",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,9 +20,9 @@
20
20
  "test": "node --test"
21
21
  },
22
22
  "dependencies": {
23
- "@jskit-ai/jskit-catalog": "0.1.71",
24
- "@jskit-ai/kernel": "0.1.63",
25
- "@jskit-ai/shell-web": "0.1.62"
23
+ "@jskit-ai/jskit-catalog": "0.1.73",
24
+ "@jskit-ai/kernel": "0.1.65",
25
+ "@jskit-ai/shell-web": "0.1.64"
26
26
  },
27
27
  "engines": {
28
28
  "node": "20.x"
@@ -14,10 +14,11 @@ import {
14
14
  } from "../core/commandCatalog.js";
15
15
 
16
16
  const WRAPPER_COMMANDS = new Set(["npx", "jsx"]);
17
- const KNOWN_GENERATE_FLAG_OPTIONS = Object.freeze(["dry-run", "run-npm-install", "json", "verbose"]);
17
+ const KNOWN_GENERATE_FLAG_OPTIONS = Object.freeze(["dry-run", "run-npm-install", "devlinks", "json", "verbose"]);
18
18
  const BOOLEAN_OPTION_NAMES = new Set([
19
19
  "dry-run",
20
20
  "run-npm-install",
21
+ "devlinks",
21
22
  "full",
22
23
  "expanded",
23
24
  "details",
@@ -747,6 +748,7 @@ function buildCommandOptionMeta(command = "", catalogModule) {
747
748
  const labels = {
748
749
  dryRun: "dry-run",
749
750
  runNpmInstall: "run-npm-install",
751
+ devlinks: "devlinks",
750
752
  full: "full",
751
753
  expanded: "expanded",
752
754
  details: "details",
@@ -61,6 +61,54 @@ function validateFileMutationShape(descriptor, descriptorPath) {
61
61
  }
62
62
  }
63
63
 
64
+ function validateLifecycleHookSpec(spec = {}, descriptorPath, label = "lifecycle hook") {
65
+ const normalized = ensureObject(spec);
66
+ if (Object.keys(normalized).length < 1) {
67
+ return null;
68
+ }
69
+
70
+ const entrypoint = String(normalized.entrypoint || "").trim();
71
+ if (!entrypoint) {
72
+ throw createCliError(`Invalid package descriptor at ${descriptorPath}: ${label} requires "entrypoint".`);
73
+ }
74
+
75
+ const exportName = String(normalized.export || "").trim() || "default";
76
+ return {
77
+ ...normalized,
78
+ entrypoint,
79
+ export: exportName
80
+ };
81
+ }
82
+
83
+ function validateLifecycleShape(descriptor, descriptorPath) {
84
+ const lifecycle = ensureObject(ensureObject(descriptor).lifecycle);
85
+ const install = ensureObject(lifecycle.install);
86
+ if (Object.keys(install).length < 1) {
87
+ return lifecycle;
88
+ }
89
+
90
+ const prepare = validateLifecycleHookSpec(install.prepare, descriptorPath, "lifecycle.install.prepare");
91
+ const finalize = validateLifecycleHookSpec(install.finalize, descriptorPath, "lifecycle.install.finalize");
92
+
93
+ if (install.finalize && typeof install.finalize === "object") {
94
+ const managesNpmInstall = install.finalize.managesNpmInstall;
95
+ if (typeof managesNpmInstall !== "undefined" && typeof managesNpmInstall !== "boolean") {
96
+ throw createCliError(
97
+ `Invalid package descriptor at ${descriptorPath}: lifecycle.install.finalize.managesNpmInstall must be boolean when provided.`
98
+ );
99
+ }
100
+ }
101
+
102
+ return {
103
+ ...lifecycle,
104
+ install: {
105
+ ...install,
106
+ ...(prepare ? { prepare } : {}),
107
+ ...(finalize ? { finalize } : {})
108
+ }
109
+ };
110
+ }
111
+
64
112
  function validatePackageDescriptorShape(descriptor, descriptorPath) {
65
113
  const normalized = ensureObject(descriptor);
66
114
  const packageId = String(normalized.packageId || "").trim();
@@ -85,9 +133,11 @@ function validatePackageDescriptorShape(descriptor, descriptorPath) {
85
133
  }
86
134
 
87
135
  validateFileMutationShape(normalized, descriptorPath);
136
+ const lifecycle = validateLifecycleShape(normalized, descriptorPath);
88
137
 
89
138
  return {
90
139
  ...normalized,
140
+ lifecycle,
91
141
  kind: normalizePackageKind(normalized.kind, descriptorPath)
92
142
  };
93
143
  }
@@ -115,11 +165,13 @@ function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { ex
115
165
  }
116
166
 
117
167
  validateFileMutationShape(normalized, descriptorPath);
168
+ const lifecycle = validateLifecycleShape(normalized, descriptorPath);
118
169
 
119
170
  return {
120
171
  ...normalized,
121
172
  packageId,
122
173
  version,
174
+ lifecycle,
123
175
  kind: normalizePackageKind(normalized.kind, descriptorPath)
124
176
  };
125
177
  }
@@ -198,6 +198,7 @@ async function applyFileMutations(
198
198
  warnings = [],
199
199
  existingManagedFiles = [],
200
200
  {
201
+ dryRun = false,
201
202
  reapplyManagedAppFiles = false
202
203
  } = {}
203
204
  ) {
@@ -224,18 +225,19 @@ async function applyFileMutations(
224
225
  for (const preparedMutation of ensureArray(preparedMutations)) {
225
226
  const mutation = ensureObject(preparedMutation.mutation);
226
227
  const operation = String(preparedMutation.operation || "").trim() || "copy-file";
227
- if (operation === "install-migration") {
228
- await applyInstallMigrationMutation({
229
- packageEntry,
230
- preparedMutation,
231
- appRoot,
232
- managedMigrations,
233
- managedMigrationById,
234
- touchedFiles,
235
- warnings
236
- });
237
- continue;
238
- }
228
+ if (operation === "install-migration") {
229
+ await applyInstallMigrationMutation({
230
+ packageEntry,
231
+ preparedMutation,
232
+ appRoot,
233
+ managedMigrations,
234
+ managedMigrationById,
235
+ touchedFiles,
236
+ warnings,
237
+ dryRun
238
+ });
239
+ continue;
240
+ }
239
241
 
240
242
  const renderedSourceContent = String(preparedMutation.renderedSourceContent || "");
241
243
  const renderedSourceBuffer = Buffer.from(renderedSourceContent, "utf8");
@@ -280,8 +282,10 @@ async function applyFileMutations(
280
282
  continue;
281
283
  }
282
284
 
283
- await mkdir(path.dirname(targetPath), { recursive: true });
284
- await writeFile(targetPath, renderedSourceContent, "utf8");
285
+ if (!dryRun) {
286
+ await mkdir(path.dirname(targetPath), { recursive: true });
287
+ await writeFile(targetPath, renderedSourceContent, "utf8");
288
+ }
285
289
 
286
290
  managedFiles.push({
287
291
  path: relativeTargetPath,
@@ -26,7 +26,8 @@ async function applyInstallMigrationMutation({
26
26
  managedMigrations,
27
27
  managedMigrationById,
28
28
  touchedFiles,
29
- warnings
29
+ warnings,
30
+ dryRun = false
30
31
  } = {}) {
31
32
  const mutation = ensureObject(preparedMutation?.mutation);
32
33
  if (mutation.preserveOnRemove === true) {
@@ -81,8 +82,10 @@ async function applyInstallMigrationMutation({
81
82
  }
82
83
 
83
84
  if (!(await fileExists(absolutePath))) {
84
- await mkdir(path.dirname(absolutePath), { recursive: true });
85
- await writeFile(absolutePath, renderedSourceContent, "utf8");
85
+ if (!dryRun) {
86
+ await mkdir(path.dirname(absolutePath), { recursive: true });
87
+ await writeFile(absolutePath, renderedSourceContent, "utf8");
88
+ }
86
89
  touchedFiles.add(relativePath);
87
90
  }
88
91
 
@@ -165,8 +168,10 @@ async function applyInstallMigrationMutation({
165
168
 
166
169
  const relativePath = targetPath.relativePath;
167
170
  const absolutePath = targetPath.absolutePath;
168
- await mkdir(path.dirname(absolutePath), { recursive: true });
169
- await writeFile(absolutePath, renderedSourceContent, "utf8");
171
+ if (!dryRun) {
172
+ await mkdir(path.dirname(absolutePath), { recursive: true });
173
+ await writeFile(absolutePath, renderedSourceContent, "utf8");
174
+ }
170
175
  touchedFiles.add(relativePath);
171
176
 
172
177
  const nextManagedRecord = {
@@ -35,7 +35,15 @@ const PRE_FILE_CONFIG_MUTATION_TARGETS = new Set([
35
35
  "config/server.js"
36
36
  ]);
37
37
 
38
- async function applyTextMutations(packageEntry, appRoot, textMutations, options, managedText, touchedFiles) {
38
+ async function applyTextMutations(
39
+ packageEntry,
40
+ appRoot,
41
+ textMutations,
42
+ options,
43
+ managedText,
44
+ touchedFiles,
45
+ { dryRun = false } = {}
46
+ ) {
39
47
  for (const mutation of textMutations) {
40
48
  const when = normalizeMutationWhen(mutation?.when);
41
49
  const configContext = when?.config ? await loadMutationWhenConfigContext(appRoot) : {};
@@ -69,8 +77,10 @@ async function applyTextMutations(packageEntry, appRoot, textMutations, options,
69
77
  const resolvedValue = interpolateOptionValue(mutation?.value || "", options, packageEntry.packageId, resolvedKey);
70
78
  const upserted = upsertEnvValue(previousContent, resolvedKey, resolvedValue);
71
79
 
72
- await mkdir(path.dirname(absoluteFile), { recursive: true });
73
- await writeFile(absoluteFile, upserted.content, "utf8");
80
+ if (!dryRun) {
81
+ await mkdir(path.dirname(absoluteFile), { recursive: true });
82
+ await writeFile(absoluteFile, upserted.content, "utf8");
83
+ }
74
84
 
75
85
  const recordKey = `${relativeFile}::${String(mutation?.id || resolvedKey)}`;
76
86
  managedText[recordKey] = {
@@ -136,8 +146,10 @@ async function applyTextMutations(packageEntry, appRoot, textMutations, options,
136
146
  continue;
137
147
  }
138
148
 
139
- await mkdir(path.dirname(absoluteFile), { recursive: true });
140
- await writeFile(absoluteFile, appended.content, "utf8");
149
+ if (!dryRun) {
150
+ await mkdir(path.dirname(absoluteFile), { recursive: true });
151
+ await writeFile(absoluteFile, appended.content, "utf8");
152
+ }
141
153
 
142
154
  const recordKey = `${relativeFile}::${mutationId}`;
143
155
  managedText[recordKey] = {
@@ -146,7 +146,8 @@ async function applyPackagePositioning({
146
146
  packageOptions,
147
147
  appRoot,
148
148
  lock,
149
- touchedFiles
149
+ touchedFiles,
150
+ dryRun = false
150
151
  }) {
151
152
  const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
152
153
  if (Object.keys(existingInstall).length < 1) {
@@ -196,7 +197,10 @@ async function applyPackagePositioning({
196
197
  [],
197
198
  touchedFiles,
198
199
  [],
199
- nextManaged.files
200
+ nextManaged.files,
201
+ {
202
+ dryRun
203
+ }
200
204
  );
201
205
  }
202
206
  if (positioningMutations.text.length > 0) {
@@ -206,7 +210,10 @@ async function applyPackagePositioning({
206
210
  positioningMutations.text,
207
211
  packageOptions,
208
212
  appliedManagedText,
209
- touchedFiles
213
+ touchedFiles,
214
+ {
215
+ dryRun
216
+ }
210
217
  );
211
218
  }
212
219
 
@@ -250,7 +257,8 @@ async function applyPackageMigrationsOnly({
250
257
  packageOptions,
251
258
  appRoot,
252
259
  lock,
253
- touchedFiles
260
+ touchedFiles,
261
+ dryRun = false
254
262
  }) {
255
263
  const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
256
264
  if (Object.keys(existingInstall).length < 1) {
@@ -303,7 +311,10 @@ async function applyPackageMigrationsOnly({
303
311
  nextManaged.migrations,
304
312
  touchedFiles,
305
313
  mutationWarnings,
306
- nextManaged.files
314
+ nextManaged.files,
315
+ {
316
+ dryRun
317
+ }
307
318
  );
308
319
  }
309
320
 
@@ -333,7 +344,8 @@ async function applyPackageInstall({
333
344
  lock,
334
345
  packageRegistry,
335
346
  touchedFiles,
336
- reportTemplateFetchStatus = null
347
+ reportTemplateFetchStatus = null,
348
+ dryRun = false
337
349
  }) {
338
350
  const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
339
351
  const existingManaged = ensureObject(existingInstall.managed);
@@ -376,7 +388,10 @@ async function applyPackageInstall({
376
388
  preFileTextMutations,
377
389
  packageOptions,
378
390
  managedRecord.managed.text,
379
- touchedFiles
391
+ touchedFiles,
392
+ {
393
+ dryRun
394
+ }
380
395
  );
381
396
  }
382
397
 
@@ -391,7 +406,8 @@ async function applyPackageInstall({
391
406
  appRoot,
392
407
  packageId: packageEntry.packageId,
393
408
  managedViteChanges: ensureObject(existingManaged.vite),
394
- touchedFiles
409
+ touchedFiles,
410
+ dryRun
395
411
  });
396
412
 
397
413
  const mutationDependencies = ensureObject(mutations.dependencies);
@@ -499,15 +515,16 @@ async function applyPackageInstall({
499
515
  packageEntryForMutations,
500
516
  appRoot,
501
517
  preparedFileMutations,
502
- managedRecord.managed.files,
503
- managedRecord.managed.migrations,
504
- touchedFiles,
505
- mutationWarnings,
506
- ensureArray(existingManaged.files),
507
- {
508
- reapplyManagedAppFiles: Object.keys(existingInstall).length > 0
509
- }
510
- );
518
+ managedRecord.managed.files,
519
+ managedRecord.managed.migrations,
520
+ touchedFiles,
521
+ mutationWarnings,
522
+ ensureArray(existingManaged.files),
523
+ {
524
+ dryRun,
525
+ reapplyManagedAppFiles: Object.keys(existingInstall).length > 0
526
+ }
527
+ );
511
528
 
512
529
  await applyTextMutations(
513
530
  packageEntryForMutations,
@@ -515,7 +532,10 @@ async function applyPackageInstall({
515
532
  postFileTextMutations,
516
533
  packageOptions,
517
534
  managedRecord.managed.text,
518
- touchedFiles
535
+ touchedFiles,
536
+ {
537
+ dryRun
538
+ }
519
539
  );
520
540
 
521
541
  await applyViteMutations(
@@ -524,7 +544,10 @@ async function applyPackageInstall({
524
544
  ensureObject(mutations.vite),
525
545
  packageOptions,
526
546
  managedRecord.managed.vite,
527
- touchedFiles
547
+ touchedFiles,
548
+ {
549
+ dryRun
550
+ }
528
551
  );
529
552
 
530
553
  mutationWarnings.push(...await collectInstallWarnings({
@@ -144,14 +144,16 @@ async function loadViteDevProxyConfig(appRoot, { context = "vite proxy config" }
144
144
  });
145
145
  }
146
146
 
147
- async function writeViteDevProxyConfig(appRoot, config = {}, touchedFiles = null) {
147
+ async function writeViteDevProxyConfig(appRoot, config = {}, touchedFiles = null, { dryRun = false } = {}) {
148
148
  const absolutePath = resolveViteDevProxyConfigAbsolutePath(appRoot);
149
149
  const relativePath = normalizeRelativePath(appRoot, absolutePath);
150
150
  const normalizedConfig = normalizeViteDevProxyConfig(config);
151
151
 
152
152
  if (normalizedConfig.entries.length < 1) {
153
153
  if (await fileExists(absolutePath)) {
154
- await rm(absolutePath);
154
+ if (!dryRun) {
155
+ await rm(absolutePath);
156
+ }
155
157
  if (touchedFiles && typeof touchedFiles.add === "function") {
156
158
  touchedFiles.add(relativePath);
157
159
  }
@@ -159,7 +161,9 @@ async function writeViteDevProxyConfig(appRoot, config = {}, touchedFiles = null
159
161
  return;
160
162
  }
161
163
 
162
- await writeJsonFile(absolutePath, normalizedConfig);
164
+ if (!dryRun) {
165
+ await writeJsonFile(absolutePath, normalizedConfig);
166
+ }
163
167
  if (touchedFiles && typeof touchedFiles.add === "function") {
164
168
  touchedFiles.add(relativePath);
165
169
  }
@@ -181,7 +185,15 @@ function normalizeViteProxyMutationRecord(value = {}) {
181
185
  });
182
186
  }
183
187
 
184
- async function applyViteMutations(packageEntry, appRoot, viteMutations, options, managedVite, touchedFiles) {
188
+ async function applyViteMutations(
189
+ packageEntry,
190
+ appRoot,
191
+ viteMutations,
192
+ options,
193
+ managedVite,
194
+ touchedFiles,
195
+ { dryRun = false } = {}
196
+ ) {
185
197
  const mutations = ensureArray(ensureObject(viteMutations).proxy).map((entry) => normalizeViteProxyMutationRecord(entry));
186
198
  if (mutations.length < 1) {
187
199
  return;
@@ -291,10 +303,16 @@ async function applyViteMutations(packageEntry, appRoot, viteMutations, options,
291
303
  return;
292
304
  }
293
305
 
294
- await writeViteDevProxyConfig(appRoot, nextConfig, touchedFiles);
306
+ await writeViteDevProxyConfig(appRoot, nextConfig, touchedFiles, { dryRun });
295
307
  }
296
308
 
297
- async function removeManagedViteProxyEntries({ appRoot, packageId, managedViteChanges = {}, touchedFiles = null } = {}) {
309
+ async function removeManagedViteProxyEntries({
310
+ appRoot,
311
+ packageId,
312
+ managedViteChanges = {},
313
+ touchedFiles = null,
314
+ dryRun = false
315
+ } = {}) {
298
316
  const managedChanges = Object.values(ensureObject(managedViteChanges))
299
317
  .map((entry) => ensureObject(entry))
300
318
  .filter((entry) => String(entry.op || "").trim() === "upsert-vite-proxy");
@@ -339,7 +357,7 @@ async function removeManagedViteProxyEntries({ appRoot, packageId, managedViteCh
339
357
  return;
340
358
  }
341
359
 
342
- await writeViteDevProxyConfig(appRoot, nextConfig, touchedFiles);
360
+ await writeViteDevProxyConfig(appRoot, nextConfig, touchedFiles, { dryRun });
343
361
  }
344
362
 
345
363
  export {
@@ -603,7 +603,9 @@ function createHealthCommands(ctx = {}) {
603
603
  packagePath,
604
604
  tableName,
605
605
  provenance: String(entry.provenance || "").trim().toLowerCase(),
606
- ownerKind: String(entry.ownerKind || "").trim().toLowerCase()
606
+ ownerKind: String(entry.ownerKind || "").trim().toLowerCase(),
607
+ providerEntrypoint: String(entry.providerEntrypoint || "").trim(),
608
+ ownershipFilter: normalizeDbIdentifier(entry.ownershipFilter)
607
609
  });
608
610
  }
609
611
 
@@ -969,7 +971,7 @@ function createHealthCommands(ctx = {}) {
969
971
  .join(", ");
970
972
  }
971
973
 
972
- async function resolveAppLocalCrudOwnershipFilters({ appRoot, appLocalRegistry }) {
974
+ async function resolveAppLocalCrudOwnershipFilters({ appRoot, appLocalRegistry, issues }) {
973
975
  const ownershipByTable = new Map();
974
976
  const packageEntries = sortStrings([...appLocalRegistry.keys()])
975
977
  .map((packageId) => appLocalRegistry.get(packageId))
@@ -980,26 +982,73 @@ function createHealthCommands(ctx = {}) {
980
982
  continue;
981
983
  }
982
984
 
983
- const providerPath = resolvePrimaryServerProviderPath(packageEntry);
984
- if (!providerPath || !(await fileExists(providerPath))) {
985
- continue;
986
- }
985
+ const serverProviderEntries = resolveServerProviderEntries(packageEntry);
986
+ const providerInfoByEntrypoint = new Map();
987
+ for (const providerEntry of serverProviderEntries) {
988
+ if (!(await fileExists(providerEntry.absolutePath))) {
989
+ continue;
990
+ }
987
991
 
988
- const sourceText = await readFile(providerPath, "utf8");
989
- const match = CRUD_OWNERSHIP_FILTER_LITERAL_PATTERN.exec(sourceText);
990
- if (!match) {
991
- continue;
992
+ const sourceText = await readFile(providerEntry.absolutePath, "utf8");
993
+ const match = CRUD_OWNERSHIP_FILTER_LITERAL_PATTERN.exec(sourceText);
994
+ if (!match) {
995
+ continue;
996
+ }
997
+
998
+ const ownershipFilter = normalizeDbIdentifier(match[1]);
999
+ providerInfoByEntrypoint.set(providerEntry.entrypoint, {
1000
+ providerPath: normalizeRelativePath(appRoot, providerEntry.absolutePath),
1001
+ ownershipFilter,
1002
+ requiredOwnerKinds: resolveRequiredOwnerKindsFromOwnershipFilter(ownershipFilter)
1003
+ });
992
1004
  }
993
- const ownershipFilter = normalizeDbIdentifier(match[1]);
994
- const requiredOwnerKinds = resolveRequiredOwnerKindsFromOwnershipFilter(ownershipFilter);
995
1005
 
996
1006
  for (const ownershipEntry of normalizeOwnedTableEntries(packageEntry)) {
1007
+ const descriptorPath = `${resolvePackageDisplayPath(packageEntry)}/package.descriptor.mjs`;
1008
+ let providerInfo = null;
1009
+ if (ownershipEntry.providerEntrypoint) {
1010
+ providerInfo = providerInfoByEntrypoint.get(ownershipEntry.providerEntrypoint) || null;
1011
+ if (!providerInfo) {
1012
+ issues.push(
1013
+ `${descriptorPath}: [crud-ownership:provider-unresolved] table "${ownershipEntry.tableName}" points at providerEntrypoint "${ownershipEntry.providerEntrypoint}" but doctor could not resolve an ownershipFilter from that provider. Make sure the file exists and declares a literal CRUD_MODULE_CONFIG.ownershipFilter.`
1014
+ );
1015
+ }
1016
+ } else if (serverProviderEntries.length === 1) {
1017
+ providerInfo = providerInfoByEntrypoint.get(serverProviderEntries[0].entrypoint) || null;
1018
+ if (!providerInfo) {
1019
+ issues.push(
1020
+ `${descriptorPath}: [crud-ownership:provider-unresolved] table "${ownershipEntry.tableName}" relies on the package's only server provider, but doctor could not resolve a literal CRUD_MODULE_CONFIG.ownershipFilter from "${serverProviderEntries[0].entrypoint}".`
1021
+ );
1022
+ }
1023
+ } else if (serverProviderEntries.length > 1) {
1024
+ issues.push(
1025
+ `${descriptorPath}: [crud-ownership:missing-provider-entrypoint] table "${ownershipEntry.tableName}" is claimed by a multi-provider CRUD package but does not declare metadata.jskit.tableOwnership.tables[].providerEntrypoint. Point it at the owning provider so doctor can verify the real ownershipFilter.`
1026
+ );
1027
+ }
1028
+
1029
+ if (
1030
+ ownershipEntry.ownershipFilter &&
1031
+ providerInfo?.ownershipFilter &&
1032
+ ownershipEntry.ownershipFilter !== providerInfo.ownershipFilter
1033
+ ) {
1034
+ issues.push(
1035
+ `${providerInfo.providerPath}: [crud-ownership:ownership-filter-mismatch] metadata declares ownershipFilter "${ownershipEntry.ownershipFilter}" for live table "${ownershipEntry.tableName}" but provider code uses "${providerInfo.ownershipFilter}". Update the provider or metadata so doctor verifies the real contract.`
1036
+ );
1037
+ }
1038
+
1039
+ const ownershipFilter = providerInfo?.ownershipFilter || "";
1040
+ if (!ownershipFilter) {
1041
+ continue;
1042
+ }
1043
+
997
1044
  ownershipByTable.set(ownershipEntry.tableName, {
998
1045
  tableName: ownershipEntry.tableName,
999
1046
  ownershipFilter,
1000
- requiredOwnerKinds,
1047
+ requiredOwnerKinds: providerInfo?.requiredOwnerKinds || new Set(),
1001
1048
  packagePath: resolvePackageDisplayPath(packageEntry),
1002
- providerPath: normalizeRelativePath(appRoot, providerPath)
1049
+ providerPath:
1050
+ providerInfo?.providerPath ||
1051
+ normalizeRelativePath(appRoot, resolvePrimaryServerProviderPath(packageEntry))
1003
1052
  });
1004
1053
  }
1005
1054
  }
@@ -1149,6 +1198,29 @@ function createHealthCommands(ctx = {}) {
1149
1198
  return "";
1150
1199
  }
1151
1200
 
1201
+ function resolveServerProviderEntries(packageEntry) {
1202
+ const rootDir = String(packageEntry?.rootDir || "").trim();
1203
+ if (!rootDir) {
1204
+ return [];
1205
+ }
1206
+
1207
+ const providers = ensureArray(ensureObject(ensureObject(packageEntry?.descriptor).runtime).server?.providers);
1208
+ const resolved = [];
1209
+ for (const rawProvider of providers) {
1210
+ const provider = ensureObject(rawProvider);
1211
+ const entrypoint = String(provider.entrypoint || "").trim();
1212
+ if (!entrypoint || entrypoint.includes("*")) {
1213
+ continue;
1214
+ }
1215
+ resolved.push({
1216
+ entrypoint,
1217
+ absolutePath: path.resolve(rootDir, entrypoint)
1218
+ });
1219
+ }
1220
+
1221
+ return resolved;
1222
+ }
1223
+
1152
1224
  function resolvePackageDisplayPath(packageEntry) {
1153
1225
  const relativeDir = String(packageEntry?.relativeDir || "").trim();
1154
1226
  if (relativeDir) {
@@ -1517,7 +1589,8 @@ function createHealthCommands(ctx = {}) {
1517
1589
  });
1518
1590
  const crudOwnershipByTable = await resolveAppLocalCrudOwnershipFilters({
1519
1591
  appRoot,
1520
- appLocalRegistry
1592
+ appLocalRegistry,
1593
+ issues
1521
1594
  });
1522
1595
  const exceptionEntriesByName = new Map();
1523
1596
  for (const entry of exceptionConfig.exceptions) {