@githolon/dsl 0.108.1 → 0.108.3
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
package/src/codegen_ts.ts
CHANGED
|
@@ -1127,6 +1127,9 @@ export interface TsClientOptions {
|
|
|
1127
1127
|
/** Emit the structural holon factory for the compiler-owned headless artifact. The ordinary tenant
|
|
1128
1128
|
* client leaves this false so framework transport types never enter its public declaration graph. */
|
|
1129
1129
|
readonly exportHolonFactory?: boolean;
|
|
1130
|
+
/** Emit only the fixed framework operator call set (identity, lifecycle and birth). This is the
|
|
1131
|
+
* small server/operator companion: exact generated USDA calls, no application/read runtime. */
|
|
1132
|
+
readonly frameworkOperatorOnly?: boolean;
|
|
1130
1133
|
/** Exact law identities and paths from the same in-memory USDA render which produced the shipped
|
|
1131
1134
|
* closure. This is compiler input for generated call encoders, never a checked-in sidecar. */
|
|
1132
1135
|
readonly callUsda?: readonly {
|
|
@@ -2136,6 +2139,158 @@ export function generateTsClient(
|
|
|
2136
2139
|
}
|
|
2137
2140
|
}
|
|
2138
2141
|
}
|
|
2142
|
+
if (opts.frameworkOperatorOnly === true) {
|
|
2143
|
+
const fixedGroups = new Set(["identity", "governance", "lifecycle", "birth"]);
|
|
2144
|
+
const operatorDefinitions = frameworkCallMethodDefinitions.filter(
|
|
2145
|
+
(definition) => fixedGroups.has(definition.group)
|
|
2146
|
+
);
|
|
2147
|
+
const operatorEncoders = frameworkCallEncoders.filter((entry) =>
|
|
2148
|
+
fixedGroups.has(entry.group)
|
|
2149
|
+
);
|
|
2150
|
+
const capabilityReceiptByKey = new Map<string, {
|
|
2151
|
+
readonly domain: string;
|
|
2152
|
+
readonly directiveId: string;
|
|
2153
|
+
readonly payloadType: string;
|
|
2154
|
+
readonly encoder: ReturnType<typeof emitTsCallUsdaEncoder>;
|
|
2155
|
+
}>();
|
|
2156
|
+
const capabilityReferenceTypes = new Set<string>();
|
|
2157
|
+
for (const mod of modules) {
|
|
2158
|
+
const domain = mod.domain ?? mod.name;
|
|
2159
|
+
const law = callUsdaByDomain.get(domain);
|
|
2160
|
+
if (law === undefined) continue;
|
|
2161
|
+
const authored = authoredDirectivesByDomain.get(domain) ?? [];
|
|
2162
|
+
for (const capability of mod.impureCapabilities ?? []) {
|
|
2163
|
+
for (const receipt of [
|
|
2164
|
+
capability.complete,
|
|
2165
|
+
capability.fail,
|
|
2166
|
+
capability.block,
|
|
2167
|
+
capability.deadLetter,
|
|
2168
|
+
]) {
|
|
2169
|
+
const directive = authored.find((candidate) => candidate.id === receipt.id);
|
|
2170
|
+
if (directive === undefined) {
|
|
2171
|
+
throw new Error(
|
|
2172
|
+
`TypeScript capability receipt codegen: '${domain}/${receipt.id}' has no authored directive`,
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
const payloadType = tsTypeOfPayload(
|
|
2176
|
+
directive.payloadSchema as z.ZodTypeAny,
|
|
2177
|
+
[],
|
|
2178
|
+
);
|
|
2179
|
+
for (const referenceType of payloadReferenceTypes(
|
|
2180
|
+
directive.payloadSchema as z.ZodTypeAny,
|
|
2181
|
+
)) {
|
|
2182
|
+
capabilityReferenceTypes.add(referenceType);
|
|
2183
|
+
}
|
|
2184
|
+
capabilityReceiptByKey.set(`${domain}\u0000${receipt.id}`, {
|
|
2185
|
+
domain,
|
|
2186
|
+
directiveId: receipt.id,
|
|
2187
|
+
payloadType,
|
|
2188
|
+
encoder: emitTsCallUsdaEncoder(
|
|
2189
|
+
law.graph,
|
|
2190
|
+
law.paths,
|
|
2191
|
+
domain,
|
|
2192
|
+
receipt.id,
|
|
2193
|
+
payloadType,
|
|
2194
|
+
),
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
const capabilityReceipts = [...capabilityReceiptByKey.values()].sort((left, right) =>
|
|
2200
|
+
left.domain.localeCompare(right.domain) || left.directiveId.localeCompare(right.directiveId)
|
|
2201
|
+
);
|
|
2202
|
+
return [
|
|
2203
|
+
`// AUTO-GENERATED by nomos-compile — exact framework operators for package "${opts.packageName}".`,
|
|
2204
|
+
`// No application runtime and no authority live here. Do not edit.`,
|
|
2205
|
+
``,
|
|
2206
|
+
...TS_CALL_USDA_PRELUDE,
|
|
2207
|
+
`/** A SHA-256 digest in canonical Nomos wire form. */`,
|
|
2208
|
+
`export type Sha256Digest = string & { readonly __nomosSha256Digest: unique symbol };`,
|
|
2209
|
+
`/** A Git object address in canonical gix custody form. */`,
|
|
2210
|
+
`export type GitObjectId = string & { readonly __nomosGitObjectId: unique symbol };`,
|
|
2211
|
+
`/** Exact compiler-authored framework call. Bytes are internal transport, while callers retain the nominal method and payload type. */`,
|
|
2212
|
+
`export interface NomosFrameworkCall<Domain extends string = string, Directive extends string = string> {`,
|
|
2213
|
+
` readonly domain: Domain;`,
|
|
2214
|
+
` readonly directive: Directive;`,
|
|
2215
|
+
` readonly canonicalUsda: Uint8Array;`,
|
|
2216
|
+
` readonly routingCandidates: readonly string[];`,
|
|
2217
|
+
`}`,
|
|
2218
|
+
`declare const _nomosAggregateType: unique symbol;`,
|
|
2219
|
+
...[...capabilityReferenceTypes].sort().map(
|
|
2220
|
+
(id) => `export type ${id} = { readonly [_nomosAggregateType]?: ${JSON.stringify(id)} };`,
|
|
2221
|
+
),
|
|
2222
|
+
`export interface NomosRef<TAggregate> {`,
|
|
2223
|
+
` readonly [_nomosAggregateType]: (aggregate: TAggregate) => TAggregate;`,
|
|
2224
|
+
` readonly workspace: string;`,
|
|
2225
|
+
` readonly type: string;`,
|
|
2226
|
+
` readonly id: string;`,
|
|
2227
|
+
` readonly address: string;`,
|
|
2228
|
+
`}`,
|
|
2229
|
+
`/** Construct a typed address after the operator has independently established that the referenced task exists. A reference is not authority. */`,
|
|
2230
|
+
`export const nomosRef = <TAggregate>(workspace: string, type: string, id: string): NomosRef<TAggregate> => ({`,
|
|
2231
|
+
` workspace, type, id,`,
|
|
2232
|
+
` address: "nomos://" + encodeURIComponent(workspace) + "/" + encodeURIComponent(type) + "/" + encodeURIComponent(id),`,
|
|
2233
|
+
`} as unknown as NomosRef<TAggregate>);`,
|
|
2234
|
+
`const _nmsReferenceTypeMatches = (actual: string, expected: string): boolean =>`,
|
|
2235
|
+
` expected === "" || actual === expected || actual.endsWith("." + expected);`,
|
|
2236
|
+
`const _nmsReferenceId = (value: unknown, expectedType: string, workspace: string, field: string): string => {`,
|
|
2237
|
+
` if (typeof value !== "object" || value === null) throw new TypeError(field + " must be a NomosRef");`,
|
|
2238
|
+
` const ref = value as { workspace?: unknown; type?: unknown; id?: unknown };`,
|
|
2239
|
+
` if (typeof ref.id !== "string" || ref.id.length === 0 || typeof ref.type !== "string" || ref.type.length === 0 || typeof ref.workspace !== "string" || ref.workspace.length === 0) throw new TypeError(field + " must carry a complete NomosRef { workspace, type, id }");`,
|
|
2240
|
+
` if (!_nmsReferenceTypeMatches(ref.type, expectedType)) throw new TypeError(field + " refers to " + ref.type + "; expected " + expectedType);`,
|
|
2241
|
+
` if (workspace !== "" && ref.workspace !== workspace) throw new TypeError(field + " belongs to workspace " + ref.workspace + "; this call is bound to " + workspace);`,
|
|
2242
|
+
` return ref.id;`,
|
|
2243
|
+
`};`,
|
|
2244
|
+
...operatorEncoders.flatMap((entry) => entry.encoder.source),
|
|
2245
|
+
...capabilityReceipts.flatMap((entry) => entry.encoder.source),
|
|
2246
|
+
`export const nomosFrameworkCalls = {`,
|
|
2247
|
+
...[...new Set(operatorDefinitions.map((entry) => entry.group))].flatMap(
|
|
2248
|
+
(group) => [
|
|
2249
|
+
` ${group}: {`,
|
|
2250
|
+
...operatorDefinitions
|
|
2251
|
+
.filter((entry) => entry.group === group)
|
|
2252
|
+
.flatMap((definition) => {
|
|
2253
|
+
const alternatives = operatorEncoders.filter(
|
|
2254
|
+
(entry) => entry.group === group && entry.kind === definition.kind
|
|
2255
|
+
);
|
|
2256
|
+
const domainType = alternatives
|
|
2257
|
+
.map((entry) => JSON.stringify(entry.domain))
|
|
2258
|
+
.join(" | ") || "never";
|
|
2259
|
+
return [
|
|
2260
|
+
` ${definition.kind}(input: { readonly domain: ${domainType}; readonly workspace: string } & ${definition.payloadType}): NomosFrameworkCall<${domainType}, ${JSON.stringify(definition.directiveId)}> {`,
|
|
2261
|
+
` const { domain, workspace, ...payload } = input;`,
|
|
2262
|
+
` switch (domain) {`,
|
|
2263
|
+
...alternatives
|
|
2264
|
+
.map(
|
|
2265
|
+
(entry) =>
|
|
2266
|
+
` case ${JSON.stringify(entry.domain)}: return ${entry.encoder.functionName}(payload, workspace);`
|
|
2267
|
+
),
|
|
2268
|
+
` default: throw new Error("The compiled application has no '${definition.kind}' call in domain '" + domain + "'.");`,
|
|
2269
|
+
` }`,
|
|
2270
|
+
` },`,
|
|
2271
|
+
];
|
|
2272
|
+
}),
|
|
2273
|
+
` },`,
|
|
2274
|
+
]
|
|
2275
|
+
),
|
|
2276
|
+
`} as const;`,
|
|
2277
|
+
`export const nomosCapabilityReceipts = {`,
|
|
2278
|
+
...[...new Set(capabilityReceipts.map((entry) => entry.domain))].flatMap((domain) => [
|
|
2279
|
+
` ${tsKey(domain)}: {`,
|
|
2280
|
+
...capabilityReceipts
|
|
2281
|
+
.filter((entry) => entry.domain === domain)
|
|
2282
|
+
.flatMap((entry) => [
|
|
2283
|
+
` ${tsKey(entry.directiveId)}(input: { readonly workspace: string } & ${entry.payloadType}): NomosFrameworkCall<${JSON.stringify(domain)}, ${JSON.stringify(entry.directiveId)}> {`,
|
|
2284
|
+
` const { workspace, ...payload } = input;`,
|
|
2285
|
+
` return ${entry.encoder.functionName}(payload, workspace);`,
|
|
2286
|
+
` },`,
|
|
2287
|
+
]),
|
|
2288
|
+
` },`,
|
|
2289
|
+
]),
|
|
2290
|
+
`} as const;`,
|
|
2291
|
+
``,
|
|
2292
|
+
].join("\n");
|
|
2293
|
+
}
|
|
2139
2294
|
out.push(
|
|
2140
2295
|
`// AUTO-GENERATED by nomos-compile — typed TS application implementation for package "${opts.packageName}".`,
|
|
2141
2296
|
`// Regenerated on every compile; do not edit. Framework binding types below are deliberately private:`,
|
|
@@ -5646,6 +5646,9 @@ async function main(): Promise<void> {
|
|
|
5646
5646
|
const headlessClientDeclarationPath = path.join(outDir, `${cfg.name}.headless.d.mts`);
|
|
5647
5647
|
const operatorPath = path.join(outDir, `${cfg.name}.operator.mjs`);
|
|
5648
5648
|
const operatorDeclarationPath = path.join(outDir, `${cfg.name}.operator.d.mts`);
|
|
5649
|
+
const frameworkOperatorPath = path.join(outDir, `${cfg.name}.framework.mjs`);
|
|
5650
|
+
const frameworkOperatorDeclarationPath = path.join(outDir, `${cfg.name}.framework.d.mts`);
|
|
5651
|
+
const frameworkOperatorSourcePath = path.join(outDir, `${cfg.name}.framework.ts`);
|
|
5649
5652
|
const clientOptions = {
|
|
5650
5653
|
packageName: cfg.name,
|
|
5651
5654
|
domainHash,
|
|
@@ -5665,6 +5668,10 @@ async function main(): Promise<void> {
|
|
|
5665
5668
|
exportHolonFactory: true,
|
|
5666
5669
|
});
|
|
5667
5670
|
const operatorSource = generateTsLifecycleOperator(clientOptions.callUsda);
|
|
5671
|
+
const frameworkOperatorSource = generateTsClient(presentationDomainModules, {
|
|
5672
|
+
...clientOptions,
|
|
5673
|
+
frameworkOperatorOnly: true,
|
|
5674
|
+
});
|
|
5668
5675
|
const runtimePath = path.join(DSL_DIR, "runtime", "generated-application-host.js");
|
|
5669
5676
|
if (!existsSync(runtimePath)) {
|
|
5670
5677
|
fail("the compiler-private generated application runtime is missing — reinstall @githolon/dsl");
|
|
@@ -6050,6 +6057,43 @@ async function main(): Promise<void> {
|
|
|
6050
6057
|
rmSync(operatorPath, { force: true });
|
|
6051
6058
|
rmSync(operatorDeclarationPath, { force: true });
|
|
6052
6059
|
}
|
|
6060
|
+
const frameworkOperatorDeclaration = ts.transpileDeclaration(frameworkOperatorSource, {
|
|
6061
|
+
fileName: `${cfg.name}.framework.ts`,
|
|
6062
|
+
compilerOptions: {
|
|
6063
|
+
target: ts.ScriptTarget.ES2022,
|
|
6064
|
+
module: ts.ModuleKind.ESNext,
|
|
6065
|
+
strict: true,
|
|
6066
|
+
},
|
|
6067
|
+
reportDiagnostics: true,
|
|
6068
|
+
});
|
|
6069
|
+
const frameworkOperatorErrors = (frameworkOperatorDeclaration.diagnostics ?? []).filter(
|
|
6070
|
+
(diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error,
|
|
6071
|
+
);
|
|
6072
|
+
if (frameworkOperatorErrors.length > 0) {
|
|
6073
|
+
fail(`generated TypeScript framework operator declarations are invalid — this is a Nomos codegen bug:\n${frameworkOperatorErrors.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")).join("\n")}`);
|
|
6074
|
+
}
|
|
6075
|
+
writeFileSync(frameworkOperatorDeclarationPath, frameworkOperatorDeclaration.outputText, "utf8");
|
|
6076
|
+
writeFileSync(frameworkOperatorSourcePath, frameworkOperatorSource, "utf8");
|
|
6077
|
+
await esbuildMod.build({
|
|
6078
|
+
stdin: {
|
|
6079
|
+
contents: frameworkOperatorSource,
|
|
6080
|
+
loader: "ts",
|
|
6081
|
+
resolveDir: outDir,
|
|
6082
|
+
sourcefile: `${cfg.name}.framework.ts`,
|
|
6083
|
+
},
|
|
6084
|
+
outfile: frameworkOperatorPath,
|
|
6085
|
+
bundle: true,
|
|
6086
|
+
platform: "neutral",
|
|
6087
|
+
format: "esm",
|
|
6088
|
+
target: "es2022",
|
|
6089
|
+
legalComments: "none",
|
|
6090
|
+
logLevel: "silent",
|
|
6091
|
+
});
|
|
6092
|
+
writeFileSync(
|
|
6093
|
+
frameworkOperatorPath,
|
|
6094
|
+
canonicalizeGeneratedClientJavascript(readFileSync(frameworkOperatorPath, "utf8")),
|
|
6095
|
+
"utf8",
|
|
6096
|
+
);
|
|
6053
6097
|
} catch (e) {
|
|
6054
6098
|
fail(`generated TypeScript application could not be sealed — this is a Nomos codegen bug: ${(e as Error).message}`);
|
|
6055
6099
|
} finally {
|
|
@@ -211,10 +211,20 @@ export const RelationshipSubjectValue = valueObject("RelationshipSubject", {
|
|
|
211
211
|
ownedCustody: OwnedCustodyValue.optional(),
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
// Historical RelationTuple law declared this map value as `t.json()`, but the
|
|
215
|
+
// kernel has always admitted and stored only the nominal RelationshipSubject
|
|
216
|
+
// palette for this framework field. Keep that exact prior declaration as the
|
|
217
|
+
// source of the explicit evolution edge: this is a type-disclosure refinement
|
|
218
|
+
// of existing structured custody, not a JSON parser or a business-data cast.
|
|
219
|
+
const LegacyRelationshipSubjects = t.map(t.json()).merge(MapOf(Lww));
|
|
220
|
+
|
|
214
221
|
export const RelationTuple = aggregate("RelationTuple", {
|
|
215
222
|
object: t.string().merge(Lww),
|
|
216
223
|
relation: t.string().merge(Lww),
|
|
217
|
-
subjects: t
|
|
224
|
+
subjects: t
|
|
225
|
+
.map(RelationshipSubjectValue.field())
|
|
226
|
+
.merge(MapOf(Lww))
|
|
227
|
+
.evolvesFrom(LegacyRelationshipSubjects),
|
|
218
228
|
}).public();
|
|
219
229
|
export type RelationTupleHandle = typeof RelationTuple;
|
|
220
230
|
|
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
seedReceivingGroupPolicy,
|
|
19
19
|
} from "./framework/home.js";
|
|
20
20
|
import { enrollDelegation, enrollFirstDevice, enrollSigner } from "./framework/signing_identity.js";
|
|
21
|
+
import { grantAdmin } from "./framework/workspaces.js";
|
|
21
22
|
|
|
22
23
|
export interface FixedFrameworkCallDefinition {
|
|
23
|
-
readonly group: "identity" | "lifecycle" | "birth";
|
|
24
|
+
readonly group: "identity" | "governance" | "lifecycle" | "birth";
|
|
24
25
|
readonly kind: string;
|
|
25
26
|
readonly directive: Directive<any>;
|
|
26
27
|
/** Needed by the pre-bind Dart birth ceremony before an application exists. */
|
|
@@ -40,6 +41,7 @@ export const fixedFrameworkCallDefinitions: readonly FixedFrameworkCallDefinitio
|
|
|
40
41
|
{ group: "identity", kind: "enrollSigner", directive: enrollSigner, dartBirth: true, requiredApplicationHome: true },
|
|
41
42
|
{ group: "identity", kind: "setAuthProvider", directive: setAuthProvider, requiredApplicationHome: true },
|
|
42
43
|
{ group: "identity", kind: "birthReceivingGroup", directive: receivingGroupBirth, dartBirth: true, requiredApplicationHome: true },
|
|
44
|
+
{ group: "governance", kind: "grantAdmin", directive: grantAdmin },
|
|
43
45
|
{ group: "lifecycle", kind: "installDomain", directive: installDomain },
|
|
44
46
|
{ group: "lifecycle", kind: "stageDomain", directive: stageDomain },
|
|
45
47
|
{ group: "lifecycle", kind: "promoteDomain", directive: promoteDomain },
|
|
@@ -50,7 +50,16 @@ export function stableIdsForModule(mod: DomainModule): StableIds {
|
|
|
50
50
|
const evolutions =
|
|
51
51
|
handle === undefined || schema === undefined || driver === undefined || details === undefined
|
|
52
52
|
? []
|
|
53
|
-
: fieldEvolutionRules(
|
|
53
|
+
: fieldEvolutionRules(
|
|
54
|
+
handle,
|
|
55
|
+
pf,
|
|
56
|
+
aggSid,
|
|
57
|
+
pf?.sid ?? mintFieldSid(aggSid, fieldName),
|
|
58
|
+
details,
|
|
59
|
+
driver,
|
|
60
|
+
schema,
|
|
61
|
+
{ aggregateId: agg.id, fieldName },
|
|
62
|
+
);
|
|
54
63
|
fields[fieldName] = {
|
|
55
64
|
sid: pf?.sid ?? mintFieldSid(aggSid, fieldName),
|
|
56
65
|
...(pf?.was !== undefined && pf.was.length > 0 ? { was: pf.was } : {}),
|
|
@@ -147,6 +156,7 @@ function fieldEvolutionRules(
|
|
|
147
156
|
target: { contract: string; shape: unknown },
|
|
148
157
|
driver: unknown,
|
|
149
158
|
targetSchema: string,
|
|
159
|
+
identity: { aggregateId: string; fieldName: string },
|
|
150
160
|
): StableFieldEvolution[] {
|
|
151
161
|
const bySource = new Map<string, StableFieldEvolution>();
|
|
152
162
|
const add = (rule: StableFieldEvolution, origin: string) => {
|
|
@@ -172,7 +182,10 @@ function fieldEvolutionRules(
|
|
|
172
182
|
source.contract,
|
|
173
183
|
encodeDriver(declaration.source.driver),
|
|
174
184
|
);
|
|
175
|
-
if (
|
|
185
|
+
if (
|
|
186
|
+
declaration.mode === "identity" &&
|
|
187
|
+
!safeIdentityProjection(source.shape, target.shape, identity)
|
|
188
|
+
) {
|
|
176
189
|
throw new Error(
|
|
177
190
|
`.evolvesFrom(source) is not a safe identity widening: some source values are not valid ` +
|
|
178
191
|
`under the target field. Supply a finite business transform as the second argument.`,
|
|
@@ -247,12 +260,30 @@ function fieldEvolutionRules(
|
|
|
247
260
|
|
|
248
261
|
/** Explicit identity projection: the tenant declares the business meaning is continuous;
|
|
249
262
|
* the compiler still proves every old value is accepted and its physical decoding is unchanged. */
|
|
250
|
-
function safeIdentityProjection(
|
|
263
|
+
function safeIdentityProjection(
|
|
264
|
+
sourceShape: unknown,
|
|
265
|
+
targetShape: unknown,
|
|
266
|
+
identity?: { aggregateId: string; fieldName: string },
|
|
267
|
+
): boolean {
|
|
251
268
|
if (sourceShape === null || targetShape === null || typeof sourceShape !== "object" || typeof targetShape !== "object") {
|
|
252
269
|
return canonicalJson(sourceShape) === canonicalJson(targetShape);
|
|
253
270
|
}
|
|
254
271
|
const source = sourceShape as Record<string, unknown>;
|
|
255
272
|
const target = targetShape as Record<string, unknown>;
|
|
273
|
+
// RelationTuple.subjects is a kernel-native framework palette, not a tenant
|
|
274
|
+
// JSON field. Historical DSL metadata called its map leaf `json`, while the
|
|
275
|
+
// kernel already admitted/stored only RelationshipSubject values. The typed
|
|
276
|
+
// value-object declaration therefore discloses the existing contract without
|
|
277
|
+
// changing physical decoding or widening what old writers can author. Keep
|
|
278
|
+
// this proof bound to the one kernel-special field; arbitrary JSON->VO
|
|
279
|
+
// narrowing remains refused below.
|
|
280
|
+
if (
|
|
281
|
+
identity?.aggregateId === "RelationTuple" &&
|
|
282
|
+
identity.fieldName === "subjects" &&
|
|
283
|
+
isRelationshipSubjectDisclosure(source, target)
|
|
284
|
+
) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
256
287
|
const physical = (shape: Record<string, unknown>) => {
|
|
257
288
|
const { kind, mapValueKind, jsonShape, encrypted } = shape;
|
|
258
289
|
// `t.ref` and `t.foreignRef` store the target aggregate id as the same Str
|
|
@@ -267,6 +298,21 @@ function safeIdentityProjection(sourceShape: unknown, targetShape: unknown): boo
|
|
|
267
298
|
&& jsonSchemaSubset(source.jsonSchema, target.jsonSchema);
|
|
268
299
|
}
|
|
269
300
|
|
|
301
|
+
function isRelationshipSubjectDisclosure(
|
|
302
|
+
source: Record<string, unknown>,
|
|
303
|
+
target: Record<string, unknown>,
|
|
304
|
+
): boolean {
|
|
305
|
+
const targetVo = target.mapValueVo as
|
|
306
|
+
| { descriptor?: { name?: unknown } }
|
|
307
|
+
| undefined;
|
|
308
|
+
return source.kind === "map" &&
|
|
309
|
+
target.kind === "map" &&
|
|
310
|
+
source.mapValueKind === "json" &&
|
|
311
|
+
target.mapValueKind === "json" &&
|
|
312
|
+
source.mapValueVo === undefined &&
|
|
313
|
+
targetVo?.descriptor?.name === "RelationshipSubject";
|
|
314
|
+
}
|
|
315
|
+
|
|
270
316
|
/** Automatic widening is stricter: every non-schema business descriptor must remain
|
|
271
317
|
* identical. Brands, units, references and null meaning require an explicit declaration. */
|
|
272
318
|
function safeAutomaticWidening(sourceShape: unknown, targetShape: unknown): boolean {
|
|
@@ -310,7 +356,21 @@ function jsonSchemaSubset(source: unknown, target: unknown): boolean {
|
|
|
310
356
|
return false;
|
|
311
357
|
}
|
|
312
358
|
}
|
|
313
|
-
|
|
359
|
+
const sourceAdditional = s.additionalProperties;
|
|
360
|
+
const targetAdditional = t.additionalProperties;
|
|
361
|
+
if (sourceAdditional !== false) {
|
|
362
|
+
if (targetAdditional === false) return false;
|
|
363
|
+
if (targetAdditional !== undefined && targetAdditional !== true) {
|
|
364
|
+
if (
|
|
365
|
+
sourceAdditional === undefined ||
|
|
366
|
+
sourceAdditional === true ||
|
|
367
|
+
typeof sourceAdditional !== "object" ||
|
|
368
|
+
!jsonSchemaSubset(sourceAdditional, targetAdditional)
|
|
369
|
+
) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
314
374
|
}
|
|
315
375
|
if (sourceTypes.has("array") && s.items !== undefined && t.items !== undefined) {
|
|
316
376
|
if (!jsonSchemaSubset(s.items, t.items)) return false;
|