@prisma-next/sql-contract-ts 0.16.0-dev.5 → 0.16.0-dev.7

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.
@@ -23,6 +23,7 @@ import type {
23
23
  import { blindCast } from '@prisma-next/utils/casts';
24
24
  import { ifDefined } from '@prisma-next/utils/defined';
25
25
  import type { NamedConstraintSpec } from './authoring-type-utils';
26
+ import { contractError } from './contract-errors';
26
27
  import type { EnumTypeHandle } from './enum-type';
27
28
  import { isEnumTypeHandle } from './enum-type';
28
29
 
@@ -402,10 +403,14 @@ export class ScalarFieldBuilder<State extends AnyScalarFieldState = AnyScalarFie
402
403
  const uniqueSpec = 'unique' in spec ? spec.unique : undefined;
403
404
 
404
405
  if (idSpec && !this.state.id) {
405
- throw new Error('field.sql({ id }) requires an existing inline .id(...) declaration.');
406
+ throw contractError(
407
+ 'CONTRACT.ARGUMENT_INVALID',
408
+ 'field.sql({ id }) requires an existing inline .id(...) declaration.',
409
+ );
406
410
  }
407
411
  if (uniqueSpec && !this.state.unique) {
408
- throw new Error(
412
+ throw contractError(
413
+ 'CONTRACT.ARGUMENT_INVALID',
409
414
  'field.sql({ unique }) requires an existing inline .unique(...) declaration.',
410
415
  );
411
416
  }
@@ -457,8 +462,10 @@ export class EnumScalarFieldBuilder<
457
462
  }
458
463
 
459
464
  override defaultSql(_expression: never): never {
460
- throw new Error(
465
+ throw contractError(
466
+ 'CONTRACT.DEFAULT_INVALID',
461
467
  'defaultSql is not available on an enum field; use .default(members.X) instead',
468
+ { meta: { reason: 'defaultSql-on-enum-field' } },
462
469
  );
463
470
  }
464
471
  }
@@ -674,7 +681,11 @@ export class RelationBuilder<State extends RelationState = AnyRelationState> {
674
681
  spec: SqlSpec,
675
682
  ): RelationBuilder<ApplyBelongsToRelationSqlSpec<State, SqlSpec>> {
676
683
  if (this.state.kind !== 'belongsTo') {
677
- throw new Error('relation.sql(...) is only supported for belongsTo relations.');
684
+ throw contractError(
685
+ 'CONTRACT.RELATION_INVALID',
686
+ 'relation.sql(...) is only supported for belongsTo relations.',
687
+ { meta: { relationKind: this.state.kind } },
688
+ );
678
689
  }
679
690
 
680
691
  return new RelationBuilder({
@@ -860,27 +871,45 @@ function normalizeTargetFieldRefInput(input: TargetFieldRef | readonly TargetFie
860
871
  const refs = Array.isArray(input) ? input : [input];
861
872
  const [first] = refs;
862
873
  if (!first) {
863
- throw new Error('Expected at least one target ref');
874
+ throw contractError('CONTRACT.FOREIGN_KEY_INVALID', 'Expected at least one target ref', {
875
+ meta: { reason: 'empty-target-refs' },
876
+ });
864
877
  }
865
878
  if (refs.some((ref) => ref.modelName !== first.modelName)) {
866
- throw new Error('All target refs in a foreign key must point to the same model');
879
+ throw contractError(
880
+ 'CONTRACT.FOREIGN_KEY_INVALID',
881
+ 'All target refs in a foreign key must point to the same model',
882
+ { meta: { mismatch: 'modelName', models: refs.map((ref) => ref.modelName) } },
883
+ );
867
884
  }
868
885
  // F-compound: all refs in a compound FK must share the same cross-space coordinate.
869
886
  // A mismatch in spaceId, namespaceId, or tableName means the refs come from
870
887
  // different spaces despite having the same modelName — an impossible FK.
871
888
  if (refs.some((ref) => ref.spaceId !== first.spaceId)) {
872
- throw new Error(
889
+ throw contractError(
890
+ 'CONTRACT.FOREIGN_KEY_INVALID',
873
891
  `All target refs in a compound foreign key must share the same spaceId (found mismatch: "${first.spaceId ?? '<local>'}" vs "${refs.find((r) => r.spaceId !== first.spaceId)?.spaceId ?? '<local>'}")`,
892
+ {
893
+ meta: {
894
+ mismatch: 'spaceId',
895
+ first: first.spaceId,
896
+ second: refs.find((r) => r.spaceId !== first.spaceId)?.spaceId,
897
+ },
898
+ },
874
899
  );
875
900
  }
876
901
  if (refs.some((ref) => ref.namespaceId !== first.namespaceId)) {
877
- throw new Error(
902
+ throw contractError(
903
+ 'CONTRACT.FOREIGN_KEY_INVALID',
878
904
  'All target refs in a compound foreign key must share the same namespaceId (found mismatch)',
905
+ { meta: { mismatch: 'namespaceId' } },
879
906
  );
880
907
  }
881
908
  if (refs.some((ref) => ref.tableName !== first.tableName)) {
882
- throw new Error(
909
+ throw contractError(
910
+ 'CONTRACT.FOREIGN_KEY_INVALID',
883
911
  'All target refs in a compound foreign key must share the same tableName (found mismatch)',
912
+ { meta: { mismatch: 'tableName' } },
884
913
  );
885
914
  }
886
915
  return {
@@ -1322,7 +1351,10 @@ export class ContractModelBuilder<
1322
1351
  ): TargetFieldRef<ModelName & string, FieldName> {
1323
1352
  const modelName = this.stageOne.modelName;
1324
1353
  if (!modelName) {
1325
- throw new Error('Model tokens require model("ModelName", ...) before calling .ref(...)');
1354
+ throw contractError(
1355
+ 'CONTRACT.MODEL_TOKEN_INVALID',
1356
+ 'Model tokens require model("ModelName", ...) before calling .ref(...)',
1357
+ );
1326
1358
  }
1327
1359
 
1328
1360
  return {
@@ -1346,8 +1378,16 @@ export class ContractModelBuilder<
1346
1378
  > {
1347
1379
  const duplicateRelationName = findDuplicateRelationName(this.stageOne.relations, relations);
1348
1380
  if (duplicateRelationName) {
1349
- throw new Error(
1381
+ throw contractError(
1382
+ 'CONTRACT.NAME_DUPLICATE',
1350
1383
  `Model "${this.stageOne.modelName ?? '<anonymous>'}" already defines relation "${duplicateRelationName}".`,
1384
+ {
1385
+ meta: {
1386
+ kind: 'relation',
1387
+ name: duplicateRelationName,
1388
+ modelName: this.stageOne.modelName,
1389
+ },
1390
+ },
1351
1391
  );
1352
1392
  }
1353
1393
 
@@ -1501,7 +1541,8 @@ function resolveNamedModelTokenName(token: {
1501
1541
  }): string {
1502
1542
  const modelName = token.stageOne.modelName;
1503
1543
  if (!modelName) {
1504
- throw new Error(
1544
+ throw contractError(
1545
+ 'CONTRACT.MODEL_TOKEN_INVALID',
1505
1546
  'Relation targets require named model tokens. Use model("ModelName", ...) before passing a token to rel.*(...).',
1506
1547
  );
1507
1548
  }
@@ -1668,7 +1709,10 @@ export function model<
1668
1709
  const input = typeof modelNameOrInput === 'string' ? maybeInput : modelNameOrInput;
1669
1710
 
1670
1711
  if (!input) {
1671
- throw new Error('model("ModelName", ...) requires a model definition.');
1712
+ throw contractError(
1713
+ 'CONTRACT.ARGUMENT_INVALID',
1714
+ 'model("ModelName", ...) requires a model definition.',
1715
+ );
1672
1716
  }
1673
1717
 
1674
1718
  return new ContractModelBuilder({
@@ -0,0 +1,39 @@
1
+ import type { StructuredError, StructuredErrorOptions } from '@prisma-next/utils/structured-error';
2
+ import { structuredError } from '@prisma-next/utils/structured-error';
3
+
4
+ export type ContractCode = `CONTRACT.${ContractSubcode}`;
5
+
6
+ type ContractSubcode =
7
+ | 'VALIDATION_FAILED'
8
+ | 'NAME_DUPLICATE'
9
+ | 'MODEL_UNKNOWN'
10
+ | 'PACK_CONTRIBUTION_INVALID'
11
+ | 'PACK_FAMILY_MISMATCH'
12
+ | 'PACK_TARGET_MISMATCH'
13
+ | 'PACK_REF_INVALID'
14
+ | 'PACK_MISSING'
15
+ | 'NAMESPACE_INVALID'
16
+ | 'NAMESPACE_UNSUPPORTED'
17
+ | 'NAMESPACE_UNKNOWN'
18
+ | 'ARGUMENT_INVALID'
19
+ | 'FOREIGN_KEY_INVALID'
20
+ | 'RELATION_INVALID'
21
+ | 'IDENTITY_INVALID'
22
+ | 'CONSTRAINT_INVALID'
23
+ | 'DEFAULT_INVALID'
24
+ | 'ENUM_INVALID'
25
+ | 'TYPE_UNKNOWN'
26
+ | 'FIELD_UNKNOWN'
27
+ | 'MODEL_TOKEN_INVALID'
28
+ | 'MODULE_EXPORT_MISSING'
29
+ | 'ENTITY_KIND_UNKNOWN'
30
+ | 'ENTITY_KIND_INVALID'
31
+ | 'TABLE_MISMATCH';
32
+
33
+ export function contractError(
34
+ code: ContractCode,
35
+ message: string,
36
+ options?: StructuredErrorOptions,
37
+ ): StructuredError {
38
+ return structuredError(code, message, options);
39
+ }
@@ -11,6 +11,7 @@ import {
11
11
  } from '@prisma-next/sql-contract/entity-handle-lowering-hook';
12
12
  import type { StorageTypeInstance } from '@prisma-next/sql-contract/types';
13
13
  import { ifDefined } from '@prisma-next/utils/defined';
14
+ import { InternalError } from '@prisma-next/utils/internal-error';
14
15
  import type {
15
16
  AttachedEntities,
16
17
  ContractDefinition,
@@ -39,6 +40,7 @@ import {
39
40
  type SqlStageSpec,
40
41
  type UniqueConstraint,
41
42
  } from './contract-dsl';
43
+ import { contractError } from './contract-errors';
42
44
  import {
43
45
  emitTypedCrossModelFallbackWarnings,
44
46
  emitTypedNamedTypeFallbackWarnings,
@@ -106,15 +108,19 @@ function resolveFieldDescriptor(
106
108
  : storageTypeReverseLookup.get(fieldState.typeRef as StorageTypeInstance);
107
109
 
108
110
  if (!typeRef) {
109
- throw new Error(
111
+ throw contractError(
112
+ 'CONTRACT.TYPE_UNKNOWN',
110
113
  `Field "${modelName}.${fieldName}" references a storage type instance that is not present in definition.types`,
114
+ { meta: { modelName, fieldName, reason: 'instance-not-in-definition-types' } },
111
115
  );
112
116
  }
113
117
 
114
118
  const referencedType = storageTypes[typeRef];
115
119
  if (!referencedType) {
116
- throw new Error(
120
+ throw contractError(
121
+ 'CONTRACT.TYPE_UNKNOWN',
117
122
  `Field "${modelName}.${fieldName}" references unknown storage type "${typeRef}"`,
123
+ { meta: { modelName, fieldName, typeRef } },
118
124
  );
119
125
  }
120
126
 
@@ -125,7 +131,11 @@ function resolveFieldDescriptor(
125
131
  };
126
132
  }
127
133
 
128
- throw new Error(`Field "${modelName}.${fieldName}" does not resolve to a storage descriptor`);
134
+ throw contractError(
135
+ 'CONTRACT.TYPE_UNKNOWN',
136
+ `Field "${modelName}.${fieldName}" does not resolve to a storage descriptor`,
137
+ { meta: { modelName, fieldName, reason: 'unresolved-storage-descriptor' } },
138
+ );
129
139
  }
130
140
 
131
141
  function mapFieldNamesToColumnNames(
@@ -136,7 +146,11 @@ function mapFieldNamesToColumnNames(
136
146
  return fieldNames.map((fieldName) => {
137
147
  const columnName = fieldToColumn[fieldName];
138
148
  if (!columnName) {
139
- throw new Error(`Unknown field "${modelName}.${fieldName}" in contract definition`);
149
+ throw contractError(
150
+ 'CONTRACT.FIELD_UNKNOWN',
151
+ `Unknown field "${modelName}.${fieldName}" in contract definition`,
152
+ { meta: { modelName, fieldName } },
153
+ );
140
154
  }
141
155
  return columnName;
142
156
  });
@@ -154,8 +168,16 @@ function assertRelationFieldArity(params: {
154
168
  return;
155
169
  }
156
170
 
157
- throw new Error(
171
+ throw contractError(
172
+ 'CONTRACT.RELATION_INVALID',
158
173
  `Relation "${params.modelName}.${params.relationName}" maps ${params.leftFields.length} ${params.leftLabel} field(s) to ${params.rightFields.length} ${params.rightLabel} field(s).`,
174
+ {
175
+ meta: {
176
+ modelName: params.modelName,
177
+ relationName: params.relationName,
178
+ reason: 'field-count-mismatch',
179
+ },
180
+ },
159
181
  );
160
182
  }
161
183
 
@@ -182,8 +204,12 @@ function resolveInlineIdConstraint(
182
204
  }
183
205
 
184
206
  if (inlineIdFields.length > 1) {
185
- throw new Error(
207
+ throw contractError(
208
+ 'CONTRACT.IDENTITY_INVALID',
186
209
  `Model "${spec.modelName}" marks multiple fields with .id(). Use .attributes(...) for compound identities.`,
210
+ {
211
+ meta: { modelName: spec.modelName, reason: 'multiple-inline-ids', fields: inlineIdFields },
212
+ },
187
213
  );
188
214
  }
189
215
 
@@ -225,14 +251,20 @@ function resolveModelIdConstraint(
225
251
  const attributeId = spec.attributesSpec?.id;
226
252
 
227
253
  if (inlineId && attributeId) {
228
- throw new Error(
254
+ throw contractError(
255
+ 'CONTRACT.IDENTITY_INVALID',
229
256
  `Model "${spec.modelName}" defines identity both inline and in .attributes(...). Pick one identity style.`,
257
+ { meta: { modelName: spec.modelName, reason: 'inline-and-attributes' } },
230
258
  );
231
259
  }
232
260
 
233
261
  const resolvedId = attributeId ?? inlineId;
234
262
  if (resolvedId && resolvedId.fields.length === 0) {
235
- throw new Error(`Model "${spec.modelName}" defines an empty identity. Add at least one field.`);
263
+ throw contractError(
264
+ 'CONTRACT.IDENTITY_INVALID',
265
+ `Model "${spec.modelName}" defines an empty identity. Add at least one field.`,
266
+ { meta: { modelName: spec.modelName, reason: 'empty-identity' } },
267
+ );
236
268
  }
237
269
 
238
270
  return resolvedId;
@@ -242,8 +274,10 @@ function resolveModelUniqueConstraints(spec: RuntimeModelSpec): readonly UniqueC
242
274
  const attributeUniques = spec.attributesSpec?.uniques ?? [];
243
275
  for (const unique of attributeUniques) {
244
276
  if (unique.fields.length === 0) {
245
- throw new Error(
277
+ throw contractError(
278
+ 'CONTRACT.CONSTRAINT_INVALID',
246
279
  `Model "${spec.modelName}" defines an empty unique constraint. Add at least one field.`,
280
+ { meta: { modelName: spec.modelName } },
247
281
  );
248
282
  }
249
283
  }
@@ -300,8 +334,10 @@ function resolveRelationForeignKeys(
300
334
  }
301
335
 
302
336
  if (!allSpecs.has(targetModelName)) {
303
- throw new Error(
337
+ throw contractError(
338
+ 'CONTRACT.MODEL_UNKNOWN',
304
339
  `Relation "${spec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
340
+ { meta: { sourceModel: spec.modelName, relationName, targetModel: targetModelName } },
305
341
  );
306
342
  }
307
343
 
@@ -344,8 +380,10 @@ function resolveRelationAnchorFields(spec: RuntimeModelSpec): readonly string[]
344
380
  return ['id'];
345
381
  }
346
382
 
347
- throw new Error(
383
+ throw contractError(
384
+ 'CONTRACT.IDENTITY_INVALID',
348
385
  `Model "${spec.modelName}" needs an explicit id or an "id" field to anchor non-owning relations`,
386
+ { meta: { modelName: spec.modelName, reason: 'missing-anchor-id' } },
349
387
  );
350
388
  }
351
389
 
@@ -406,8 +444,10 @@ function lowerBelongsToRelation(
406
444
 
407
445
  const targetSpec = allSpecs.get(targetModelName);
408
446
  if (!targetSpec) {
409
- throw new Error(
447
+ throw contractError(
448
+ 'CONTRACT.MODEL_UNKNOWN',
410
449
  `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
450
+ { meta: { sourceModel: currentSpec.modelName, relationName, targetModel: targetModelName } },
411
451
  );
412
452
  }
413
453
 
@@ -442,8 +482,10 @@ function lowerHasOwnershipRelation(
442
482
  const targetModelName = resolveRelationModelName(relation.toModel);
443
483
  const targetSpec = allSpecs.get(targetModelName);
444
484
  if (!targetSpec) {
445
- throw new Error(
485
+ throw contractError(
486
+ 'CONTRACT.MODEL_UNKNOWN',
446
487
  `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
488
+ { meta: { sourceModel: currentSpec.modelName, relationName, targetModel: targetModelName } },
447
489
  );
448
490
  }
449
491
 
@@ -489,16 +531,20 @@ function lowerManyToManyRelation(
489
531
  const targetModelName = resolveRelationModelName(relation.toModel);
490
532
  const targetSpec = allSpecs.get(targetModelName);
491
533
  if (!targetSpec) {
492
- throw new Error(
534
+ throw contractError(
535
+ 'CONTRACT.MODEL_UNKNOWN',
493
536
  `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
537
+ { meta: { sourceModel: currentSpec.modelName, relationName, targetModel: targetModelName } },
494
538
  );
495
539
  }
496
540
 
497
541
  const throughModelName = resolveRelationModelName(relation.through);
498
542
  const throughSpec = allSpecs.get(throughModelName);
499
543
  if (!throughSpec) {
500
- throw new Error(
544
+ throw contractError(
545
+ 'CONTRACT.MODEL_UNKNOWN',
501
546
  `Relation "${currentSpec.modelName}.${relationName}" references unknown through model "${throughModelName}"`,
547
+ { meta: { sourceModel: currentSpec.modelName, relationName, targetModel: throughModelName } },
502
548
  );
503
549
  }
504
550
 
@@ -510,8 +556,16 @@ function lowerManyToManyRelation(
510
556
  currentAnchorFields.length !== throughFromFields.length ||
511
557
  targetAnchorFields.length !== throughToFields.length
512
558
  ) {
513
- throw new Error(
559
+ throw contractError(
560
+ 'CONTRACT.RELATION_INVALID',
514
561
  `Relation "${currentSpec.modelName}.${relationName}" has mismatched many-to-many field counts.`,
562
+ {
563
+ meta: {
564
+ modelName: currentSpec.modelName,
565
+ relationName,
566
+ reason: 'many-to-many-field-count-mismatch',
567
+ },
568
+ },
515
569
  );
516
570
  }
517
571
 
@@ -644,8 +698,10 @@ function assertKnownExtensionPack(
644
698
  if (extensionPacks !== undefined && Object.hasOwn(extensionPacks, spaceId)) {
645
699
  return;
646
700
  }
647
- throw new Error(
701
+ throw contractError(
702
+ 'CONTRACT.PACK_MISSING',
648
703
  `${context} references contract space "${spaceId}" but "${spaceId}" is not declared in extensionPacks. Add the pack to extensionPacks.`,
704
+ { meta: { spaceId, context } },
649
705
  );
650
706
  }
651
707
 
@@ -671,8 +727,10 @@ function resolveForeignKeyNodes(
671
727
 
672
728
  const targetSpec = allSpecs.get(foreignKey.targetModel);
673
729
  if (!targetSpec) {
674
- throw new Error(
730
+ throw contractError(
731
+ 'CONTRACT.MODEL_UNKNOWN',
675
732
  `Foreign key on "${spec.modelName}" references unknown model "${foreignKey.targetModel}"`,
733
+ { meta: { sourceModel: spec.modelName, targetModel: foreignKey.targetModel } },
676
734
  );
677
735
  }
678
736
 
@@ -694,8 +752,10 @@ function resolveForeignKeyNodes(
694
752
 
695
753
  const targetSpec = allSpecs.get(foreignKey.targetModel);
696
754
  if (!targetSpec) {
697
- throw new Error(
755
+ throw contractError(
756
+ 'CONTRACT.MODEL_UNKNOWN',
698
757
  `Foreign key on "${spec.modelName}" references unknown model "${foreignKey.targetModel}"`,
758
+ { meta: { sourceModel: spec.modelName, targetModel: foreignKey.targetModel } },
699
759
  );
700
760
  }
701
761
 
@@ -725,7 +785,7 @@ function resolveModelNode(
725
785
  );
726
786
  const columnName = spec.fieldToColumn[fieldName];
727
787
  if (!columnName) {
728
- throw new Error(`Column name resolution failed for "${spec.modelName}.${fieldName}"`);
788
+ throw new InternalError(`Column name resolution failed for "${spec.modelName}.${fieldName}"`);
729
789
  }
730
790
 
731
791
  const enumHandle =
@@ -798,8 +858,10 @@ function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection
798
858
  for (const [modelName, modelDefinition] of Object.entries(models)) {
799
859
  const tokenModelName = modelDefinition.stageOne.modelName;
800
860
  if (tokenModelName && tokenModelName !== modelName) {
801
- throw new Error(
861
+ throw contractError(
862
+ 'CONTRACT.MODEL_TOKEN_INVALID',
802
863
  `Model token "${tokenModelName}" must be assigned to models.${tokenModelName}. Received models.${modelName}.`,
864
+ { meta: { tokenModelName, assignedKey: modelName } },
803
865
  );
804
866
  }
805
867
 
@@ -812,8 +874,10 @@ function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection
812
874
  const tableKey = JSON.stringify([namespaceId, tableName]);
813
875
  const existingModel = tableOwners.get(tableKey);
814
876
  if (existingModel) {
815
- throw new Error(
877
+ throw contractError(
878
+ 'CONTRACT.NAME_DUPLICATE',
816
879
  `Models "${existingModel}" and "${modelName}" both map to table "${tableName}".`,
880
+ { meta: { kind: 'table', name: tableName, first: existingModel, second: modelName } },
817
881
  );
818
882
  }
819
883
  tableOwners.set(tableKey, modelName);
@@ -827,8 +891,10 @@ function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection
827
891
  fieldState.columnName ?? applyNaming(fieldName, definition.naming?.columns);
828
892
  const existingField = columnOwners.get(columnName);
829
893
  if (existingField) {
830
- throw new Error(
894
+ throw contractError(
895
+ 'CONTRACT.NAME_DUPLICATE',
831
896
  `Model "${modelName}" maps both "${existingField}" and "${fieldName}" to column "${columnName}".`,
897
+ { meta: { kind: 'column', name: columnName, first: existingField, second: fieldName } },
832
898
  );
833
899
  }
834
900
  columnOwners.set(columnName, fieldName);
@@ -976,8 +1042,10 @@ function lowerPackEntityHandles(
976
1042
  for (const handle of entities) {
977
1043
  const component = owningComponent.get(handle.entityKind);
978
1044
  if (component === undefined) {
979
- throw new Error(
1045
+ throw contractError(
1046
+ 'CONTRACT.ENTITY_KIND_UNKNOWN',
980
1047
  `defineContract: entities contains a handle with entityKind "${handle.entityKind}", which no composed pack registers. Compose a pack whose entityTypes contribution claims "${handle.entityKind}", or remove the handle.`,
1048
+ { meta: { entityKind: handle.entityKind } },
981
1049
  );
982
1050
  }
983
1051
  const refs: Record<string, ResolvedEntityHandleRef> = {};
@@ -994,8 +1062,10 @@ function lowerPackEntityHandles(
994
1062
  const authoring = component.authoring;
995
1063
  if (!providesEntityHandleLowering(authoring)) {
996
1064
  const kinds = [...new Set(handles.map((entry) => entry.handle.entityKind))].sort();
997
- throw new Error(
1065
+ throw contractError(
1066
+ 'CONTRACT.PACK_CONTRIBUTION_INVALID',
998
1067
  `defineContract: entityKind(s) ${kinds.map((kind) => `"${kind}"`).join(', ')} are registered by a pack that does not implement entity-handle lowering (no lowerEntityHandles on its authoring contributions).`,
1068
+ { meta: { entityKinds: kinds, reason: 'missing-lowerEntityHandles' } },
999
1069
  );
1000
1070
  }
1001
1071
  for (const row of authoring.lowerEntityHandles({ handles, defaultNamespaceId })) {
@@ -1005,8 +1075,10 @@ function lowerPackEntityHandles(
1005
1075
  forNamespace[row.entityKind] = forKind;
1006
1076
  const existing = forKind[row.key];
1007
1077
  if (existing !== undefined && existing !== row.entity) {
1008
- throw new Error(
1078
+ throw contractError(
1079
+ 'CONTRACT.NAME_DUPLICATE',
1009
1080
  `defineContract: two different "${row.entityKind}" entities named "${row.key}" in namespace "${row.namespaceId}" — pack-entity names must be unique per namespace.`,
1081
+ { meta: { kind: row.entityKind, name: row.key, namespaceId: row.namespaceId } },
1010
1082
  );
1011
1083
  }
1012
1084
  forKind[row.key] = row.entity;