@githolon/dsl 0.101.5 → 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/build_package.ts
CHANGED
|
@@ -28,9 +28,6 @@
|
|
|
28
28
|
* is carried in the package USD IR + the committed `nomos.stable-ids.json` lockfile; the old
|
|
29
29
|
* `domain_identity_*.json` sidecars (and `writeIdentity`) are REMOVED. The per-domain hash is surfaced
|
|
30
30
|
* in the compile summary.
|
|
31
|
-
* * `packageUsda(usdJson, javascript)` — the `.package.usda` envelope, byte-exact
|
|
32
|
-
* to `build_nomos.mjs` `packageUsda()` == Rust `domain_package_from_js`
|
|
33
|
-
* (deterministic-rquickjs/src/domain_package.rs).
|
|
34
31
|
* * `emitUsdJsonForModules(modules)` — the one-line USD-IR emit every
|
|
35
32
|
* `emit_*_usd_json.ts` wrapped (`emitUsd` over `/Nomos/<domain>` layers).
|
|
36
33
|
*/
|
|
@@ -53,6 +50,12 @@ import type { CombinedDecl } from "./combined.js";
|
|
|
53
50
|
import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
|
|
54
51
|
import type { WorkspaceTypeDecl } from "./workspace_type.js";
|
|
55
52
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
53
|
+
import {
|
|
54
|
+
applicationLanguageOf,
|
|
55
|
+
assertApplicationLanguageCoversAuthoredDirectives,
|
|
56
|
+
executableDirectivesForLanguage,
|
|
57
|
+
remapWorkspaceInvariantsForLanguage,
|
|
58
|
+
} from "./intent.js";
|
|
56
59
|
import { synthesizePreconditionInvariants } from "./precondition.js";
|
|
57
60
|
import {
|
|
58
61
|
BIRTH_METER_AGGREGATES,
|
|
@@ -69,7 +72,11 @@ import {
|
|
|
69
72
|
type TrustSourceDecl,
|
|
70
73
|
type TrustPolicyDecl,
|
|
71
74
|
} from "./attested.js";
|
|
72
|
-
import {
|
|
75
|
+
import {
|
|
76
|
+
discoverRelationTypeDeclarations,
|
|
77
|
+
mergeRelationTypeDeclarations,
|
|
78
|
+
type RelationTypeDecl,
|
|
79
|
+
} from "./relation_schema.js";
|
|
73
80
|
import { domainHash, emitManifestBytes } from "./manifest.js";
|
|
74
81
|
import { collectValueObjectDescriptors } from "./value_object.js";
|
|
75
82
|
import { emitUsd } from "./usd.js";
|
|
@@ -86,7 +93,10 @@ import {
|
|
|
86
93
|
applyConflictPolicyResolvers,
|
|
87
94
|
conflictPoliciesOf,
|
|
88
95
|
} from "./conflicts.js";
|
|
89
|
-
import {
|
|
96
|
+
import {
|
|
97
|
+
expandBusinessBirthExports,
|
|
98
|
+
isBusinessBirthDirective,
|
|
99
|
+
} from "./business_birth.js";
|
|
90
100
|
import { withOwnedCustodyExports } from "./framework/owned_custody.js";
|
|
91
101
|
import { withBusinessCorrectionExports } from "./framework/business_corrections.js";
|
|
92
102
|
import {
|
|
@@ -120,7 +130,7 @@ export function aggregatesOf(mod: Mod): AggregateHandle[] {
|
|
|
120
130
|
return out;
|
|
121
131
|
}
|
|
122
132
|
|
|
123
|
-
/** Collect every exported `
|
|
133
|
+
/** Collect every exported `recordContract(...)` handle (`__isContract` duck-type). */
|
|
124
134
|
export function contractsOf(mod: Mod): import("./contract.js").ContractHandle[] {
|
|
125
135
|
const out: import("./contract.js").ContractHandle[] = [];
|
|
126
136
|
for (const v of Object.values(mod)) {
|
|
@@ -193,8 +203,16 @@ export interface DomainModuleSpec {
|
|
|
193
203
|
export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
194
204
|
const domain = spec.domain ?? spec.name;
|
|
195
205
|
const conflictPolicies = conflictPoliciesOf(spec.modules, domain);
|
|
206
|
+
const applicationLanguage = applicationLanguageOf(spec.modules, domain);
|
|
196
207
|
let merged: Mod = {};
|
|
197
208
|
for (const m of spec.modules) merged = { ...merged, ...m };
|
|
209
|
+
if (applicationLanguage !== undefined) {
|
|
210
|
+
assertApplicationLanguageCoversAuthoredDirectives(
|
|
211
|
+
applicationLanguage,
|
|
212
|
+
directivesOf(merged),
|
|
213
|
+
domain,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
198
216
|
merged = expandBusinessBirthExports(merged, spec.domain ?? spec.name);
|
|
199
217
|
merged = withOwnedCustodyExports(merged);
|
|
200
218
|
merged = withBusinessCorrectionExports(merged);
|
|
@@ -219,10 +237,28 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
219
237
|
// discovered it off the same expansion). Role-free domains are byte-identical (era rule).
|
|
220
238
|
merged = expandRoleExports(merged);
|
|
221
239
|
const roles = rolesOf(merged);
|
|
240
|
+
const exportedRelationTypes = discoverRelationTypeDeclarations(merged);
|
|
222
241
|
|
|
223
242
|
const byId = new Map<string, AggregateHandle>();
|
|
224
243
|
for (const a of [...aggregatesOf(merged), ...(spec.extraAggregates ?? [])]) byId.set(a.id, a);
|
|
225
|
-
const
|
|
244
|
+
for (const policy of applicationLanguage?.workspaceSharing ?? []) {
|
|
245
|
+
byId.set(policy.invitationLaw.claim.id, policy.invitationLaw.claim);
|
|
246
|
+
}
|
|
247
|
+
const discoveredDirectives = directivesOf(merged);
|
|
248
|
+
const directives = applyConflictPolicyResolvers(
|
|
249
|
+
applicationLanguage === undefined
|
|
250
|
+
? discoveredDirectives
|
|
251
|
+
: executableDirectivesForLanguage(applicationLanguage, discoveredDirectives, domain),
|
|
252
|
+
conflictPolicies,
|
|
253
|
+
);
|
|
254
|
+
// Invitations are ordinary, replayed law hidden behind the generated application. Each sharing
|
|
255
|
+
// policy contributes one possession-proof redemption directive; it is injected after the tenant
|
|
256
|
+
// language has been closed so it cannot become a second public lane.
|
|
257
|
+
for (const policy of applicationLanguage?.workspaceSharing ?? []) {
|
|
258
|
+
if (!directives.some((candidate) => candidate.id === policy.invitationLaw.redeem.id)) {
|
|
259
|
+
directives.push(policy.invitationLaw.redeem);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
226
262
|
// RELATIONS AUTO-ENSURE + AUTO-COMPOSE (co2 recovery DX): a domain that USES relation law — declares a
|
|
227
263
|
// relation schema, a `.requires(...)`-gated directive, or relate-family/signer directives ensuring
|
|
228
264
|
// RelationTuple — needs the framework RelationTuple aggregate IN the schema on TWO counts: (1) the deploy
|
|
@@ -264,6 +300,7 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
264
300
|
}
|
|
265
301
|
const usesRelations =
|
|
266
302
|
declaresPrivateRead ||
|
|
303
|
+
exportedRelationTypes.length > 0 ||
|
|
267
304
|
(spec.relationTypes ?? []).length > 0 ||
|
|
268
305
|
directives.some((d) => (d as { requiresRelationGate?: unknown }).requiresRelationGate !== undefined) ||
|
|
269
306
|
directives.some((d) => d.aggregateId === RelationTuple.id);
|
|
@@ -343,6 +380,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
343
380
|
const queryById = new Map<string, QueryDecl>();
|
|
344
381
|
for (const query of spec.queries ?? []) queryById.set(query.id, query);
|
|
345
382
|
const queries = [...queryById.values()];
|
|
383
|
+
for (const policy of applicationLanguage?.workspaceSharing ?? []) {
|
|
384
|
+
if (!queries.some((candidate) => candidate.id === policy.invitationLaw.pendingByRecipient.id)) {
|
|
385
|
+
queries.push(policy.invitationLaw.pendingByRecipient);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
346
388
|
const counts = [...(spec.counts ?? [])];
|
|
347
389
|
const spatials = [...(spec.spatials ?? [])];
|
|
348
390
|
const deriveds = [...(spec.deriveds ?? [])];
|
|
@@ -368,7 +410,17 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
368
410
|
for (const invariant of spec.workspaceInvariants ?? []) {
|
|
369
411
|
workspaceInvariantById.set(invariant.id, invariant);
|
|
370
412
|
}
|
|
371
|
-
const workspaceInvariants =
|
|
413
|
+
const workspaceInvariants = applicationLanguage === undefined
|
|
414
|
+
? [...workspaceInvariantById.values()]
|
|
415
|
+
: remapWorkspaceInvariantsForLanguage(
|
|
416
|
+
[...workspaceInvariantById.values()],
|
|
417
|
+
applicationLanguage,
|
|
418
|
+
);
|
|
419
|
+
for (const policy of applicationLanguage?.workspaceSharing ?? []) {
|
|
420
|
+
if (!workspaceInvariants.some((candidate) => candidate.id === policy.invitationLaw.gate.id)) {
|
|
421
|
+
workspaceInvariants.push(policy.invitationLaw.gate);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
372
424
|
if (declaresBirth) {
|
|
373
425
|
const haveInvariant = new Set(workspaceInvariants.map((w) => w.id));
|
|
374
426
|
for (const lifecycle of [
|
|
@@ -411,14 +463,10 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
411
463
|
queries.push(internalDelegationQuery);
|
|
412
464
|
}
|
|
413
465
|
}
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
for (const value of spec.relationTypes ?? []) {
|
|
419
|
-
relationTypeByKey.set(`${value.global === true ? "global" : "local"}:${value.type}`, value);
|
|
420
|
-
}
|
|
421
|
-
const relationTypes = [...relationTypeByKey.values()];
|
|
466
|
+
const relationTypes = mergeRelationTypeDeclarations([
|
|
467
|
+
...exportedRelationTypes,
|
|
468
|
+
...(spec.relationTypes ?? []),
|
|
469
|
+
]);
|
|
422
470
|
// RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX — same idiom as the meter
|
|
423
471
|
// queries above): a domain that declares relation law (a relation schema, a `.requires(...)` gated
|
|
424
472
|
// directive, or relate-family/signer directives ensuring RelationTuple) gets the framework's
|
|
@@ -576,12 +624,26 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
576
624
|
directives,
|
|
577
625
|
spec.domain ?? spec.name,
|
|
578
626
|
);
|
|
627
|
+
const publicApplicationIntents = applicationLanguage === undefined
|
|
628
|
+
? undefined
|
|
629
|
+
: [
|
|
630
|
+
...applicationLanguage.intents,
|
|
631
|
+
...directives.filter((directive) =>
|
|
632
|
+
isBusinessBirthDirective(directive) &&
|
|
633
|
+
!applicationLanguage.intents.some((intent) => intent.id === directive.id)),
|
|
634
|
+
];
|
|
579
635
|
|
|
580
636
|
return {
|
|
581
637
|
name: spec.name,
|
|
582
638
|
domain: spec.domain ?? spec.name,
|
|
583
639
|
aggregates,
|
|
584
640
|
directives,
|
|
641
|
+
...(publicApplicationIntents !== undefined
|
|
642
|
+
? { applicationIntents: publicApplicationIntents }
|
|
643
|
+
: {}),
|
|
644
|
+
...(applicationLanguage !== undefined && applicationLanguage.workspaceSharing.length > 0
|
|
645
|
+
? { applicationWorkspaceSharing: [...applicationLanguage.workspaceSharing] }
|
|
646
|
+
: {}),
|
|
585
647
|
// Diagnostic metadata for the compile summary — NOT law (not hashed). Omit-when-empty.
|
|
586
648
|
...(autoDeclaredEnsures.length > 0 ? { autoDeclaredEnsures } : {}),
|
|
587
649
|
// ROLE SUGAR codegen metadata — NOT law (not hashed): drives the `canEdit<Agg>`/`canView<Agg>`
|
|
@@ -1125,63 +1187,9 @@ export function buildIdentity(modules: readonly DomainModule[]): IdentityEmit {
|
|
|
1125
1187
|
// per-domain hash is surfaced in the compile summary.
|
|
1126
1188
|
|
|
1127
1189
|
// ─────────────────────────────────────────────────────────────────────────────────
|
|
1128
|
-
// The
|
|
1190
|
+
// The canonical USD-IR emit
|
|
1129
1191
|
// ─────────────────────────────────────────────────────────────────────────────────
|
|
1130
1192
|
|
|
1131
|
-
/** The package format marker the kernel checks (domain_package.rs / build_nomos.mjs). */
|
|
1132
|
-
export const NOMOS_DOMAIN_PACKAGE_FORMAT = "nomos.openusd-ir-package.v1";
|
|
1133
|
-
|
|
1134
|
-
/** Lowercase hex of the UTF-8 bytes (== build_nomos.mjs `hexUtf8` == Rust `hex_encode`). */
|
|
1135
|
-
export function hexUtf8(text: string): string {
|
|
1136
|
-
return Buffer.from(text, "utf8").toString("hex");
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
/**
|
|
1140
|
-
* The `.package.usda` envelope — BYTE-EXACT to `build_nomos.mjs` `packageUsda()` and
|
|
1141
|
-
* Rust `domain_package_from_js` (deterministic-rquickjs/src/domain_package.rs:11-33).
|
|
1142
|
-
* The kernel's acceptance is substring-based field extraction over this template; the
|
|
1143
|
-
* domainHash the deploy names is sha256 over the WHOLE returned string (trailing
|
|
1144
|
-
* newline included) exactly as committed.
|
|
1145
|
-
*/
|
|
1146
|
-
export function packageUsda(usdJson: string, javascript: string, opts: { compact?: boolean } = {}): string {
|
|
1147
|
-
// THE COMPACT ERA (opt-in, 2026-07-04): base64 blob fields (`*B64`, 1.33× text) instead of hex
|
|
1148
|
-
// (2×). Requires every consumer to carry the decode arm (kernel domain_package.rs extract_blob +
|
|
1149
|
-
// the JS parsers) — shipped ahead of this emit, so `compact: true` is the tenant's explicit
|
|
1150
|
-
// choice once their stack is current. Hex stays the default emit until a future major; hex
|
|
1151
|
-
// packages parse FOREVER (the era rule). sha256(the returned string) is the domainHash either
|
|
1152
|
-
// way — a compact recompile is ONE named hash move (batched with minify + locale-strip).
|
|
1153
|
-
if (opts.compact) {
|
|
1154
|
-
return `#usda 1.0
|
|
1155
|
-
(
|
|
1156
|
-
customData = {
|
|
1157
|
-
string nomos:format = "${NOMOS_DOMAIN_PACKAGE_FORMAT}"
|
|
1158
|
-
}
|
|
1159
|
-
)
|
|
1160
|
-
|
|
1161
|
-
def "NomosDomainPackage"
|
|
1162
|
-
{
|
|
1163
|
-
custom string nomos:usdJsonB64 = "${Buffer.from(usdJson, "utf8").toString("base64")}"
|
|
1164
|
-
custom string nomos:executable:language = "javascript"
|
|
1165
|
-
custom string nomos:executable:javascriptB64 = "${Buffer.from(javascript, "utf8").toString("base64")}"
|
|
1166
|
-
}
|
|
1167
|
-
`;
|
|
1168
|
-
}
|
|
1169
|
-
return `#usda 1.0
|
|
1170
|
-
(
|
|
1171
|
-
customData = {
|
|
1172
|
-
string nomos:format = "${NOMOS_DOMAIN_PACKAGE_FORMAT}"
|
|
1173
|
-
}
|
|
1174
|
-
)
|
|
1175
|
-
|
|
1176
|
-
def "NomosDomainPackage"
|
|
1177
|
-
{
|
|
1178
|
-
custom string nomos:usdJsonHex = "${hexUtf8(usdJson)}"
|
|
1179
|
-
custom string nomos:executable:language = "javascript"
|
|
1180
|
-
custom string nomos:executable:javascriptHex = "${hexUtf8(javascript)}"
|
|
1181
|
-
}
|
|
1182
|
-
`;
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
1193
|
/**
|
|
1186
1194
|
* The USD-IR JSON for a package's modules: `emitUsd` over `/Nomos/<domain>` layers,
|
|
1187
1195
|
* `JSON.stringify`'d EXACTLY like every `emit_*_usd_json.ts` (single line, insertion
|
package/src/business_birth.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// NOMOS — one business birth at the tenant surface; compute-science mechanics lower here.
|
|
2
2
|
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { BUSINESS_BIRTH_SEED_PAYLOAD_FIELD } from "./aggregate.js";
|
|
4
|
+
import { BUSINESS_BIRTH_SEED_PAYLOAD_FIELD, OWNED_READ_RELATION } from "./aggregate.js";
|
|
5
5
|
import { ensure } from "./authoring.js";
|
|
6
6
|
import { directive, type Directive } from "./directive.js";
|
|
7
7
|
import {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type ParentAttestedBirthMarker,
|
|
13
13
|
} from "./births.js";
|
|
14
14
|
import { readFrameworkDeclared } from "./read.js";
|
|
15
|
+
import type { Field } from "./fields.js";
|
|
15
16
|
import {
|
|
16
17
|
AuthProvider,
|
|
17
18
|
authProvider,
|
|
@@ -38,7 +39,7 @@ import {
|
|
|
38
39
|
birthAttestation,
|
|
39
40
|
seedBirthAttestation,
|
|
40
41
|
} from "./framework/home.js";
|
|
41
|
-
import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
|
|
42
|
+
import { RelationTuple, relationTuplesByObject, writeTuple } from "./framework/relations.js";
|
|
42
43
|
import {
|
|
43
44
|
SignerDelegation,
|
|
44
45
|
enrollDelegation,
|
|
@@ -57,6 +58,9 @@ export interface BusinessBirthMetadata {
|
|
|
57
58
|
readonly rootId: string;
|
|
58
59
|
readonly namespace: string;
|
|
59
60
|
readonly keyField: string;
|
|
61
|
+
/** Private proof/lowering policy; omitted from every generated tenant payload. */
|
|
62
|
+
readonly founderRoles: readonly string[];
|
|
63
|
+
readonly founderScope: string;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export interface BusinessBirthDirective extends Directive<Record<string, unknown>> {
|
|
@@ -104,7 +108,16 @@ function businessBirthsOf(mod: Record<string, unknown>): BusinessBirthDecl[] {
|
|
|
104
108
|
);
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
|
|
111
|
+
// A declared birth remains reusable as framework law, but once a wider business
|
|
112
|
+
// intent composes it there must not also be a generated direct-birth lane. That
|
|
113
|
+
// second lane could skip the intent's parent-workspace consequences and let two
|
|
114
|
+
// apparently valid tenant APIs create observably different worlds.
|
|
115
|
+
const applicationIntentOnlyBirths = new WeakSet<object>();
|
|
116
|
+
|
|
117
|
+
function lowerOne<P extends Record<string, unknown>, F extends Record<string, Field>>(
|
|
118
|
+
decl: BusinessBirthDecl<P, F>,
|
|
119
|
+
domain: string,
|
|
120
|
+
): Record<string, unknown> {
|
|
108
121
|
const fields = (decl.payloadSchema as z.ZodObject<z.ZodRawShape>).shape;
|
|
109
122
|
for (const field of INTERNAL_FIELDS) {
|
|
110
123
|
if (Object.prototype.hasOwnProperty.call(fields, field)) {
|
|
@@ -116,8 +129,11 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
|
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
const stem = decl.aggregate.id.replace(/[^A-Za-z0-9]/g, "");
|
|
119
|
-
const
|
|
120
|
-
const
|
|
132
|
+
const aggregateNoun = decl.aggregate.id.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
133
|
+
const namespace = (decl.namespace ?? aggregateNoun).replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
134
|
+
// Record identity follows the aggregate noun; workspace addressing follows the optional business
|
|
135
|
+
// namespace. Changing a URL/name must never silently change which aggregate row is the root.
|
|
136
|
+
const rootId = `${aggregateNoun}:self`;
|
|
121
137
|
const seedId = `nomosSeed${stem}`;
|
|
122
138
|
const birthId = `nomosBirth${stem}`;
|
|
123
139
|
const publicSchema = decl.payloadSchema as z.ZodObject<z.ZodRawShape>;
|
|
@@ -135,9 +151,42 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
|
|
|
135
151
|
const seedBase = directive(seedId)
|
|
136
152
|
.ensures(decl.aggregate)
|
|
137
153
|
.payload(seedSchema)
|
|
138
|
-
.plan((p) => {
|
|
139
|
-
decl.plan(ensure(decl.aggregate, rootId), p);
|
|
140
|
-
|
|
154
|
+
.plan((p, ctx) => {
|
|
155
|
+
const businessOps = decl.plan(ensure(decl.aggregate, rootId), p as P, ctx);
|
|
156
|
+
const founder = p.principal as string;
|
|
157
|
+
const grantedAt = p.bornAt as string;
|
|
158
|
+
return [
|
|
159
|
+
...(Array.isArray(businessOps) ? businessOps : []),
|
|
160
|
+
// Every born workspace is operable from intent zero. This is framework administration, not a
|
|
161
|
+
// tenant role, and therefore never appears in the generated application surface.
|
|
162
|
+
...writeTuple({
|
|
163
|
+
object: "workspace:self",
|
|
164
|
+
relation: "admin",
|
|
165
|
+
subject: founder,
|
|
166
|
+
grantedBy: founder,
|
|
167
|
+
grantedAt,
|
|
168
|
+
}),
|
|
169
|
+
...(decl.aggregate.audience?.kind === "owned"
|
|
170
|
+
? writeTuple({
|
|
171
|
+
object: "workspace:self",
|
|
172
|
+
relation: OWNED_READ_RELATION,
|
|
173
|
+
subject: founder,
|
|
174
|
+
grantedBy: founder,
|
|
175
|
+
grantedAt,
|
|
176
|
+
})
|
|
177
|
+
: []),
|
|
178
|
+
// Business ownership remains explicit policy. The tenant names roles; Nomos owns subjects,
|
|
179
|
+
// provenance, tuple mechanics and the atomic genesis ordering underneath them.
|
|
180
|
+
...decl.founderRoles.flatMap((relation) =>
|
|
181
|
+
writeTuple({
|
|
182
|
+
object: `${decl.founderScope}:self`,
|
|
183
|
+
relation,
|
|
184
|
+
subject: founder,
|
|
185
|
+
grantedBy: founder,
|
|
186
|
+
grantedAt,
|
|
187
|
+
}),
|
|
188
|
+
),
|
|
189
|
+
];
|
|
141
190
|
});
|
|
142
191
|
const seed = {
|
|
143
192
|
...seedBase,
|
|
@@ -149,7 +198,7 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
|
|
|
149
198
|
.payload(parentSchema)
|
|
150
199
|
.plan((p) => {
|
|
151
200
|
const founder = p.principal as string;
|
|
152
|
-
const child = workspaceNameForKey(namespace, String(decl.key(p)));
|
|
201
|
+
const child = workspaceNameForKey(namespace, String(decl.key(p as P)));
|
|
153
202
|
const childPayload = { ...(p as Record<string, unknown>) };
|
|
154
203
|
if (encryptedBirth) {
|
|
155
204
|
const rawSeed = childPayload[BUSINESS_BIRTH_SEED_PAYLOAD_FIELD];
|
|
@@ -251,8 +300,14 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
|
|
|
251
300
|
rootId,
|
|
252
301
|
namespace,
|
|
253
302
|
keyField: decl.keyField,
|
|
303
|
+
founderRoles: decl.founderRoles,
|
|
304
|
+
founderScope: decl.founderScope,
|
|
254
305
|
};
|
|
255
|
-
const parent = {
|
|
306
|
+
const parent = {
|
|
307
|
+
...birthBase,
|
|
308
|
+
businessBirth: metadata,
|
|
309
|
+
...(applicationIntentOnlyBirths.has(decl) ? { businessBirthInternal: true } : {}),
|
|
310
|
+
} as BusinessBirthDirective;
|
|
256
311
|
return {
|
|
257
312
|
[`__nomos_${birthId}`]: parent,
|
|
258
313
|
[`__nomos_${seedId}`]: seed,
|
|
@@ -295,6 +350,34 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
|
|
|
295
350
|
};
|
|
296
351
|
}
|
|
297
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Lower a declared business birth for use as one consequence of a wider business intent.
|
|
355
|
+
* The child recipe remains private; the outer intent carries the birth gate and can add
|
|
356
|
+
* same-workspace business consequences atomically.
|
|
357
|
+
*/
|
|
358
|
+
export function businessBirthConsequence<
|
|
359
|
+
P extends Record<string, unknown>,
|
|
360
|
+
F extends Record<string, Field>,
|
|
361
|
+
>(
|
|
362
|
+
decl: BusinessBirthDecl<P, F>,
|
|
363
|
+
): BusinessBirthDirective {
|
|
364
|
+
if (decl.domain === undefined) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`birth(${decl.aggregate.id}) is composed by another intent, so name its owning domain once with ` +
|
|
367
|
+
`.inDomain("...")`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
applicationIntentOnlyBirths.add(decl);
|
|
371
|
+
const lowered = Object.values(lowerOne(decl, decl.domain)).find(
|
|
372
|
+
(value): value is BusinessBirthDirective =>
|
|
373
|
+
!!value && typeof value === "object" && (value as { businessBirth?: unknown }).businessBirth !== undefined,
|
|
374
|
+
);
|
|
375
|
+
if (lowered === undefined) {
|
|
376
|
+
throw new Error(`birth(${decl.aggregate.id}): Nomos could not lower the declared business birth`);
|
|
377
|
+
}
|
|
378
|
+
return lowered;
|
|
379
|
+
}
|
|
380
|
+
|
|
298
381
|
/** Lower the one tenant-facing birth declaration into ordinary, gate-visible framework directives. */
|
|
299
382
|
export function expandBusinessBirthExports<M extends Record<string, unknown>>(
|
|
300
383
|
mod: M,
|
package/src/clear_directive.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { directive, type Directive, type RequirableDirective } from "./directive
|
|
|
29
29
|
import { instance, type AggregateHandle } from "./aggregate.js";
|
|
30
30
|
import { clear } from "./ops.js";
|
|
31
31
|
import type { Field } from "./fields.js";
|
|
32
|
+
import { idOf } from "./value_object.js";
|
|
32
33
|
|
|
33
34
|
/** Capitalize the first character (the clear-directive id joins `clear` + AggId + Field). */
|
|
34
35
|
function cap(s: string): string {
|
|
@@ -51,9 +52,10 @@ export function clearDirectiveId(aggregateId: string, field: string): string {
|
|
|
51
52
|
* `clearRequires`.
|
|
52
53
|
*/
|
|
53
54
|
export function makeClearDirective(agg: AggregateHandle, field: string, requires?: string): Directive<unknown> {
|
|
55
|
+
const payloadId = idOf(agg);
|
|
54
56
|
const built = directive(clearDirectiveId(agg.id, field))
|
|
55
57
|
.mutates(agg)
|
|
56
|
-
.payload(z.object({ id:
|
|
58
|
+
.payload(z.object({ id: payloadId, clearedAt: z.string() }))
|
|
57
59
|
.plan((p: { id: string; clearedAt: string }) => [clear(instance(agg, p.id), field as never)])
|
|
58
60
|
.doc(
|
|
59
61
|
`Explicitly clears the '${field}' field of a ${agg.id} (writes an explicit null — the declared clearable lane).`,
|