@githolon/dsl 0.101.6 → 0.102.0
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 +13 -0
- package/dart/lib/nomos_client.dart +3 -52
- package/dart/lib/src/dev_cloud.dart +5 -0
- package/dart/lib/src/home.dart +10 -15
- package/dart/lib/src/nms_conformance_runtime.dart +12 -0
- package/dart/lib/src/nms_generated_runtime.dart +29 -0
- package/dart/lib/src/nms_test_environment.dart +8 -0
- package/dart/lib/src/node_runner_transport.dart +56 -18
- package/dart/lib/src/provider.dart +5 -1
- package/dart/lib/src/record_origin.dart +55 -0
- package/dart/lib/src/runner/node-entry.mjs +9105 -0
- package/dart/lib/src/runner/offline-install.mjs +4729 -0
- package/dart/lib/src/session_bound.dart +23 -10
- package/dart/lib/src/source_trust.dart +51 -0
- package/dart/lib/src/test_harness.dart +490 -58
- package/dart/lib/src/webview_bridge.dart +167 -58
- package/dart/lib/src/workspace_name.dart +7 -1
- package/dart/pubspec.lock +3 -3
- package/dart/pubspec.yaml +4 -2
- package/dart/test/acquire_session_credentials_test.dart +34 -17
- package/dart/test/acquire_session_test.dart +109 -24
- package/dart/test/adv_realm_interference_test.dart +227 -99
- package/dart/test/bundle_version_pairing_test.dart +1 -1
- package/dart/test/cloud_bound_topology_test.dart +1 -1
- package/dart/test/cluster_attested_test.dart +52 -35
- package/dart/test/cluster_births_test.dart +5 -2
- package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
- package/dart/test/cluster_local_autosync_test.dart +5 -2
- package/dart/test/cluster_scale_test.dart +5 -2
- package/dart/test/compat_test.dart +1 -1
- package/dart/test/dlq_session_surface_test.dart +2 -1
- package/dart/test/errors_test.dart +1 -1
- package/dart/test/first_app_gauntlet_test.dart +78 -51
- package/dart/test/four_layer_nested_topology_test.dart +1 -1
- package/dart/test/harness_phase_timeout_test.dart +1 -1
- package/dart/test/home_parity_test.dart +9 -5
- package/dart/test/local_sync_inert_test.dart +22 -14
- package/dart/test/node_runner_transport_test.dart +9 -5
- package/dart/test/null_slot_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +5 -2
- package/dart/test/schema_validation_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +2 -1
- package/dart/test/session_bound_test.dart +92 -119
- package/dart/test/stale_bundle_test.dart +1 -1
- package/dart/test/test_harness_test.dart +77 -22
- package/dart/test/vo_read_typing_test.dart +32 -14
- package/dart/test/watch_after_offer_stall_test.dart +2 -1
- package/dart/test/webview_bridge_test.dart +17 -4
- package/dart/test/wire_test.dart +1 -1
- package/dart_types/CHANGELOG.md +8 -0
- package/dart_types/lib/nomos_types.dart +7 -6
- package/dart_types/lib/src/activity.dart +23 -13
- package/dart_types/lib/src/foreign_ref.dart +61 -112
- package/dart_types/lib/src/invitation.dart +96 -0
- package/dart_types/lib/src/nomos_ref.dart +97 -59
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +125 -19
- package/package.json +7 -3
- package/runtime/generated-application-host.js +8851 -0
- package/src/activity.ts +2 -2
- package/src/aggregate.ts +24 -1
- package/src/application_composition.ts +282 -0
- package/src/application_topology.ts +151 -0
- package/src/attested_core.ts +2 -2
- package/src/authoring_context.ts +9 -0
- package/src/birth_meter_ensure.ts +1 -1
- package/src/births.ts +53 -5
- package/src/build_package.ts +79 -71
- package/src/business_birth.ts +93 -10
- package/src/clear_directive.ts +3 -1
- package/src/codegen_dart.ts +369 -420
- package/src/codegen_dart_application.ts +1508 -0
- package/src/codegen_dart_client.ts +156 -95
- package/src/codegen_dart_scenario.ts +299 -0
- package/src/codegen_openapi.ts +8 -3
- package/src/codegen_proof.ts +1113 -1137
- package/src/codegen_provider_dart.ts +12 -1
- package/src/codegen_ts.ts +1176 -496
- package/src/compat_package_main.ts +22 -10
- package/src/compile_package_main.ts +1536 -337
- package/src/contract.ts +10 -4
- package/src/dart_barrel_guard.ts +5 -1
- package/src/describe_model.ts +13 -5
- package/src/directive.ts +244 -5
- package/src/engine_entry.ts +56 -13
- package/src/engine_javascript.ts +31 -0
- package/src/evolve_preview.ts +3 -12
- package/src/framework/application_home.ts +24 -10
- package/src/framework/capability.ts +15 -1
- package/src/framework/domain_lifecycle.ts +8 -0
- package/src/framework/framework_aggregates.ts +2 -2
- package/src/framework/genesis.ts +11 -1
- package/src/framework/home.ts +178 -75
- package/src/framework/impure_capability.ts +24 -7
- package/src/framework/relations.ts +13 -5
- package/src/framework/workspaces.ts +13 -12
- package/src/generated_client_names.ts +42 -3
- package/src/idof_lint.ts +65 -13
- package/src/index.ts +46 -12
- package/src/intent.ts +587 -0
- package/src/law_check.ts +64 -16
- package/src/precondition.ts +16 -4
- package/src/read_projection_namespace.ts +2 -7
- package/src/recipient.ts +33 -0
- package/src/relation_schema.ts +93 -0
- package/src/secondary_ensure.ts +11 -2
- package/src/shape_compat.ts +11 -1
- package/src/usd.ts +30 -8
- package/src/usd_layers.ts +447 -246
- package/src/usd_state.ts +1 -1
- package/src/value_object.ts +49 -13
- package/src/wire_encode.ts +124 -26
- package/src/workspace_sharing.ts +269 -0
- package/src/zod_introspection.ts +84 -0
- package/dart/example/nomos_secure_key_store.dart +0 -82
- package/dart/example/nomos_webview_transport.dart +0 -63
- package/dart/lib/nomos_headless.dart +0 -50
- package/dart/lib/testing.dart +0 -14
- package/dart/test/co2_product_gauntlet_test.dart +0 -244
- package/src/framework/identity.ts +0 -537
package/src/law_check.ts
CHANGED
|
@@ -33,7 +33,12 @@ import type { DomainModule } from "./codegen_dart.js";
|
|
|
33
33
|
import { admitsNull, type Field, type ValueObjectDescriptor, type ValueObjectFieldDescriptor } from "./fields.js";
|
|
34
34
|
import type { FieldOp, PlannedOp } from "./ops.js";
|
|
35
35
|
import { deterministicPorts } from "./ctx.js";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
synthesizeDirectivePayload,
|
|
38
|
+
synthesizePayload,
|
|
39
|
+
withBirthProbeGlobals,
|
|
40
|
+
type SynthesizedPayload,
|
|
41
|
+
} from "./codegen_proof.js";
|
|
37
42
|
import { __drainAuthoring, __resetAuthoring } from "./authoring.js";
|
|
38
43
|
import { __drainReads, __resetReads } from "./read.js";
|
|
39
44
|
import { __drainBirths, __resetBirths } from "./births.js";
|
|
@@ -83,6 +88,9 @@ interface ProbeResult {
|
|
|
83
88
|
readonly error?: unknown;
|
|
84
89
|
/** The FIELD ops the plan emitted (authoring sink + returned ops), when it didn't throw. */
|
|
85
90
|
readonly ops: FieldOp[];
|
|
91
|
+
/** Captured reads reached before return/throw. An empty-state read may make a valid stateful plan
|
|
92
|
+
* deliberately reject this isolated probe; the generated prerequisite journey is its positive proof. */
|
|
93
|
+
readonly reads: readonly { readonly queryId: string }[];
|
|
86
94
|
/** The top-level payload keys the plan actually READ (via the tracking proxy). */
|
|
87
95
|
readonly readFields: ReadonlySet<string>;
|
|
88
96
|
}
|
|
@@ -93,7 +101,7 @@ interface ProbeResult {
|
|
|
93
101
|
function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | undefined {
|
|
94
102
|
let synth: SynthesizedPayload;
|
|
95
103
|
try {
|
|
96
|
-
synth =
|
|
104
|
+
synth = synthesizeDirectivePayload(dir, where);
|
|
97
105
|
} catch {
|
|
98
106
|
return undefined;
|
|
99
107
|
}
|
|
@@ -118,6 +126,7 @@ function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | u
|
|
|
118
126
|
let threw = false;
|
|
119
127
|
let error: unknown;
|
|
120
128
|
let ops: FieldOp[] = [];
|
|
129
|
+
let reads: readonly { readonly queryId: string }[] = [];
|
|
121
130
|
withBirthProbeGlobals(() => {
|
|
122
131
|
__resetAuthoring();
|
|
123
132
|
__resetReads();
|
|
@@ -126,9 +135,11 @@ function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | u
|
|
|
126
135
|
returned = dir.plan(tracked as never, deterministicPorts({ physical: 1, replica: 1, principal: "principal:probe" }));
|
|
127
136
|
const drained: PlannedOp[] = [...__drainAuthoring(), ...(Array.isArray(returned) ? returned : [])];
|
|
128
137
|
ops = drained.filter((o): o is FieldOp => (o as { kind?: string }).kind !== "strike");
|
|
138
|
+
reads = __drainReads();
|
|
129
139
|
} catch (e) {
|
|
130
140
|
threw = true;
|
|
131
141
|
error = e;
|
|
142
|
+
reads = __drainReads();
|
|
132
143
|
} finally {
|
|
133
144
|
// Drain the sinks on every path so a throwing probe never leaks state into the next.
|
|
134
145
|
try { __drainAuthoring(); } catch { /* drained */ }
|
|
@@ -136,7 +147,7 @@ function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | u
|
|
|
136
147
|
try { __drainBirths(); } catch { /* drained */ }
|
|
137
148
|
}
|
|
138
149
|
});
|
|
139
|
-
return { synth, probePayload, returned, threw, error, ops, readFields };
|
|
150
|
+
return { synth, probePayload, returned, threw, error, ops, reads, readFields };
|
|
140
151
|
}
|
|
141
152
|
|
|
142
153
|
/** `zodObjectShape` with a swallow — a non-object payload yields `{}` (the return twin handles it). */
|
|
@@ -335,8 +346,24 @@ function deletedAmbientFindings(domain: string, dir: Directive<unknown>, probe:
|
|
|
335
346
|
// `return []` after `create(...).set(...)`; a plain object) throws "not iterable" at the FIRST
|
|
336
347
|
// dispatch on every peer. This is payload-INDEPENDENT (the return TYPE is fixed) ⇒ REFUSE.
|
|
337
348
|
// A plan that THREW on the sample is payload-DEPENDENT (can't prove) ⇒ WARN naming the sample.
|
|
338
|
-
function planReturnFindings(
|
|
349
|
+
function planReturnFindings(
|
|
350
|
+
domain: string,
|
|
351
|
+
dir: Directive<unknown>,
|
|
352
|
+
probe: ProbeResult,
|
|
353
|
+
generatedJourneyProvesDirective: boolean,
|
|
354
|
+
): Finding[] {
|
|
339
355
|
if (probe.threw) {
|
|
356
|
+
const declaredReadIds = new Set([
|
|
357
|
+
...(dir.declaredQueryReads ?? []).map((query) => query.id),
|
|
358
|
+
...(dir.declaredForeignReads ?? []).map((read) => read.queryId),
|
|
359
|
+
]);
|
|
360
|
+
const threwAfterDeclaredRead = probe.reads.length > 0 &&
|
|
361
|
+
probe.reads.every((read) => declaredReadIds.has(read.queryId));
|
|
362
|
+
// An isolated empty projection is the wrong positive fixture for a stateful business action. When the
|
|
363
|
+
// generator has built a real prerequisite journey for this exact public directive, that journey—not a
|
|
364
|
+
// fabricated checker row—proves the plan's return shape against actual folded state. CI executes it.
|
|
365
|
+
// Keep warning for private/unproved directives and for throws before any declared read.
|
|
366
|
+
if (threwAfterDeclaredRead && generatedJourneyProvesDirective) return [];
|
|
340
367
|
return [
|
|
341
368
|
{
|
|
342
369
|
domain,
|
|
@@ -393,6 +420,17 @@ function unreadPayloadFindings(domain: string, dir: Directive<unknown>, probe: P
|
|
|
393
420
|
gateConsumed.add(firstDeviceGate.keyHashField);
|
|
394
421
|
gateConsumed.add(firstDeviceGate.verifiedViaField);
|
|
395
422
|
}
|
|
423
|
+
// A cross-custody attestation gate verifies the signed quote directly from
|
|
424
|
+
// the payload. `publicKey` and `signature` are deliberately verification
|
|
425
|
+
// inputs rather than stored business fields, so the plan must not pretend to
|
|
426
|
+
// consume them merely to placate this lint. All field names are law-declared;
|
|
427
|
+
// do not hardcode the recordShare convention here.
|
|
428
|
+
const attestationGate = (dir as {
|
|
429
|
+
attestationAuthorizedGate?: { fields?: Readonly<Record<string, string>> };
|
|
430
|
+
}).attestationAuthorizedGate;
|
|
431
|
+
for (const field of Object.values(attestationGate?.fields ?? {})) {
|
|
432
|
+
if (typeof field === "string") gateConsumed.add(field);
|
|
433
|
+
}
|
|
396
434
|
// A dynamic .readsFrom(fromPayload("<wsField>"), …) edge routes the foreign read by a payload field the
|
|
397
435
|
// ROUTING lane consumes (the plan may pass it straight to read(…, {from}) without a tracked property
|
|
398
436
|
// read on the sampled path) — a declared trust-edge field is never a dead field.
|
|
@@ -1037,7 +1075,7 @@ function topLevelPortReadFindings(mod: DomainModule, portReadingDomains: Readonl
|
|
|
1037
1075
|
function foreignReadsPerformed(dir: Directive<unknown>, where: string): Set<string> | undefined {
|
|
1038
1076
|
let synth: SynthesizedPayload;
|
|
1039
1077
|
try {
|
|
1040
|
-
synth =
|
|
1078
|
+
synth = synthesizeDirectivePayload(dir, where);
|
|
1041
1079
|
} catch {
|
|
1042
1080
|
return undefined;
|
|
1043
1081
|
}
|
|
@@ -1144,30 +1182,40 @@ export function checkDomainModules(
|
|
|
1144
1182
|
): Finding[] {
|
|
1145
1183
|
const findings: Finding[] = [];
|
|
1146
1184
|
const portReadingDomains = opts?.portReadingDomains ?? new Set<string>();
|
|
1185
|
+
const generatedJourneyDirectiveIds = new Map<string, Set<string>>();
|
|
1186
|
+
try {
|
|
1187
|
+
const index = proofLegsIndex(modules as DomainModule[]);
|
|
1188
|
+
for (const [domain, legs] of Object.entries(index?.domains ?? {})) {
|
|
1189
|
+
const ids = generatedJourneyDirectiveIds.get(domain) ?? new Set<string>();
|
|
1190
|
+
if (legs.directiveId !== undefined) ids.add(legs.directiveId);
|
|
1191
|
+
for (const create of legs.extraCreates ?? []) ids.add(create.directiveId);
|
|
1192
|
+
for (const birth of legs.childBirths ?? (legs.childBirth === undefined ? [] : [legs.childBirth])) {
|
|
1193
|
+
ids.add(birth.birthDirectiveId);
|
|
1194
|
+
}
|
|
1195
|
+
for (const bootstrap of legs.relationBootstraps ?? (legs.relationBootstrap === undefined ? [] : [legs.relationBootstrap])) {
|
|
1196
|
+
ids.add(bootstrap.gatedDirectiveId);
|
|
1197
|
+
}
|
|
1198
|
+
generatedJourneyDirectiveIds.set(domain, ids);
|
|
1199
|
+
}
|
|
1200
|
+
} catch {
|
|
1201
|
+
// The ordinary findings and proof diagnostics retain their own exact failures. Never suppress from an
|
|
1202
|
+
// index we could not derive.
|
|
1203
|
+
}
|
|
1147
1204
|
|
|
1148
1205
|
for (const mod of modules) {
|
|
1149
1206
|
const domain = mod.domain ?? mod.name;
|
|
1207
|
+
const generatedJourney = generatedJourneyDirectiveIds.get(domain) ?? new Set<string>();
|
|
1150
1208
|
const aggById = new Map<string, AggregateHandle>();
|
|
1151
1209
|
for (const a of mod.aggregates) aggById.set(a.id, a);
|
|
1152
1210
|
|
|
1153
1211
|
// Slice-2 twin: the module top level read the captured ports (pristine-pool ineligibility).
|
|
1154
1212
|
findings.push(...topLevelPortReadFindings(mod, portReadingDomains));
|
|
1155
1213
|
|
|
1156
|
-
// The NOTE twin (unify): the auto-declared secondary ensures composeDomainModule injected.
|
|
1157
|
-
for (const ty of mod.autoDeclaredEnsures ?? []) {
|
|
1158
|
-
findings.push({
|
|
1159
|
-
domain,
|
|
1160
|
-
level: "note",
|
|
1161
|
-
what: `auto-declared Ensure→${ty} from a plan's secondary ensure(...) — the id-mint gate was opened for it`,
|
|
1162
|
-
fix: `no action needed; to make it explicit, export a directive that \`.ensures(${ty})\``,
|
|
1163
|
-
});
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
1214
|
for (const dir of mod.directives as Directive<unknown>[]) {
|
|
1167
1215
|
const where = `${domain}/${dir.id}`;
|
|
1168
1216
|
const probe = probeDirective(dir, where);
|
|
1169
1217
|
if (probe === undefined) continue; // unsynthesizable payload — proven by hand, not by check
|
|
1170
|
-
findings.push(...planReturnFindings(domain, dir, probe));
|
|
1218
|
+
findings.push(...planReturnFindings(domain, dir, probe, generatedJourney.has(dir.id)));
|
|
1171
1219
|
findings.push(...unreadPayloadFindings(domain, dir, probe));
|
|
1172
1220
|
findings.push(...opVsSchemaFindings(domain, dir, probe, aggById));
|
|
1173
1221
|
findings.push(...deletedAmbientFindings(domain, dir, probe));
|
package/src/precondition.ts
CHANGED
|
@@ -78,12 +78,24 @@ export function synthesizePreconditionInvariants<P>(directive: Directive<P>): Wo
|
|
|
78
78
|
id,
|
|
79
79
|
on: directive.id,
|
|
80
80
|
reads: (ctx: { intent: Record<string, unknown> }): InvariantRef[] => [
|
|
81
|
-
|
|
81
|
+
// Preconditions judge the state the directive is attempting to extend.
|
|
82
|
+
// The gate's reserved `pre:` namespace resolves the committed pre-apply
|
|
83
|
+
// aggregate; an ordinary ref would resolve the directive's trial result
|
|
84
|
+
// and therefore make every successful create appear to be a duplicate.
|
|
85
|
+
refAs(REF_NAME, desc.aggregateId, `pre:${desc.idField(ctx.intent)}`),
|
|
82
86
|
],
|
|
83
87
|
assert: (snapshots: Record<string, Record<string, unknown>>) => {
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
88
|
+
// Hosts historically represented an unresolved ref either by omitting
|
|
89
|
+
// the key or by returning an empty projection. Treat both identically;
|
|
90
|
+
// an admitted aggregate has at least one folded field after its atomic
|
|
91
|
+
// create intent, so an empty object is not a durable record.
|
|
92
|
+
const projected = snapshots[REF_NAME] as
|
|
93
|
+
| Record<string, unknown>
|
|
94
|
+
| undefined;
|
|
95
|
+
const row = projected !== undefined &&
|
|
96
|
+
desc.fieldNames.some((field) => Object.hasOwn(projected, field))
|
|
97
|
+
? projected
|
|
98
|
+
: undefined;
|
|
87
99
|
return desc.predicate(row) ? { accept: true } : { reject: desc.rejectCode };
|
|
88
100
|
},
|
|
89
101
|
...(desc.declassify !== undefined ? { declassify: desc.declassify } : {}),
|
|
@@ -89,15 +89,10 @@ export const GLOBAL_READ_PROJECTION_AGGREGATE_IDS: ReadonlySet<string> = new Set
|
|
|
89
89
|
"BirthPolicy",
|
|
90
90
|
"BirthGrant",
|
|
91
91
|
// home holons
|
|
92
|
-
"HomeOwner",
|
|
93
92
|
"SharedWithMe",
|
|
94
93
|
"BirthAttestation",
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
"User",
|
|
98
|
-
"DeviceKey",
|
|
99
|
-
"Organization",
|
|
100
|
-
"PublicUser",
|
|
94
|
+
"NomosInternalReceivingGroup7c1f5e",
|
|
95
|
+
"NomosInternalReceivingGroupPolicy7c1f5e",
|
|
101
96
|
// native relations (the one authz spine)
|
|
102
97
|
"RelationTuple",
|
|
103
98
|
// repair
|
package/src/recipient.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { zodDef, zodKind } from "./zod_introspection.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A business invitation recipient. It is wire-compatible with a string because
|
|
6
|
+
* law records the durable address, but generated applications expose the value
|
|
7
|
+
* as `Recipient<User>` so a UID, email address, principal, or record id cannot
|
|
8
|
+
* accidentally be passed in its place.
|
|
9
|
+
*/
|
|
10
|
+
export const NOMOS_RECIPIENT_BRAND: unique symbol = Symbol.for("nomos.recipient");
|
|
11
|
+
|
|
12
|
+
export function recipient(): z.ZodType<string> {
|
|
13
|
+
const schema = z.string().min(1);
|
|
14
|
+
Object.defineProperty(schema, NOMOS_RECIPIENT_BRAND, {
|
|
15
|
+
value: "User",
|
|
16
|
+
enumerable: false,
|
|
17
|
+
});
|
|
18
|
+
return schema;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Resolve a recipient annotation through Zod's omission/null wrappers. */
|
|
22
|
+
export function recipientMetaOf(zt: z.ZodTypeAny): "User" | undefined {
|
|
23
|
+
let current: z.ZodTypeAny | undefined = zt;
|
|
24
|
+
for (let hops = 0; current !== undefined && hops < 16; hops += 1) {
|
|
25
|
+
if ((current as unknown as Record<symbol, unknown>)[NOMOS_RECIPIENT_BRAND] === "User") {
|
|
26
|
+
return "User";
|
|
27
|
+
}
|
|
28
|
+
const kind = zodKind(current);
|
|
29
|
+
if (kind !== "optional" && kind !== "default" && kind !== "nullable") return undefined;
|
|
30
|
+
current = zodDef(current).innerType;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
package/src/relation_schema.ts
CHANGED
|
@@ -112,6 +112,99 @@ export function isRelationTypeDecl(v: unknown): v is RelationTypeDecl {
|
|
|
112
112
|
return typeof v === "object" && v !== null && (v as { __isRelationType?: unknown }).__isRelationType === true;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Discover authored relation fragments without the generic declaration scanner's JSON
|
|
117
|
+
* deduplication. A relation declaration stores its relations in a Map, and every Map
|
|
118
|
+
* stringifies as `{}`; JSON deduplication therefore made two distinct fragments of the
|
|
119
|
+
* same type look identical and silently discarded one of them.
|
|
120
|
+
*/
|
|
121
|
+
export function discoverRelationTypeDeclarations(
|
|
122
|
+
bag: Readonly<Record<string, unknown>>,
|
|
123
|
+
): RelationTypeDecl[] {
|
|
124
|
+
const seen = new Set<RelationTypeDecl>();
|
|
125
|
+
const declarations: RelationTypeDecl[] = [];
|
|
126
|
+
const add = (value: unknown): void => {
|
|
127
|
+
if (!isRelationTypeDecl(value) || seen.has(value)) return;
|
|
128
|
+
seen.add(value);
|
|
129
|
+
declarations.push(value);
|
|
130
|
+
};
|
|
131
|
+
for (const value of Object.values(bag)) {
|
|
132
|
+
if (Array.isArray(value)) for (const entry of value) add(entry);
|
|
133
|
+
else add(value);
|
|
134
|
+
}
|
|
135
|
+
return declarations;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compose independently authored fragments of one relation type into one complete
|
|
140
|
+
* declaration. Distinct relation names union; duplicate names and conflicting caps
|
|
141
|
+
* hard-fail. Validation happens on the complete type so a policy fragment may safely
|
|
142
|
+
* use `computed("owner")` when the owner relation is declared by another module in the
|
|
143
|
+
* same domain.
|
|
144
|
+
*/
|
|
145
|
+
export function mergeRelationTypeDeclarations(
|
|
146
|
+
declarations: readonly RelationTypeDecl[],
|
|
147
|
+
): RelationTypeDecl[] {
|
|
148
|
+
const unique = [...new Set(declarations)];
|
|
149
|
+
const groups = new Map<string, RelationTypeDecl[]>();
|
|
150
|
+
for (const declaration of unique) {
|
|
151
|
+
const key = `${declaration.global === true ? "global" : "local"}:${declaration.type}`;
|
|
152
|
+
const group = groups.get(key) ?? [];
|
|
153
|
+
group.push(declaration);
|
|
154
|
+
groups.set(key, group);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const merged: RelationTypeDecl[] = [];
|
|
158
|
+
for (const group of groups.values()) {
|
|
159
|
+
const first = group[0]!;
|
|
160
|
+
if (group.length === 1) {
|
|
161
|
+
validateRelationType(first);
|
|
162
|
+
merged.push(first);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const relations = new Map<string, RelationDefAst>();
|
|
166
|
+
let caps: RelationCaps | undefined;
|
|
167
|
+
for (const declaration of group) {
|
|
168
|
+
if (declaration.caps !== undefined) {
|
|
169
|
+
if (
|
|
170
|
+
caps !== undefined &&
|
|
171
|
+
(caps.maxDepth !== declaration.caps.maxDepth || caps.maxFanout !== declaration.caps.maxFanout)
|
|
172
|
+
) {
|
|
173
|
+
throw new Error(`relationType("${declaration.type}"): conflicting caps across declarations`);
|
|
174
|
+
}
|
|
175
|
+
caps = declaration.caps;
|
|
176
|
+
}
|
|
177
|
+
for (const [relation, definition] of declaration.relations) {
|
|
178
|
+
const existing = relations.get(relation);
|
|
179
|
+
if (existing !== undefined) {
|
|
180
|
+
// Framework-owned declarations can cross the published-CLI/package bundle boundary as
|
|
181
|
+
// distinct object identities. The private brand plus byte-identical definition identifies
|
|
182
|
+
// that one canonical concept; tenant-authored collisions remain a loud hard failure.
|
|
183
|
+
const repeatedFrameworkDeclaration =
|
|
184
|
+
(first as RelationTypeDecl & { businessBirthInternal?: boolean }).businessBirthInternal === true &&
|
|
185
|
+
(declaration as RelationTypeDecl & { businessBirthInternal?: boolean }).businessBirthInternal === true &&
|
|
186
|
+
JSON.stringify(existing) === JSON.stringify(definition);
|
|
187
|
+
if (repeatedFrameworkDeclaration) continue;
|
|
188
|
+
throw new Error(
|
|
189
|
+
`relationType("${declaration.type}"): relation "${relation}" declared in two relationType() blocks`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
relations.set(relation, definition);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const declaration: RelationTypeDecl = {
|
|
196
|
+
type: first.type,
|
|
197
|
+
relations,
|
|
198
|
+
...(caps === undefined ? {} : { caps }),
|
|
199
|
+
...(first.global === true ? { global: true } : {}),
|
|
200
|
+
__isRelationType: true,
|
|
201
|
+
};
|
|
202
|
+
validateRelationType(declaration);
|
|
203
|
+
merged.push(declaration);
|
|
204
|
+
}
|
|
205
|
+
return merged;
|
|
206
|
+
}
|
|
207
|
+
|
|
115
208
|
// ─────────────────────────────── relation-type NAMESPACING ───────────────────────────────
|
|
116
209
|
|
|
117
210
|
/**
|
package/src/secondary_ensure.ts
CHANGED
|
@@ -57,6 +57,10 @@ export function sanctioningEnsureDirective(agg: AggregateHandle): Directive<unkn
|
|
|
57
57
|
.ensures(agg)
|
|
58
58
|
.payload(z.object({}).strict())
|
|
59
59
|
.plan(() => []) as unknown as Directive<unknown>;
|
|
60
|
+
// This declaration exists only to certify the id-mint lane. It is never a
|
|
61
|
+
// tenant action and must not appear in generated clients, applications, or
|
|
62
|
+
// OpenAPI, irrespective of whether the aggregate itself is framework-owned.
|
|
63
|
+
(dir as { businessBirthInternal?: true }).businessBirthInternal = true;
|
|
60
64
|
// FRAMEWORK-SHARED DEDUP (Dart codegen): when the sanctioned aggregate is a FRAMEWORK singleton
|
|
61
65
|
// (e.g. RelationTuple — auto-ensured into EVERY relation-using domain), this directive is injected
|
|
62
66
|
// per-domain but its payload class (`NomosEnsure_<Agg>Payload`, zero-field) is byte-identical across
|
|
@@ -73,8 +77,10 @@ export function sanctioningEnsureDirective(agg: AggregateHandle): Directive<unkn
|
|
|
73
77
|
|
|
74
78
|
/**
|
|
75
79
|
* Discover the secondary-ensured types a domain's directives introduce that NO directive already
|
|
76
|
-
* declares `.ensures()`
|
|
77
|
-
*
|
|
80
|
+
* declares as its PRIMARY `.ensures()` target — the SORTED gap set the compose seam must sanction.
|
|
81
|
+
* Declared `.alsoEnsures()` outputs are included directly as well as plan-probed ensures. New kernels
|
|
82
|
+
* understand `secondaryOutputs`, but older supported kernels only open the deterministic-id lane from
|
|
83
|
+
* a primary Ensure marker; the hidden sanction keeps one compiled package portable across both.
|
|
78
84
|
*/
|
|
79
85
|
export function discoverUnsanctionedEnsures(
|
|
80
86
|
directives: readonly Directive<unknown>[],
|
|
@@ -87,6 +93,9 @@ export function discoverUnsanctionedEnsures(
|
|
|
87
93
|
}
|
|
88
94
|
const gap = new Set<string>();
|
|
89
95
|
for (const d of directives) {
|
|
96
|
+
for (const output of (d as { secondaryOutputs?: readonly { aggregateId: string; marker: string }[] }).secondaryOutputs ?? []) {
|
|
97
|
+
if (output.marker === "ensures" && !declared.has(output.aggregateId)) gap.add(output.aggregateId);
|
|
98
|
+
}
|
|
90
99
|
// A `.births()` directive's genesis-recipe plan runs under a different probe surface; its meter
|
|
91
100
|
// record is sanctioned by `birth_meter_ensure.ts`, so leave it to that lane (avoid double probing).
|
|
92
101
|
if ((d as { birthsChild?: boolean }).birthsChild === true) continue;
|
package/src/shape_compat.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { z } from "zod";
|
|
|
26
26
|
import { introspectPayload, type DomainModule, type PayloadFieldSpec } from "./codegen_dart.js";
|
|
27
27
|
import type { StableIds } from "./stable_ids_types.js";
|
|
28
28
|
import type { Field } from "./fields.js";
|
|
29
|
+
import { isBusinessIntent } from "./intent.js";
|
|
29
30
|
|
|
30
31
|
/** One payload field's normalized shape: its kind + whether the schema makes it optional. */
|
|
31
32
|
export interface PayloadFieldShape {
|
|
@@ -126,6 +127,12 @@ function payloadFieldKind(spec: PayloadFieldSpec): string {
|
|
|
126
127
|
case "wrapped":
|
|
127
128
|
// typed-id (string wire) or unit (number wire) — the interface sees the WIRE kind.
|
|
128
129
|
return spec.dartType.endsWith("Id") ? "string" : "double";
|
|
130
|
+
case "ref":
|
|
131
|
+
// NomosRef<T> is aggregate-typed in the client and remains a string id on the wire.
|
|
132
|
+
return "string";
|
|
133
|
+
case "recipient":
|
|
134
|
+
// Recipient<User> is nominal in clients and an opaque delivery address on the law wire.
|
|
135
|
+
return "string";
|
|
129
136
|
case "vo":
|
|
130
137
|
case "voList":
|
|
131
138
|
// Interface kinds stay at the base lane for now (slice D of vo_authoring_end_to_end.md
|
|
@@ -192,7 +199,10 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
|
|
|
192
199
|
fieldEntries.push([spec.name, { kind: payloadFieldKind(spec), optional: !!spec.optional }]);
|
|
193
200
|
}
|
|
194
201
|
}
|
|
195
|
-
dirEntries.push([
|
|
202
|
+
dirEntries.push([
|
|
203
|
+
d.businessBirth === undefined || isBusinessIntent(d) ? d.id : "birth",
|
|
204
|
+
{ fields: sortedRec(fieldEntries) },
|
|
205
|
+
]);
|
|
196
206
|
}
|
|
197
207
|
Object.assign(directives, sortedRec(dirEntries));
|
|
198
208
|
const aggregates: Record<string, AggregateReadShape> = {};
|
package/src/usd.ts
CHANGED
|
@@ -72,7 +72,7 @@ export type RefMode = WireRefMode;
|
|
|
72
72
|
* (an aggregate is a single-source schema); they pass through by path.
|
|
73
73
|
* - `directive`: a composable law unit. `requires` is the capability set (#137:
|
|
74
74
|
* add/narrow, never silently remove); `scope` is the optional composable scope
|
|
75
|
-
* path (#137: narrow-not-widen). `
|
|
75
|
+
* path (#137: narrow-not-widen). `artifactRef` is the certified-artifact handle
|
|
76
76
|
* — left `undefined` here (stage 2).
|
|
77
77
|
*/
|
|
78
78
|
export type UsdPrim =
|
|
@@ -123,7 +123,7 @@ export type UsdPrim =
|
|
|
123
123
|
*/
|
|
124
124
|
readonly payloadJsonSchema?: unknown;
|
|
125
125
|
/** Certified-artifact payload reference — `undefined` for now (stage 2). */
|
|
126
|
-
readonly
|
|
126
|
+
readonly artifactRef?: string;
|
|
127
127
|
};
|
|
128
128
|
|
|
129
129
|
/** One captured query declaration on a USD layer (read-side closure). `key` is the
|
|
@@ -753,7 +753,7 @@ function sortPrims(prims: UsdPrim[]): UsdPrim[] {
|
|
|
753
753
|
* `manifest.ts`'s `domainManifest` (and therefore its `encodeKernelSchema` driver encoding)
|
|
754
754
|
* — no driver-encoding logic is duplicated here. Aggregate prims carry the exact
|
|
755
755
|
* encoded schema; directive prims map `requiresCapability` → `requires: [cap]`, leave
|
|
756
|
-
* `scope`/`
|
|
756
|
+
* `scope`/`artifactRef` `undefined` (no static scope or certified artifact today).
|
|
757
757
|
*/
|
|
758
758
|
function encodeModuleToPrims(layerPath: string, manifest: CanonicalManifest): UsdPrim[] {
|
|
759
759
|
const prims: UsdPrim[] = [];
|
|
@@ -789,7 +789,7 @@ function encodeModuleToPrims(layerPath: string, manifest: CanonicalManifest): Us
|
|
|
789
789
|
// Omit-when-empty from the manifest, so a single-output prim is byte-identical to before.
|
|
790
790
|
...(d.secondaryOutputs !== undefined ? { secondaryOutputs: d.secondaryOutputs } : {}),
|
|
791
791
|
payloadJsonSchema: d.payloadJsonSchema,
|
|
792
|
-
// `scope`/`
|
|
792
|
+
// `scope`/`artifactRef` deliberately omitted (undefined): no static scope in
|
|
793
793
|
// the DSL today, payload is the stage-2 certified artifact.
|
|
794
794
|
});
|
|
795
795
|
}
|
|
@@ -930,6 +930,7 @@ function serializeRelationSchema(
|
|
|
930
930
|
domainKey: string,
|
|
931
931
|
): UsdLayer["nomosRelations"] {
|
|
932
932
|
if (decls.length === 0) return undefined;
|
|
933
|
+
const namespacedTypes = new Set(decls.filter((decl) => decl.global !== true).map((decl) => decl.type));
|
|
933
934
|
type AllowedW = { kind: string; type?: string; ns?: string; relation?: string };
|
|
934
935
|
const allowedKey = (a: AllowedW): string =>
|
|
935
936
|
a.kind === "direct" ? `direct:${a.type}` : a.kind === "wildcard" ? "wildcard" : `userset:${a.type}#${a.relation}`;
|
|
@@ -952,7 +953,18 @@ function serializeRelationSchema(
|
|
|
952
953
|
}
|
|
953
954
|
for (const [rel, def] of d.relations) {
|
|
954
955
|
if (entry.relations.has(rel)) throw new Error(`relationType("${d.type}"): relation "${rel}" declared in two relationType() blocks`);
|
|
955
|
-
|
|
956
|
+
// An allowed object/userset subject names another relation type. When that target is local to this
|
|
957
|
+
// domain it must carry the SAME qualifier as the schema key and objectOf(...) tuple subject; otherwise
|
|
958
|
+
// a perfectly typed parent edge is stored under `<domain>__Parent` while the gate only permits bare
|
|
959
|
+
// `Parent`, making tuple-to-userset inheritance silently dead. Framework-global user/key types stay bare.
|
|
960
|
+
const allowed = [...def.allowed]
|
|
961
|
+
.map((subject) => {
|
|
962
|
+
if ((subject.kind === "direct" || subject.kind === "userset") && namespacedTypes.has(subject.type)) {
|
|
963
|
+
return { ...subject, type: qualifyRelationType(domainKey, subject.type) };
|
|
964
|
+
}
|
|
965
|
+
return subject;
|
|
966
|
+
})
|
|
967
|
+
.sort((x, y) => (allowedKey(x as AllowedW) < allowedKey(y as AllowedW) ? -1 : 1));
|
|
956
968
|
entry.relations.set(rel, { rewrite: def.rewrite, allowed });
|
|
957
969
|
}
|
|
958
970
|
}
|
|
@@ -982,6 +994,16 @@ function synthesizeReadRelationTypes(
|
|
|
982
994
|
for (const a of manifest.aggregates) {
|
|
983
995
|
if (a.requiresRead !== undefined && a.owned === undefined) caps.add(a.requiresRead);
|
|
984
996
|
}
|
|
997
|
+
// Pack acquisition happens before per-record filtering/decryption. Any law with owned state therefore
|
|
998
|
+
// needs the same obscure capability at workspace grain, and the receiving-group genesis always writes
|
|
999
|
+
// that seat so a standalone group law remains self-consistent too. This grants ciphertext transport;
|
|
1000
|
+
// each owned record still has its separately synthesized aggregate-grain relation below.
|
|
1001
|
+
if (
|
|
1002
|
+
manifest.aggregates.some((aggregate) => aggregate.owned !== undefined) ||
|
|
1003
|
+
manifest.aggregates.some((aggregate) => aggregate.id === "NomosInternalReceivingGroup7c1f5e")
|
|
1004
|
+
) {
|
|
1005
|
+
caps.add(OWNED_READ_RELATION);
|
|
1006
|
+
}
|
|
985
1007
|
const out: import("./relation_schema.js").RelationTypeDecl[] = [];
|
|
986
1008
|
// GLOBAL (un-namespaced): the read gate hardcodes `ObjectRef::new("workspace","self")` in Rust
|
|
987
1009
|
// (`wasm_git_holon.rs`), so the synthesized read-visibility `workspace` type MUST stay bare — it is
|
|
@@ -1542,7 +1564,7 @@ export function emitUsd(
|
|
|
1542
1564
|
// DOMAIN CONTRACTS: the additive `nomosContracts` descriptor (hash-inert, omit-when-empty).
|
|
1543
1565
|
const nomosContracts = buildNomosContracts(l.module);
|
|
1544
1566
|
const flowEndorse = [...l.module.directives]
|
|
1545
|
-
.flatMap((d) => harvestPrincipalFlows(d as never).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
|
|
1567
|
+
.flatMap((d) => harvestPrincipalFlows(d as never, l.module.domain ?? l.module.name).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
|
|
1546
1568
|
.sort((a, b) =>
|
|
1547
1569
|
a.directiveId !== b.directiveId ? (a.directiveId < b.directiveId ? -1 : 1)
|
|
1548
1570
|
: a.sink !== b.sink ? (a.sink < b.sink ? -1 : 1)
|
|
@@ -1645,7 +1667,7 @@ function aggregateEqual(
|
|
|
1645
1667
|
* Merge a stronger `over` prim onto a `base` prim at the SAME path, per the #137
|
|
1646
1668
|
* typed rules — reusing `compose.ts`'s `mergeRequiresScope` for directive prims
|
|
1647
1669
|
* (requires add/narrow-not-remove; scope narrow-not-widen; widen/remove need
|
|
1648
|
-
* `authority`). An uncertified
|
|
1670
|
+
* `authority`). An uncertified artifact would always be refused — but `artifactRef`
|
|
1649
1671
|
* is unset here (stage 2), so there is nothing to refuse yet.
|
|
1650
1672
|
*
|
|
1651
1673
|
* Mixing prim KINDS at one path, or redefining an aggregate's schema across layers,
|
|
@@ -1720,7 +1742,7 @@ function mergeUsdPrim(base: UsdPrim, over: UsdPrim, opts: ComposeOptions): UsdPr
|
|
|
1720
1742
|
: base.emits !== undefined
|
|
1721
1743
|
? { emits: base.emits }
|
|
1722
1744
|
: {}),
|
|
1723
|
-
//
|
|
1745
|
+
// artifactRef stays unset (a later certified-artifact slice).
|
|
1724
1746
|
};
|
|
1725
1747
|
return out;
|
|
1726
1748
|
}
|