@githolon/dsl 0.103.4 → 0.103.6
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/dart/CHANGELOG.md +8 -0
- package/dart/lib/src/runner/node-entry.mjs +333 -117
- package/dart/lib/src/runner/offline-install.mjs +173 -67
- package/dart/lib/src/session_bound.dart +111 -2
- package/dart/lib/src/test_harness.dart +51 -41
- package/dart/lib/src/webview_bridge.dart +714 -60
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_test.dart +62 -5
- package/dart/test/cloud_bound_topology_test.dart +59 -38
- package/dart/test/session_bound_test.dart +25 -10
- package/dart/test/webview_bridge_test.dart +326 -7
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/nomos_types.dart +1 -0
- package/dart_types/lib/src/git_object_id.dart +37 -0
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +347 -113
- package/src/births.ts +16 -1
- package/src/business_birth.ts +55 -34
- package/src/codegen_dart.ts +68 -12
- package/src/codegen_dart_application.ts +114 -19
- package/src/codegen_dart_client.ts +149 -6
- package/src/codegen_proof.ts +16 -9
- package/src/codegen_provider_dart.ts +5 -0
- package/src/codegen_ts.ts +115 -19
- package/src/compile_package_main.ts +159 -43
- package/src/fields.ts +43 -0
- package/src/framework/domain_lifecycle.ts +108 -88
- package/src/framework/genesis.ts +2 -1
- package/src/framework/home.ts +6 -43
- package/src/framework/workspaces.ts +42 -25
- package/src/idof_lint.ts +65 -51
- package/src/index.ts +21 -1
- package/src/intent.ts +11 -1
- package/src/law_graph.ts +81 -11
- package/src/relation_schema.ts +18 -0
- package/src/shape_compat.ts +3 -0
- package/src/stable_ids_for_module.ts +7 -1
- package/src/value_object.ts +40 -0
package/src/idof_lint.ts
CHANGED
|
@@ -105,72 +105,86 @@ export function assertIdOfLintForDirective(dir: Directive<unknown>, where: strin
|
|
|
105
105
|
// tenant-facing idOf lint here would force framework wire schemas to masquerade as public
|
|
106
106
|
// application schemas and, worse, reject every aggregate that opts into owned custody.
|
|
107
107
|
if ((dir as { businessBirthInternal?: unknown }).businessBirthInternal === true) return;
|
|
108
|
-
const probed = probePlanOps(dir, where);
|
|
109
|
-
if (probed === undefined) return;
|
|
110
108
|
let shape: Record<string, z.ZodTypeAny>;
|
|
111
109
|
try {
|
|
112
110
|
shape = zodObjectShape(dir.payloadSchema as z.ZodTypeAny);
|
|
113
111
|
} catch {
|
|
114
112
|
return;
|
|
115
113
|
}
|
|
116
|
-
//
|
|
117
|
-
// `
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
114
|
+
// Probe both payload presences. The ordinary proof deliberately omits optional fields, but that
|
|
115
|
+
// would make `optional().setOptional(...)` invisible to the identity lint: exactly the shape that
|
|
116
|
+
// can smuggle a business string into a typed aggregate reference. The second probe includes every
|
|
117
|
+
// sampleable top-level optional and deliberately ignores a possibly sparse author example. It is
|
|
118
|
+
// lint-only; all existing proof/effect consumers retain the ordinary omitted-optional semantics.
|
|
119
|
+
const probedVariants = [
|
|
120
|
+
probePlanOps(dir, where),
|
|
121
|
+
probePlanOps(dir, `${where} (optional fields present)`, {
|
|
122
|
+
includeOptionalTopLevel: true,
|
|
123
|
+
useDirectiveExample: false,
|
|
124
|
+
}),
|
|
125
|
+
];
|
|
126
|
+
if (probedVariants.every((variant) => variant === undefined)) return;
|
|
127
|
+
for (const probed of probedVariants) {
|
|
128
|
+
if (probed === undefined) continue;
|
|
129
|
+
// Reverse index: sampled STRING value → the top-level payload field name(s) that produced it.
|
|
130
|
+
// `synthesizePayload` gives every string-kind field a DISTINCT `"sample-<field>"` value (or a
|
|
131
|
+
// schema-constrained fallback), so an exact match is unambiguous in practice.
|
|
132
|
+
const byValue = new Map<string, string>();
|
|
133
|
+
for (const [name, value] of Object.entries(probed.synth.value)) {
|
|
134
|
+
if (typeof value === "string") byValue.set(value, name);
|
|
135
|
+
}
|
|
136
|
+
for (const op of probed.ops) {
|
|
137
|
+
// A bound mutation addresses one existing aggregate instance. The payload field carrying that
|
|
138
|
+
// address is therefore not "some string" or merely "some branded id": it must be idOf(the exact
|
|
139
|
+
// aggregate being mutated). This is the identity spine of the generated application API. Without
|
|
140
|
+
// this guard a model can compile `entryId: z.string()`, after which every generated caller can mix
|
|
141
|
+
// unrelated record ids and is encouraged to reconstruct identity as application state.
|
|
142
|
+
const effectiveMarker = op.marker ?? (op.aggregateType === dir.aggregateId ? dir.marker : "mutates");
|
|
143
|
+
if (op.aggregateType !== undefined && (effectiveMarker === "mutates" || effectiveMarker === "archives")) {
|
|
144
|
+
const payloadField = byValue.get(op.aggregateId);
|
|
145
|
+
if (payloadField !== undefined) {
|
|
146
|
+
const fieldSchema = shape[payloadField];
|
|
147
|
+
const actualTarget = fieldSchema === undefined ? undefined : idOfMetaOf(fieldSchema);
|
|
148
|
+
if (actualTarget !== op.aggregateType) {
|
|
149
|
+
const expected = shortAggName(op.aggregateType);
|
|
150
|
+
const actual = actualTarget === undefined
|
|
151
|
+
? "an untyped string"
|
|
152
|
+
: `idOf(${shortAggName(actualTarget)})`;
|
|
153
|
+
const verb = effectiveMarker === "archives" ? "archive" : "mutate";
|
|
154
|
+
throw new IdOfLintError(
|
|
142
155
|
`directive '${dir.id}': payload field '${payloadField}' selects the ${expected} record to ${verb} ` +
|
|
143
156
|
`but carries ${actual} — declare it as idOf(${expected}) so create, update, find and watch use ` +
|
|
144
157
|
`the same generated NomosRef<${expected}>.`,
|
|
145
|
-
|
|
158
|
+
);
|
|
159
|
+
}
|
|
146
160
|
}
|
|
147
161
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
if (op.kind !== "set" && op.kind !== "setEntry") continue;
|
|
163
|
+
if (typeof op.value !== "string") continue;
|
|
164
|
+
const targetAgg = refTargetOf(op);
|
|
165
|
+
if (targetAgg === undefined) continue; // not a ref-kind field (or one the DSL can't name a target for)
|
|
166
|
+
const payloadField = byValue.get(op.value);
|
|
167
|
+
if (payloadField === undefined) continue; // not traceable to a top-level payload field — no verdict
|
|
168
|
+
const fieldSchema = shape[payloadField];
|
|
169
|
+
if (fieldSchema === undefined) continue;
|
|
170
|
+
const onAgg = shortAggName(op.aggregateType ?? op.aggregateId);
|
|
171
|
+
const targetName = shortAggName(targetAgg);
|
|
172
|
+
const actualTarget = idOfMetaOf(fieldSchema);
|
|
173
|
+
if (actualTarget === targetAgg) continue;
|
|
174
|
+
if (actualTarget !== undefined) {
|
|
175
|
+
throw new IdOfLintError(
|
|
176
|
+
`directive '${dir.id}': payload field '${payloadField}' feeds ref field '${onAgg}.${op.field}' ` +
|
|
177
|
+
`(-> ${targetName}) but carries idOf(${shortAggName(actualTarget)}) — declare it as ` +
|
|
178
|
+
`idOf(${targetName}) so generated callers carry NomosRef<${targetName}>.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (!isBareString(fieldSchema) && brandedIdMetaOf(fieldSchema) === undefined) continue;
|
|
162
182
|
throw new IdOfLintError(
|
|
163
183
|
`directive '${dir.id}': payload field '${payloadField}' feeds ref field '${onAgg}.${op.field}' ` +
|
|
164
|
-
`(-> ${targetName}) but
|
|
165
|
-
`
|
|
184
|
+
`(-> ${targetName}) but is not idOf(${targetName}) — declare it as idOf(${targetName}) so generated ` +
|
|
185
|
+
`Dart/TS carry NomosRef<${targetName}>, matching the read model. (dart_codegen_parity.md P2)`,
|
|
166
186
|
);
|
|
167
187
|
}
|
|
168
|
-
if (!isBareString(fieldSchema) && brandedIdMetaOf(fieldSchema) === undefined) continue;
|
|
169
|
-
throw new IdOfLintError(
|
|
170
|
-
`directive '${dir.id}': payload field '${payloadField}' feeds ref field '${onAgg}.${op.field}' ` +
|
|
171
|
-
`(-> ${targetName}) but is not idOf(${targetName}) — declare it as idOf(${targetName}) so generated ` +
|
|
172
|
-
`Dart/TS carry NomosRef<${targetName}>, matching the read model. (dart_codegen_parity.md P2)`,
|
|
173
|
-
);
|
|
174
188
|
}
|
|
175
189
|
}
|
|
176
190
|
|
package/src/index.ts
CHANGED
|
@@ -56,10 +56,16 @@ export {
|
|
|
56
56
|
jsonWithNullSchema,
|
|
57
57
|
type EvidenceRefValue,
|
|
58
58
|
evidenceRefSchema,
|
|
59
|
+
type NomosRefValue,
|
|
60
|
+
nomosRefValueSchema,
|
|
59
61
|
type Sha256Digest,
|
|
62
|
+
type GitObjectId,
|
|
60
63
|
sha256Digest,
|
|
64
|
+
gitObjectId,
|
|
61
65
|
isSha256DigestSchema,
|
|
66
|
+
isGitObjectIdSchema,
|
|
62
67
|
NOMOS_SHA256_DIGEST_BRAND,
|
|
68
|
+
NOMOS_GIT_OBJECT_ID_BRAND,
|
|
63
69
|
// FIELD FACETS (the endorse model — security_model.md): authority/standing/attribution gate
|
|
64
70
|
// set() acceptance of PrincipalEvidence; secret/public are the confidentiality lattice.
|
|
65
71
|
authorityField,
|
|
@@ -80,6 +86,8 @@ export {
|
|
|
80
86
|
idOf,
|
|
81
87
|
idOfMetaOf,
|
|
82
88
|
idOfTargetKindOf,
|
|
89
|
+
refOf,
|
|
90
|
+
refOfMetaOf,
|
|
83
91
|
versionedRefOf,
|
|
84
92
|
versionedRefOfMetaOf,
|
|
85
93
|
brandedId,
|
|
@@ -87,7 +95,9 @@ export {
|
|
|
87
95
|
voMetaOf,
|
|
88
96
|
voSample,
|
|
89
97
|
NOMOS_VO_BRAND,
|
|
98
|
+
NOMOS_REFOF_BRAND,
|
|
90
99
|
NOMOS_VERSIONED_REF_BRAND,
|
|
100
|
+
type RefOfMeta,
|
|
91
101
|
type VersionedRefOfMeta,
|
|
92
102
|
geo,
|
|
93
103
|
type ValueObject,
|
|
@@ -617,13 +627,21 @@ export {
|
|
|
617
627
|
} from "./framework/capability.js";
|
|
618
628
|
export {
|
|
619
629
|
DOMAIN_INSTALLATION_PHASES,
|
|
630
|
+
DOMAIN_LIFECYCLE_DIRECTIVES,
|
|
631
|
+
MODEL_LIFECYCLE_AUTHORITY,
|
|
620
632
|
DomainInstallation,
|
|
633
|
+
DomainInstallExecutor,
|
|
634
|
+
DomainInstallationPhase,
|
|
635
|
+
currentLawReference,
|
|
636
|
+
domainInstallGenesisOffer,
|
|
621
637
|
domainInstallationAggregateId,
|
|
622
638
|
installDomain,
|
|
623
639
|
stageDomain,
|
|
624
640
|
promoteDomain,
|
|
625
641
|
rollbackDomain,
|
|
626
|
-
type
|
|
642
|
+
type DomainLifecycleDirectiveId,
|
|
643
|
+
type CurrentLawReference,
|
|
644
|
+
type LawClosureReference,
|
|
627
645
|
} from "./framework/domain_lifecycle.js";
|
|
628
646
|
export {
|
|
629
647
|
WORKSPACE_SYNC_DIRECTIONS,
|
|
@@ -666,6 +684,7 @@ export {
|
|
|
666
684
|
// NATIVE ZANZIBAR RELATION SCHEMA (P1) — the userset-rewrite law as pure data.
|
|
667
685
|
export {
|
|
668
686
|
relationType,
|
|
687
|
+
relationRef,
|
|
669
688
|
isRelationTypeDecl,
|
|
670
689
|
validateRelationType,
|
|
671
690
|
self,
|
|
@@ -691,6 +710,7 @@ export {
|
|
|
691
710
|
selfObject,
|
|
692
711
|
objectOf,
|
|
693
712
|
type RelationObjectToken,
|
|
713
|
+
type RelationRef,
|
|
694
714
|
type RelationTypeDecl,
|
|
695
715
|
type RelationTypeBuilder,
|
|
696
716
|
type RelationDefAst,
|
package/src/intent.ts
CHANGED
|
@@ -24,7 +24,11 @@ import type {
|
|
|
24
24
|
InvariantReadIdPart,
|
|
25
25
|
WorkspaceInvariantDecl,
|
|
26
26
|
} from "./framework/workspace_invariant.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
BUSINESS_BIRTH_POLICY_BINDING,
|
|
29
|
+
businessBirthConsequence,
|
|
30
|
+
type BusinessBirthDirective,
|
|
31
|
+
} from "./business_birth.js";
|
|
28
32
|
import type { BusinessBirthDecl } from "./births.js";
|
|
29
33
|
import type { WorkspaceSharingPolicy } from "./workspace_sharing.js";
|
|
30
34
|
|
|
@@ -164,6 +168,7 @@ function remapNativeReads(
|
|
|
164
168
|
|
|
165
169
|
export interface BusinessIntent<I = unknown> extends Directive<I> {
|
|
166
170
|
readonly [INTENT_BRAND]: true;
|
|
171
|
+
readonly [BUSINESS_BIRTH_POLICY_BINDING]?: (domains: readonly string[]) => void;
|
|
167
172
|
readonly steps: readonly IntentStep<I, any>[];
|
|
168
173
|
/** The complete public business boundary. Runtime lowering still uses Directive.payloadSchema privately. */
|
|
169
174
|
readonly contract: IntentContract<I>;
|
|
@@ -433,6 +438,8 @@ class IntentBuilder<I> {
|
|
|
433
438
|
};
|
|
434
439
|
|
|
435
440
|
const businessBirth = (birthSteps[0]?.directive as BusinessBirthDirective | undefined)?.businessBirth;
|
|
441
|
+
const bindBusinessBirthPolicy =
|
|
442
|
+
(birthSteps[0]?.directive as BusinessBirthDirective | undefined)?.[BUSINESS_BIRTH_POLICY_BINDING];
|
|
436
443
|
return {
|
|
437
444
|
[INTENT_BRAND]: true,
|
|
438
445
|
id: this.id,
|
|
@@ -472,6 +479,9 @@ class IntentBuilder<I> {
|
|
|
472
479
|
? {}
|
|
473
480
|
: {
|
|
474
481
|
birthsChild: true,
|
|
482
|
+
...(bindBusinessBirthPolicy === undefined
|
|
483
|
+
? {}
|
|
484
|
+
: { [BUSINESS_BIRTH_POLICY_BINDING]: bindBusinessBirthPolicy }),
|
|
475
485
|
businessBirth: {
|
|
476
486
|
...businessBirth,
|
|
477
487
|
publicPayloadSchema: this.contractDeclaration.accepts,
|
package/src/law_graph.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
lowerLawValueType,
|
|
32
32
|
type LawConstant,
|
|
33
33
|
type LawLiteral,
|
|
34
|
+
type LawValueMember,
|
|
34
35
|
type LawValueConstraint,
|
|
35
36
|
type LawValueType,
|
|
36
37
|
} from "./law_value_type.js";
|
|
@@ -176,6 +177,47 @@ export interface LawPayloadContractNode {
|
|
|
176
177
|
readonly fields: readonly LawStructuralRef<"value-field">[];
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
/** Compiler-owned facts used to author `.owned()` effects. These are part of the
|
|
181
|
+
* internal typed USDA call contract, never the tenant-facing business contract. */
|
|
182
|
+
const OWNED_AUTHORING_PAYLOAD_MEMBERS = [
|
|
183
|
+
{
|
|
184
|
+
name: OWNED_KEY_PAYLOAD_FIELD,
|
|
185
|
+
required: false,
|
|
186
|
+
value: { kind: "string", constraints: [] },
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: OWNED_CUSTODY_PAYLOAD_FIELD,
|
|
190
|
+
required: false,
|
|
191
|
+
value: { kind: "string", constraints: [] },
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
|
|
195
|
+
required: false,
|
|
196
|
+
value: {
|
|
197
|
+
kind: "list",
|
|
198
|
+
element: {
|
|
199
|
+
kind: "record",
|
|
200
|
+
fields: [
|
|
201
|
+
{ name: "type", required: true, value: { kind: "string", constraints: [] } },
|
|
202
|
+
{ name: "id", required: true, value: { kind: "string", constraints: [] } },
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
] as const satisfies readonly LawValueMember[];
|
|
208
|
+
|
|
209
|
+
function internalPayloadContract(
|
|
210
|
+
tenantContract: LawValueType,
|
|
211
|
+
capabilities: ReadonlySet<"owned-authoring">,
|
|
212
|
+
): LawValueType {
|
|
213
|
+
if (tenantContract.kind !== "record") return tenantContract;
|
|
214
|
+
if (!capabilities.has("owned-authoring")) return tenantContract;
|
|
215
|
+
return {
|
|
216
|
+
kind: "record",
|
|
217
|
+
fields: [...tenantContract.fields, ...OWNED_AUTHORING_PAYLOAD_MEMBERS],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
179
221
|
export interface LawReadParameter {
|
|
180
222
|
readonly name: string;
|
|
181
223
|
readonly field?: LawFieldPathRef;
|
|
@@ -1112,7 +1154,7 @@ export function discoverLawGraph(
|
|
|
1112
1154
|
const intentNodes: LawIntentNode[] = intents.map((intent) => {
|
|
1113
1155
|
const intentEntity = intentEntities.get(intent.id)!;
|
|
1114
1156
|
const businessBirth = (intent as {
|
|
1115
|
-
businessBirth?: { publicPayloadSchema?: z.ZodTypeAny };
|
|
1157
|
+
businessBirth?: { publicPayloadSchema?: z.ZodTypeAny; owned?: boolean };
|
|
1116
1158
|
}).businessBirth;
|
|
1117
1159
|
const payloadContractEntity = ownedEntity(
|
|
1118
1160
|
"contract",
|
|
@@ -1122,21 +1164,32 @@ export function discoverLawGraph(
|
|
|
1122
1164
|
`${intent.id}#accepts`,
|
|
1123
1165
|
options.source?.("intent", intent.id, module.name),
|
|
1124
1166
|
);
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1167
|
+
// A business birth has two deliberately different typed boundaries:
|
|
1168
|
+
//
|
|
1169
|
+
// * `contract.accepts` / `businessBirth.publicPayloadSchema` is the tenant-facing input;
|
|
1170
|
+
// * `payloadSchema` is the executable call contract after Nomos has added its private
|
|
1171
|
+
// principal, timestamp and (when needed) sealed-genesis facts.
|
|
1172
|
+
//
|
|
1173
|
+
// Collapsing both to `contract.accepts` made the generated client author the correct private
|
|
1174
|
+
// facts into a USDA call whose `nomos:accepts` contract did not declare them. Keep ordinary
|
|
1175
|
+
// compound intents on their public contract because they have no framework-only birth carrier;
|
|
1176
|
+
// only a declared business birth owns this second typed boundary.
|
|
1177
|
+
const tenantPayloadContract = lowerLawValueType(
|
|
1178
|
+
isBusinessIntent(intent) && businessBirth === undefined
|
|
1179
|
+
? intent.contract.accepts
|
|
1180
|
+
: intent.payloadSchema,
|
|
1128
1181
|
);
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
const reserved = [
|
|
1182
|
+
if (tenantPayloadContract.kind !== "record") {
|
|
1183
|
+
fail(`directive '${intent.id}': payload must lower to a record`);
|
|
1184
|
+
}
|
|
1185
|
+
for (const field of tenantPayloadContract.fields) {
|
|
1186
|
+
const reserved = new Set<string>([
|
|
1134
1187
|
OWNED_KEY_PAYLOAD_FIELD,
|
|
1135
1188
|
OWNED_CUSTODY_PAYLOAD_FIELD,
|
|
1136
1189
|
OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
|
|
1137
1190
|
BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
|
|
1138
|
-
];
|
|
1139
|
-
if (!reserved.
|
|
1191
|
+
]);
|
|
1192
|
+
if (!reserved.has(field.name)) continue;
|
|
1140
1193
|
const frameworkBirthCarrier = field.name === BUSINESS_BIRTH_SEED_PAYLOAD_FIELD && businessBirth !== undefined;
|
|
1141
1194
|
if (frameworkBirthCarrier) continue;
|
|
1142
1195
|
fail(
|
|
@@ -1145,6 +1198,23 @@ export function discoverLawGraph(
|
|
|
1145
1198
|
`a different name.`,
|
|
1146
1199
|
);
|
|
1147
1200
|
}
|
|
1201
|
+
const affectedAggregateIds = [
|
|
1202
|
+
intent.aggregateId,
|
|
1203
|
+
...(intent.secondaryOutputs ?? []).map((output) => output.aggregateId),
|
|
1204
|
+
];
|
|
1205
|
+
const frameworkCapabilities = new Set<"owned-authoring">();
|
|
1206
|
+
if (
|
|
1207
|
+
businessBirth?.owned === true ||
|
|
1208
|
+
affectedAggregateIds.some((aggregateId) =>
|
|
1209
|
+
aggregateByLabel.get(aggregateId)?.node.ownerField !== undefined)
|
|
1210
|
+
) {
|
|
1211
|
+
frameworkCapabilities.add("owned-authoring");
|
|
1212
|
+
}
|
|
1213
|
+
const payloadShape = lowerAnonymousContractFields(
|
|
1214
|
+
internalPayloadContract(tenantPayloadContract, frameworkCapabilities),
|
|
1215
|
+
payloadContractEntity,
|
|
1216
|
+
);
|
|
1217
|
+
for (const node of payloadShape.nodes) anonymousValueNodes.set(node.entity.id, node);
|
|
1148
1218
|
payloadContractNodes.push({
|
|
1149
1219
|
kind: "payload-contract",
|
|
1150
1220
|
entity: payloadContractEntity,
|
package/src/relation_schema.ts
CHANGED
|
@@ -108,6 +108,24 @@ export interface RelationTypeDecl {
|
|
|
108
108
|
readonly __isRelationType: true;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* A relation name that has deliberately crossed the authoring boundary.
|
|
113
|
+
*
|
|
114
|
+
* Plain strings remain accepted by the low-level DSL while existing tenant law migrates, but framework
|
|
115
|
+
* vocabulary should be coined once with {@link relationRef} and then reused by relation declarations,
|
|
116
|
+
* gates and tuple writes. That makes a rename a compiler-guided change instead of three unrelated string
|
|
117
|
+
* edits which can produce valid-looking, permanently unauthorized law.
|
|
118
|
+
*/
|
|
119
|
+
declare const NOMOS_RELATION_REF: unique symbol;
|
|
120
|
+
export type RelationRef<Name extends string = string> = Name & {
|
|
121
|
+
readonly [NOMOS_RELATION_REF]: "NomosRelationRef";
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Create one typed relation handle after validating the wire token once. */
|
|
125
|
+
export function relationRef<const Name extends string>(name: Name): RelationRef<Name> {
|
|
126
|
+
return requireName(name, "relation") as RelationRef<Name>;
|
|
127
|
+
}
|
|
128
|
+
|
|
111
129
|
export function isRelationTypeDecl(v: unknown): v is RelationTypeDecl {
|
|
112
130
|
return typeof v === "object" && v !== null && (v as { __isRelationType?: unknown }).__isRelationType === true;
|
|
113
131
|
}
|
package/src/shape_compat.ts
CHANGED
|
@@ -130,6 +130,9 @@ function payloadFieldKind(spec: PayloadFieldSpec): string {
|
|
|
130
130
|
case "ref":
|
|
131
131
|
// NomosRef<T> is aggregate-typed in the client and remains a string id on the wire.
|
|
132
132
|
return "string";
|
|
133
|
+
case "addressRef":
|
|
134
|
+
// A workspace-qualified NomosRef<T> preserves its complete address object.
|
|
135
|
+
return "json";
|
|
133
136
|
case "recipient":
|
|
134
137
|
// Recipient<User> is nominal in clients and an opaque delivery address on the law wire.
|
|
135
138
|
return "string";
|
|
@@ -254,7 +254,13 @@ function safeIdentityProjection(sourceShape: unknown, targetShape: unknown): boo
|
|
|
254
254
|
const target = targetShape as Record<string, unknown>;
|
|
255
255
|
const physical = (shape: Record<string, unknown>) => {
|
|
256
256
|
const { kind, mapValueKind, jsonShape, encrypted } = shape;
|
|
257
|
-
|
|
257
|
+
// `t.ref` and `t.foreignRef` store the target aggregate id as the same Str
|
|
258
|
+
// value as `t.string`; `ref` is a semantic/codegen contract, not a distinct
|
|
259
|
+
// kernel encoding. The JSON-schema subset check below still prevents an
|
|
260
|
+
// atomic NomosRef object (or any other non-string reference contract) from
|
|
261
|
+
// being mistaken for this identity migration.
|
|
262
|
+
const encodingKind = kind === "ref" ? "string" : kind;
|
|
263
|
+
return { kind: encodingKind, mapValueKind, jsonShape, encrypted };
|
|
258
264
|
};
|
|
259
265
|
return canonicalJson(physical(source)) === canonicalJson(physical(target))
|
|
260
266
|
&& jsonSchemaSubset(source.jsonSchema, target.jsonSchema);
|
package/src/value_object.ts
CHANGED
|
@@ -321,6 +321,46 @@ const nomosAddressSchema = z.object({
|
|
|
321
321
|
id: z.string().min(1),
|
|
322
322
|
});
|
|
323
323
|
|
|
324
|
+
/**
|
|
325
|
+
* THE WORKSPACE-QUALIFIED REFERENCE BRAND — `refOf(CatalogueWorkspace)` is
|
|
326
|
+
* wire-identical to Nomos' typed `{ workspace, type, id }` address, while
|
|
327
|
+
* generated clients expose it as `NomosRef<CatalogueWorkspace>`. Unlike
|
|
328
|
+
* `idOf(...)`, this deliberately preserves the workspace because it is the
|
|
329
|
+
* canonical payload shape for cross-workspace navigation and custody effects.
|
|
330
|
+
*/
|
|
331
|
+
export const NOMOS_REFOF_BRAND: unique symbol = Symbol.for("nomos.refOf");
|
|
332
|
+
|
|
333
|
+
export interface RefOfMeta {
|
|
334
|
+
readonly aggregateType: string;
|
|
335
|
+
readonly targetKind: "aggregate" | "contract";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function refOf(target: RefTarget): z.ZodType<z.infer<typeof nomosAddressSchema>> {
|
|
339
|
+
const resolved = refTargetMeta(target, "refOf");
|
|
340
|
+
const schema = nomosAddressSchema.strict();
|
|
341
|
+
Object.defineProperty(schema, NOMOS_REFOF_BRAND, {
|
|
342
|
+
value: {
|
|
343
|
+
aggregateType: resolved.type,
|
|
344
|
+
targetKind: resolved.kind,
|
|
345
|
+
} satisfies RefOfMeta,
|
|
346
|
+
enumerable: false,
|
|
347
|
+
});
|
|
348
|
+
return schema;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Resolve `refOf(...)` metadata through omission/null wrappers. */
|
|
352
|
+
export function refOfMetaOf(zt: z.ZodTypeAny): RefOfMeta | undefined {
|
|
353
|
+
let cur: z.ZodTypeAny | undefined = zt;
|
|
354
|
+
for (let hops = 0; cur !== undefined && hops < 16; hops++) {
|
|
355
|
+
const branded = (cur as unknown as Record<symbol, unknown>)[NOMOS_REFOF_BRAND];
|
|
356
|
+
if (typeof branded === "object" && branded !== null) return branded as RefOfMeta;
|
|
357
|
+
const kind = zodKind(cur);
|
|
358
|
+
if (kind !== "optional" && kind !== "default" && kind !== "nullable") return undefined;
|
|
359
|
+
cur = zodDef(cur).innerType;
|
|
360
|
+
}
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
|
|
324
364
|
/**
|
|
325
365
|
* A payload field holding an immutable publication and its stable current-version
|
|
326
366
|
* series. The kernel receives `{pinned, series}`; `current` is compiled law and
|