@prisma-next/contract 0.12.0 → 0.13.0-dev.10

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.
Files changed (78) hide show
  1. package/dist/apply-specifier-default-control-policy.d.mts +7 -0
  2. package/dist/apply-specifier-default-control-policy.d.mts.map +1 -0
  3. package/dist/apply-specifier-default-control-policy.mjs +12 -0
  4. package/dist/apply-specifier-default-control-policy.mjs.map +1 -0
  5. package/dist/{canonicalization-DFE0HJkI.d.mts → canonicalization-BqYzAeWE.d.mts} +2 -2
  6. package/dist/canonicalization-BqYzAeWE.d.mts.map +1 -0
  7. package/dist/{canonicalization-path-match-b2jFuEso.mjs → canonicalization-path-match-CNgHuwM_.mjs} +1 -1
  8. package/dist/{canonicalization-path-match-b2jFuEso.mjs.map → canonicalization-path-match-CNgHuwM_.mjs.map} +1 -1
  9. package/dist/contract-types-CtIh62PH.d.mts +82 -0
  10. package/dist/contract-types-CtIh62PH.d.mts.map +1 -0
  11. package/dist/{contract-validation-error-ClZaKqMW.mjs → contract-validation-error-BFA66rwU.mjs} +1 -1
  12. package/dist/{contract-validation-error-ClZaKqMW.mjs.map → contract-validation-error-BFA66rwU.mjs.map} +1 -1
  13. package/dist/{contract-validation-error-T5LH4DW-.d.mts → contract-validation-error-D7g0kmcc.d.mts} +1 -1
  14. package/dist/{contract-validation-error-T5LH4DW-.d.mts.map → contract-validation-error-D7g0kmcc.d.mts.map} +1 -1
  15. package/dist/contract-validation-error.d.mts +1 -1
  16. package/dist/contract-validation-error.mjs +1 -1
  17. package/dist/default-namespace-D5X_k6hJ.d.mts +25 -0
  18. package/dist/default-namespace-D5X_k6hJ.d.mts.map +1 -0
  19. package/dist/default-namespace-rpdJeUMq.mjs +33 -0
  20. package/dist/default-namespace-rpdJeUMq.mjs.map +1 -0
  21. package/dist/default-namespace.d.mts +2 -0
  22. package/dist/default-namespace.mjs +2 -0
  23. package/dist/domain-envelope-DKOnhO5d.d.mts +352 -0
  24. package/dist/domain-envelope-DKOnhO5d.d.mts.map +1 -0
  25. package/dist/enum-accessor.d.mts +77 -0
  26. package/dist/enum-accessor.d.mts.map +1 -0
  27. package/dist/enum-accessor.mjs +45 -0
  28. package/dist/enum-accessor.mjs.map +1 -0
  29. package/dist/hashing-utils.d.mts +1 -1
  30. package/dist/hashing-utils.d.mts.map +1 -1
  31. package/dist/hashing-utils.mjs +5 -4
  32. package/dist/hashing-utils.mjs.map +1 -1
  33. package/dist/hashing.d.mts +2 -2
  34. package/dist/hashing.d.mts.map +1 -1
  35. package/dist/hashing.mjs +38 -11
  36. package/dist/hashing.mjs.map +1 -1
  37. package/dist/{namespace-id-CVpkSFUK.mjs → namespace-id-CUxYd4KL.mjs} +1 -1
  38. package/dist/{namespace-id-CVpkSFUK.mjs.map → namespace-id-CUxYd4KL.mjs.map} +1 -1
  39. package/dist/resolve-domain-model-BovPAsW2.mjs +20 -0
  40. package/dist/resolve-domain-model-BovPAsW2.mjs.map +1 -0
  41. package/dist/resolve-domain-model-CSEqpByI.d.mts +17 -0
  42. package/dist/resolve-domain-model-CSEqpByI.d.mts.map +1 -0
  43. package/dist/resolve-domain-model.d.mts +2 -0
  44. package/dist/resolve-domain-model.mjs +2 -0
  45. package/dist/types.d.mts +21 -4
  46. package/dist/types.d.mts.map +1 -0
  47. package/dist/types.mjs +40 -30
  48. package/dist/types.mjs.map +1 -1
  49. package/dist/validate-domain.d.mts +1 -1
  50. package/dist/validate-domain.mjs +6 -3
  51. package/dist/validate-domain.mjs.map +1 -1
  52. package/package.json +10 -6
  53. package/src/apply-specifier-default-control-policy.ts +12 -0
  54. package/src/canonicalization-storage-sort.ts +8 -1
  55. package/src/canonicalization.ts +13 -4
  56. package/src/contract-types.ts +9 -5
  57. package/src/control-policy.ts +25 -0
  58. package/src/cross-reference.ts +15 -3
  59. package/src/default-namespace.ts +36 -0
  60. package/src/domain-envelope.ts +3 -61
  61. package/src/domain-namespace-access.ts +32 -0
  62. package/src/domain-types.ts +32 -1
  63. package/src/enum-accessor.ts +173 -0
  64. package/src/exports/apply-specifier-default-control-policy.ts +1 -0
  65. package/src/exports/default-namespace.ts +1 -0
  66. package/src/exports/enum-accessor.ts +11 -0
  67. package/src/exports/resolve-domain-model.ts +1 -0
  68. package/src/exports/types.ts +16 -7
  69. package/src/hashing.ts +42 -13
  70. package/src/resolve-domain-model.ts +27 -0
  71. package/src/types.ts +18 -2
  72. package/src/validate-domain.ts +6 -0
  73. package/src/value-set-ref.ts +26 -0
  74. package/dist/canonicalization-DFE0HJkI.d.mts.map +0 -1
  75. package/dist/contract-types-xgwKtd7y.d.mts +0 -233
  76. package/dist/contract-types-xgwKtd7y.d.mts.map +0 -1
  77. package/dist/domain-envelope-4hyFtJ4_.d.mts +0 -110
  78. package/dist/domain-envelope-4hyFtJ4_.d.mts.map +0 -1
@@ -0,0 +1,32 @@
1
+ import { DomainNamespaceResolutionError } from './contract-validation-error';
2
+ import { soleDomainNamespaceId } from './default-namespace';
3
+ import type { ApplicationDomain } from './domain-envelope';
4
+ import type { ContractModelBase, ContractValueObject } from './domain-types';
5
+
6
+ /**
7
+ * Models map for the contract's single domain namespace. Throws when the
8
+ * contract does not declare exactly one namespace — bare-name access is
9
+ * ambiguous across namespaces and must be qualified explicitly (TML-2550).
10
+ */
11
+ export function domainModelsAtDefaultNamespace<TModels extends Record<string, ContractModelBase>>(
12
+ domain: ApplicationDomain<TModels>,
13
+ ): TModels {
14
+ const namespaceId = soleDomainNamespaceId(domain);
15
+ const domainNamespace = domain.namespaces[namespaceId];
16
+ if (domainNamespace === undefined) {
17
+ throw new DomainNamespaceResolutionError(
18
+ `domain namespace "${namespaceId}" is not present on the contract`,
19
+ );
20
+ }
21
+ return domainNamespace.models;
22
+ }
23
+
24
+ /**
25
+ * Value objects for the contract's single domain namespace, when present.
26
+ * Throws when the contract does not declare exactly one namespace.
27
+ */
28
+ export function domainValueObjectsAtDefaultNamespace<
29
+ TModels extends Record<string, ContractModelBase>,
30
+ >(domain: ApplicationDomain<TModels>): Record<string, ContractValueObject> | undefined {
31
+ return domain.namespaces[soleDomainNamespaceId(domain)]?.valueObjects;
32
+ }
@@ -1,4 +1,6 @@
1
1
  import type { CrossReference } from './cross-reference';
2
+ import type { JsonValue } from './types';
3
+ import type { ValueSetRef } from './value-set-ref';
2
4
 
3
5
  export type ScalarFieldType = {
4
6
  readonly kind: 'scalar';
@@ -23,6 +25,17 @@ export type ContractField = {
23
25
  readonly type: ContractFieldType;
24
26
  readonly many?: true;
25
27
  readonly dict?: true;
28
+ readonly valueSet?: ValueSetRef;
29
+ };
30
+
31
+ /**
32
+ * A domain enum: an ordered set of named members, each with a codec-encoded
33
+ * value. The `codecId` identifies the codec used to encode member values in
34
+ * storage. The `members` array is ordered (declaration order is preserved).
35
+ */
36
+ export type ContractEnum = {
37
+ readonly codecId: string;
38
+ readonly members: readonly { readonly name: string; readonly value: JsonValue }[];
26
39
  };
27
40
 
28
41
  export type ContractRelationOn = {
@@ -30,12 +43,30 @@ export type ContractRelationOn = {
30
43
  readonly targetFields: readonly string[];
31
44
  };
32
45
 
33
- export type ContractReferenceRelation = {
46
+ export type ContractRelationThrough = {
47
+ readonly table: string;
48
+ readonly namespaceId: string;
49
+ readonly parentColumns: readonly string[];
50
+ readonly childColumns: readonly string[];
51
+ readonly targetColumns: readonly string[];
52
+ };
53
+
54
+ export type ContractManyToManyRelation = {
55
+ readonly to: CrossReference;
56
+ readonly cardinality: 'N:M';
57
+ readonly on: ContractRelationOn;
58
+ readonly through: ContractRelationThrough;
59
+ };
60
+
61
+ export type ContractNonJunctionRelation = {
34
62
  readonly to: CrossReference;
35
63
  readonly cardinality: '1:1' | '1:N' | 'N:1';
36
64
  readonly on: ContractRelationOn;
65
+ readonly through?: never;
37
66
  };
38
67
 
68
+ export type ContractReferenceRelation = ContractManyToManyRelation | ContractNonJunctionRelation;
69
+
39
70
  export type ContractEmbedRelation = {
40
71
  readonly to: CrossReference;
41
72
  readonly cardinality: '1:1' | '1:N';
@@ -0,0 +1,173 @@
1
+ import type { Contract } from './contract-types';
2
+ import type { ContractEnum } from './domain-types';
3
+ import type { JsonValue } from './types';
4
+
5
+ /**
6
+ * Runtime view of a domain enum, built at the client from the emitted
7
+ * `ContractEnum` JSON (codec-encoded `JsonValue` members, literal types erased).
8
+ *
9
+ * This deliberately mirrors the accessor shape of the authoring-time
10
+ * `EnumTypeHandle` (in `contract-ts`) rather than reusing it: that handle carries
11
+ * the literal value generics and lives in the authoring layer, which the
12
+ * foundation layer cannot depend on. The two are the same surface seen from the
13
+ * two planes — authoring (typed) and runtime (validated JSON).
14
+ */
15
+ export interface EnumAccessor {
16
+ readonly values: readonly JsonValue[];
17
+ readonly names: readonly string[];
18
+ readonly members: Readonly<Record<string, JsonValue>>;
19
+ has(v: JsonValue): boolean;
20
+ nameOf(v: JsonValue): string | undefined;
21
+ ordinalOf(v: JsonValue): number;
22
+ }
23
+
24
+ export function createEnumAccessor(contractEnum: ContractEnum): EnumAccessor {
25
+ const values = Object.freeze(contractEnum.members.map((m) => m.value));
26
+ const names = Object.freeze(contractEnum.members.map((m) => m.name));
27
+ const members: Readonly<Record<string, JsonValue>> = Object.freeze(
28
+ Object.fromEntries(contractEnum.members.map((m) => [m.name, m.value])),
29
+ );
30
+
31
+ const valueSet = new Set(values);
32
+ const valueToName = new Map(contractEnum.members.map((m) => [m.value, m.name]));
33
+ const valueToOrdinal = new Map(values.map((v, i) => [v, i]));
34
+
35
+ return {
36
+ values,
37
+ names,
38
+ members,
39
+ has: (v: JsonValue) => valueSet.has(v),
40
+ nameOf: (v: JsonValue) => valueToName.get(v),
41
+ ordinalOf: (v: JsonValue) => valueToOrdinal.get(v) ?? -1,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Build the enum-accessor map for a single namespace, keyed by enum name.
47
+ * Each namespace facet exposes only its own enums — the IR keys enums under
48
+ * `domain.namespaces[ns].enum`, so the same name in two namespaces resolves
49
+ * independently rather than colliding in one flat map.
50
+ */
51
+ export function buildEnumsMapForNamespace(
52
+ domain: {
53
+ readonly namespaces: Readonly<
54
+ Record<string, { readonly enum?: Readonly<Record<string, ContractEnum>> }>
55
+ >;
56
+ },
57
+ namespaceId: string,
58
+ ): Record<string, EnumAccessor> {
59
+ const result: Record<string, EnumAccessor> = {};
60
+ const namespace = domain.namespaces[namespaceId];
61
+ if (namespace?.enum) {
62
+ for (const [name, contractEnum] of Object.entries(namespace.enum)) {
63
+ result[name] = createEnumAccessor(contractEnum);
64
+ }
65
+ }
66
+ return result;
67
+ }
68
+
69
+ /**
70
+ * Build the enum-accessor map for every namespace of a domain, keyed by
71
+ * namespace id then enum name. This is the lane-agnostic enum surface the
72
+ * `db.enums` facade member exposes: enums are contract metadata, the same
73
+ * whether reached through the sql lane or the orm lane, so the facade builds
74
+ * this once and projects it per target.
75
+ */
76
+ export function buildNamespacedEnums(domain: {
77
+ readonly namespaces: Readonly<
78
+ Record<string, { readonly enum?: Readonly<Record<string, ContractEnum>> }>
79
+ >;
80
+ }): Record<string, Record<string, EnumAccessor>> {
81
+ const result: Record<string, Record<string, EnumAccessor>> = {};
82
+ for (const namespaceId of Object.keys(domain.namespaces)) {
83
+ result[namespaceId] = buildEnumsMapForNamespace(domain, namespaceId);
84
+ }
85
+ return result;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Type-level projection of the namespaced enum surface.
90
+ //
91
+ // These types derive the literal-preserving accessor shape from the contract,
92
+ // hung off the `db.enums` facade map (`db.enums.<ns>.<Name>`). They are the
93
+ // same accessors the runtime builds above, but typed from the two emission
94
+ // paths:
95
+ // - Emitted contracts carry the literal enum entries under
96
+ // `domain.namespaces[ns].enum`; each maps to a `ContractEnumAccessor`.
97
+ // - The no-emit (built) contract carries them flat on `enumAccessors`
98
+ // (already accessor-shaped, literal-preserving), since its built domain
99
+ // type does not narrow `namespaces[ns].enum`. All authored enums land in
100
+ // the single built namespace, so exposing the flat map per namespace is
101
+ // correct there.
102
+ // Only `SqlContractResult` carries `enumAccessors`; emitted contracts never
103
+ // do, so the two carriers never overlap.
104
+ // ---------------------------------------------------------------------------
105
+
106
+ type Present<T> = Exclude<T, undefined>;
107
+
108
+ // A domain enum entry as carried in `domain.namespaces[ns].enum[name]`: an
109
+ // ordered member tuple. The no-emit (built) path preserves the literal member
110
+ // values so the derived accessor keeps its literal `values`/`names`/`members`.
111
+ type EnumMemberEntry = { readonly name: string; readonly value: unknown };
112
+ type EnumEntry = { readonly members: readonly EnumMemberEntry[] };
113
+
114
+ type EnumEntryValues<Entry extends EnumEntry> = {
115
+ readonly [I in keyof Entry['members']]: Entry['members'][I] extends EnumMemberEntry
116
+ ? Entry['members'][I]['value']
117
+ : never;
118
+ };
119
+
120
+ type EnumEntryNames<Entry extends EnumEntry> = {
121
+ readonly [I in keyof Entry['members']]: Entry['members'][I] extends EnumMemberEntry
122
+ ? Entry['members'][I]['name']
123
+ : never;
124
+ };
125
+
126
+ type EnumEntryMembers<Entry extends EnumEntry> = {
127
+ readonly [M in Entry['members'][number] as M['name']]: M['value'];
128
+ };
129
+
130
+ // The runtime accessor shape for one enum, with literal `values`/`names`/
131
+ // `members` derived from the entry's member tuple. Mirrors `EnumAccessor`'s
132
+ // runtime surface and the authoring `EnumTypeHandle` accessor.
133
+ export type ContractEnumAccessor<Entry extends EnumEntry> = {
134
+ readonly values: EnumEntryValues<Entry>;
135
+ readonly names: EnumEntryNames<Entry>;
136
+ readonly members: EnumEntryMembers<Entry>;
137
+ has(v: EnumEntryValues<Entry>[number]): boolean;
138
+ nameOf(v: EnumEntryValues<Entry>[number]): string | undefined;
139
+ ordinalOf(v: EnumEntryValues<Entry>[number]): number;
140
+ };
141
+
142
+ type EnumEntriesToAccessors<Enums> = {
143
+ readonly [K in keyof Enums]: Enums[K] extends EnumEntry ? ContractEnumAccessor<Enums[K]> : never;
144
+ };
145
+
146
+ type BuiltEnumAccessorsOf<TContract> = TContract extends {
147
+ readonly enumAccessors: infer A;
148
+ }
149
+ ? A
150
+ : Record<never, never>;
151
+
152
+ type NamespaceEnumEntries<TNamespace> = TNamespace extends {
153
+ readonly enum?: infer E;
154
+ }
155
+ ? unknown extends E
156
+ ? Record<never, never>
157
+ : Present<E>
158
+ : Record<never, never>;
159
+
160
+ // The per-namespace enum accessors. Each namespace exposes only its own enums
161
+ // (the IR's `domain.namespaces[ns].enum`), so the same enum name in two
162
+ // namespaces resolves to each namespace's own accessor.
163
+ export type NamespaceEnumAccessors<
164
+ TContract extends Contract,
165
+ NsId extends keyof TContract['domain']['namespaces'],
166
+ > = EnumEntriesToAccessors<NamespaceEnumEntries<TContract['domain']['namespaces'][NsId]>> &
167
+ BuiltEnumAccessorsOf<TContract>;
168
+
169
+ // The lane-agnostic enum surface exposed on the `db.enums` facade member: a
170
+ // namespace-keyed map projected per target exactly like `db.sql` / `db.orm`.
171
+ export type NamespacedEnums<TContract extends Contract> = {
172
+ readonly [Ns in keyof TContract['domain']['namespaces']]: NamespaceEnumAccessors<TContract, Ns>;
173
+ };
@@ -0,0 +1 @@
1
+ export { applySpecifierDefaultControlPolicy } from '../apply-specifier-default-control-policy';
@@ -0,0 +1 @@
1
+ export { soleDomainNamespaceId, UNBOUND_DOMAIN_NAMESPACE_ID } from '../default-namespace';
@@ -0,0 +1,11 @@
1
+ export type {
2
+ ContractEnumAccessor,
3
+ EnumAccessor,
4
+ NamespacedEnums,
5
+ NamespaceEnumAccessors,
6
+ } from '../enum-accessor';
7
+ export {
8
+ buildEnumsMapForNamespace,
9
+ buildNamespacedEnums,
10
+ createEnumAccessor,
11
+ } from '../enum-accessor';
@@ -0,0 +1 @@
1
+ export { type ResolvedDomainModel, resolveDomainModel } from '../resolve-domain-model';
@@ -1,33 +1,39 @@
1
1
  export type {
2
2
  Contract,
3
3
  ContractExecutionSection,
4
- ContractModelsMap,
5
- ContractValueObjectsMap,
4
+ ContractModelDefinitions,
5
+ ContractValueObjectDefinitions,
6
6
  } from '../contract-types';
7
7
  export { DomainNamespaceResolutionError } from '../contract-validation-error';
8
+ export type { ControlPolicy } from '../control-policy';
9
+ export { effectiveControlPolicy } from '../control-policy';
8
10
  export type { CrossReference } from '../cross-reference';
9
11
  export { CrossReferenceSchema, crossRef } from '../cross-reference';
12
+ export { soleDomainNamespaceId } from '../default-namespace';
10
13
  export type {
11
14
  ApplicationDomain,
12
15
  ApplicationDomainNamespace,
13
16
  ContractWithDomain,
14
17
  } from '../domain-envelope';
18
+ export { UNBOUND_DOMAIN_NAMESPACE_ID } from '../domain-envelope';
15
19
  export {
16
- contractModels,
17
- contractValueObjects,
18
- resolveSingleDomainNamespaceId,
19
- UNBOUND_DOMAIN_NAMESPACE_ID,
20
- } from '../domain-envelope';
20
+ domainModelsAtDefaultNamespace,
21
+ domainValueObjectsAtDefaultNamespace,
22
+ } from '../domain-namespace-access';
21
23
  export type {
22
24
  ContractDiscriminator,
23
25
  ContractEmbedRelation,
26
+ ContractEnum,
24
27
  ContractField,
25
28
  ContractFieldType,
29
+ ContractManyToManyRelation,
26
30
  ContractModel,
27
31
  ContractModelBase,
32
+ ContractNonJunctionRelation,
28
33
  ContractReferenceRelation,
29
34
  ContractRelation,
30
35
  ContractRelationOn,
36
+ ContractRelationThrough,
31
37
  ContractValueObject,
32
38
  ContractVariantEntry,
33
39
  EmbedRelationKeys,
@@ -39,6 +45,7 @@ export type {
39
45
  } from '../domain-types';
40
46
  export type { NamespaceId } from '../namespace-id';
41
47
  export { asNamespaceId } from '../namespace-id';
48
+ export { type ResolvedDomainModel, resolveDomainModel } from '../resolve-domain-model';
42
49
  export type {
43
50
  $,
44
51
  Brand,
@@ -58,6 +65,7 @@ export type {
58
65
  GeneratedValueSpec,
59
66
  JsonPrimitive,
60
67
  JsonValue,
68
+ LedgerEntryRecord,
61
69
  PlanMeta,
62
70
  ProfileHashBase,
63
71
  Source,
@@ -74,3 +82,4 @@ export {
74
82
  isExecutionMutationDefaultValue,
75
83
  profileHash,
76
84
  } from '../types';
85
+ export type { ValueSetRef } from '../value-set-ref';
package/src/hashing.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { blindCast, castAs } from '@prisma-next/utils/casts';
2
3
  import { ifDefined } from '@prisma-next/utils/defined';
3
4
  import type { JsonObject } from '@prisma-next/utils/json';
4
5
  import {
@@ -11,6 +12,33 @@ import type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from './type
11
12
 
12
13
  const SCHEMA_VERSION = '1';
13
14
 
15
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
16
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
17
+ }
18
+
19
+ // Storage hashes fingerprint table/column layout, not which target pack emitted a
20
+ // namespace. Persisted contract.json carries namespace `kind` discriminators;
21
+ // authoring-time hashes never included them (IR `kind` is non-enumerable).
22
+ function omitNamespaceKindsForHash(storage: unknown): unknown {
23
+ if (!isPlainRecord(storage)) {
24
+ return storage;
25
+ }
26
+ const namespaces = storage['namespaces'];
27
+ if (!isPlainRecord(namespaces)) {
28
+ return storage;
29
+ }
30
+ const stripped: Record<string, unknown> = {};
31
+ for (const [nsId, ns] of Object.entries(namespaces)) {
32
+ if (isPlainRecord(ns)) {
33
+ const { kind: _kind, ...rest } = ns;
34
+ stripped[nsId] = rest;
35
+ } else {
36
+ stripped[nsId] = ns;
37
+ }
38
+ }
39
+ return { ...storage, namespaces: stripped };
40
+ }
41
+
14
42
  function sha256(content: string): string {
15
43
  const hash = createHash('sha256');
16
44
  hash.update(content);
@@ -24,28 +52,23 @@ type HashContractSection = Record<string, unknown> & {
24
52
 
25
53
  function hashContract(section: HashContractSection): string {
26
54
  const { shouldPreserveEmpty, sortStorage, ...sectionData } = section;
27
- // Blind cast: the synthesised object is a hash-only stand-in
28
- // never returned to callers, never executed as a Contract.
29
- // `canonicalizeContract` only walks the storage / execution /
30
- // capabilities slices, all of which are populated above, so the
31
- // missing precise Contract typing on the other slots is
32
- // immaterial for the hash result.
33
- const contract = {
55
+ const storageForHash = omitNamespaceKindsForHash(sectionData['storage'] ?? {});
56
+ const contract = blindCast<Contract, 'hash-only partial contract for canonicalizeContract'>({
34
57
  targetFamily: sectionData['targetFamily'],
35
58
  target: sectionData['target'],
36
59
  roots: {},
37
60
  domain: { namespaces: {} },
38
- storage: sectionData['storage'] ?? {},
39
61
  execution: sectionData['execution'],
40
62
  extensionPacks: {},
41
63
  capabilities: sectionData['capabilities'] ?? {},
42
64
  meta: {},
43
65
  profileHash: '',
44
66
  ...sectionData,
45
- } as unknown as Contract;
67
+ storage: storageForHash,
68
+ });
46
69
  return canonicalizeContract(contract, {
47
70
  schemaVersion: SCHEMA_VERSION,
48
- serializeContract: (c) => JSON.parse(JSON.stringify(c)) as JsonObject,
71
+ serializeContract: (c) => castAs<JsonObject>(JSON.parse(JSON.stringify(c))),
49
72
  ...ifDefined('shouldPreserveEmpty', shouldPreserveEmpty),
50
73
  ...ifDefined('sortStorage', sortStorage),
51
74
  });
@@ -60,7 +83,9 @@ export type ComputeStorageHashArgs = {
60
83
  };
61
84
 
62
85
  export function computeStorageHash(args: ComputeStorageHashArgs): StorageHashBase<string> {
63
- return sha256(hashContract(args)) as StorageHashBase<string>;
86
+ return blindCast<StorageHashBase<string>, 'sha256 digest of canonicalized storage'>(
87
+ sha256(hashContract(args)),
88
+ );
64
89
  }
65
90
 
66
91
  export function computeExecutionHash(args: {
@@ -68,7 +93,9 @@ export function computeExecutionHash(args: {
68
93
  targetFamily: string;
69
94
  execution: Record<string, unknown>;
70
95
  }): ExecutionHashBase<string> {
71
- return sha256(hashContract(args)) as ExecutionHashBase<string>;
96
+ return blindCast<ExecutionHashBase<string>, 'sha256 digest of canonicalized execution'>(
97
+ sha256(hashContract(args)),
98
+ );
72
99
  }
73
100
 
74
101
  export function computeProfileHash(args: {
@@ -76,5 +103,7 @@ export function computeProfileHash(args: {
76
103
  targetFamily: string;
77
104
  capabilities: Record<string, Record<string, boolean>>;
78
105
  }): ProfileHashBase<string> {
79
- return sha256(hashContract(args)) as ProfileHashBase<string>;
106
+ return blindCast<ProfileHashBase<string>, 'sha256 digest of canonicalized profile'>(
107
+ sha256(hashContract(args)),
108
+ );
80
109
  }
@@ -0,0 +1,27 @@
1
+ import type { ApplicationDomain } from './domain-envelope';
2
+ import type { ContractModelBase } from './domain-types';
3
+
4
+ export interface ResolvedDomainModel {
5
+ readonly namespaceId: string;
6
+ readonly model: ContractModelBase;
7
+ }
8
+
9
+ /**
10
+ * Resolve a bare domain model name to its namespace coordinate and model IR by
11
+ * scanning the contract's namespaces. For the single-namespace contracts in
12
+ * scope the scan is exact; cross-namespace bare-name collisions are selected
13
+ * explicitly (TML-2550).
14
+ */
15
+ export function resolveDomainModel(
16
+ domain: ApplicationDomain,
17
+ modelName: string,
18
+ ): ResolvedDomainModel | undefined {
19
+ for (const namespaceId of Object.keys(domain.namespaces)) {
20
+ const model = domain.namespaces[namespaceId]?.models[modelName];
21
+ if (model !== undefined) {
22
+ return { namespaceId, model };
23
+ }
24
+ }
25
+
26
+ return undefined;
27
+ }
package/src/types.ts CHANGED
@@ -57,11 +57,13 @@ export type StorageEntitySlot = Readonly<Record<string, unknown>>;
57
57
 
58
58
  /**
59
59
  * Plain-data namespace entry in a storage block. Every hydrated contract
60
- * carries at least `id` plus zero or more entity-kind slot maps (`tables`,
61
- * `collections`, …). Foundation declares only this shape — no IR machinery.
60
+ * carries at least `id` plus entity-kind slot maps under `entries`
61
+ * (`table`, `collection`, …). Foundation declares only this shape — no IR
62
+ * machinery.
62
63
  */
63
64
  export interface StorageNamespace {
64
65
  readonly id: string;
66
+ readonly entries: Readonly<Record<string, StorageEntitySlot>>;
65
67
  }
66
68
 
67
69
  /**
@@ -243,3 +245,17 @@ export interface ContractMarkerRecord {
243
245
  readonly meta: Record<string, unknown>;
244
246
  readonly invariants: readonly string[];
245
247
  }
248
+
249
+ /**
250
+ * One applied migration edge from the per-space ledger journal.
251
+ * Returned by `readLedger` in append (apply) order.
252
+ */
253
+ export interface LedgerEntryRecord {
254
+ readonly space: string;
255
+ readonly migrationName: string;
256
+ readonly migrationHash: string;
257
+ readonly from: string | null;
258
+ readonly to: string;
259
+ readonly appliedAt: Date;
260
+ readonly operationCount: number;
261
+ }
@@ -140,6 +140,12 @@ function validateVariantsAndBases(modelIndex: ModelIndex, errors: string[]): voi
140
140
  function validateRelationTargets(modelIndex: ModelIndex, errors: string[]): void {
141
141
  for (const { namespaceId, name: modelName, model } of iterateIndexedModels(modelIndex)) {
142
142
  for (const [relName, relation] of Object.entries(model.relations ?? {})) {
143
+ // Cross-space relations (relation.to.space is defined) target a model in a
144
+ // different contract space. The local domain index only contains this
145
+ // contract's own models, so the target is intentionally absent here.
146
+ // Existence of the target is verified by the aggregate verifier at
147
+ // deploy time (after the spaces are composed), not at parse time.
148
+ if (relation.to.space !== undefined) continue;
143
149
  if (!lookupModel(modelIndex, relation.to)) {
144
150
  errors.push(
145
151
  `Relation "${relName}" on model "${namespaceId}:${modelName}" targets "${relation.to.namespace}:${relation.to.model}" which does not exist in domain.namespaces`,
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Space-aware reference coordinate for a domain enum or storage value-set.
3
+ *
4
+ * `plane` names the contract plane the referenced entity lives in:
5
+ * - `'domain'` — the entity lives in the domain plane's `enum` slot.
6
+ * - `'storage'` — the entity lives in the storage plane's `valueSet` slot.
7
+ *
8
+ * `entityKind` names the source entity-kind:
9
+ * - `'enum'` — the referenced entity is a domain enum.
10
+ * - `'value-set'` — the referenced entity is a storage value-set.
11
+ *
12
+ * `namespaceId` admits the `UNBOUND_NAMESPACE_ID` (`__unbound__`) sentinel for
13
+ * single-namespace (unbound) references.
14
+ *
15
+ * Cross-space discrimination is presence-based: when `spaceId` is absent the
16
+ * reference is local (same contract-space); when `spaceId` is present the
17
+ * reference is cross-space. This mirrors the `ForeignKeyReference` carrier
18
+ * convention — no separate tag field — so local refs are JSON-minimal.
19
+ */
20
+ export interface ValueSetRef {
21
+ readonly plane: 'domain' | 'storage';
22
+ readonly namespaceId: string;
23
+ readonly entityKind: 'enum' | 'value-set';
24
+ readonly name: string;
25
+ readonly spaceId?: string;
26
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"canonicalization-DFE0HJkI.d.mts","names":[],"sources":["../src/canonicalization.ts"],"mappings":";;;;;;AAcA;;;;;;KAAY,iBAAA,IAAqB,QAAA,EAAU,QAAA,KAAa,UAAU;;AAAA;AAUlE;;;;AAA6D;AAS7D;KATY,sBAAA,IAA0B,IAAuB;;;AASlB;AA6K3C;;;;KA7KY,WAAA,IAAe,OAAgB;AAAA,UA6K1B,2BAAA;EAAA,SACN,aAAA;EAAA;;;;;;;;AA0ByB;AAQpC;EAlCW,SAWA,iBAAA,EAAmB,iBAAA;;;;;;;WAOnB,mBAAA,GAAsB,sBAAA;EAiB/B;;;;;AAEO;AA0BT;EA5BE,SATS,WAAA,GAAc,WAAA;AAAA;;;;;;iBAQT,4BAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,EAAS,2BAAA,GACR,MAAA;AAAA,iBA0Ba,oBAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,EAAS,2BAA2B"}