@githolon/dsl 0.32.0 → 0.34.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/lib/src/errors.dart +105 -1
- package/dart/lib/src/webview_bridge.dart +6 -4
- package/dart/test/webview_bridge_test.dart +65 -0
- package/package.json +1 -1
- package/src/aggregate.ts +12 -1
- package/src/authoring.ts +4 -1
- package/src/birth_meter.ts +300 -0
- package/src/birth_meter_ensure.ts +67 -0
- package/src/build_package.ts +59 -0
- package/src/codegen_dart.ts +347 -3
- package/src/codegen_ts.ts +74 -1
- package/src/compile_engine.ts +13 -2
- package/src/compile_package_main.ts +42 -2
- package/src/directive.ts +10 -1
- package/src/engine_entry.ts +56 -9
- package/src/fields.ts +84 -0
- package/src/framework/birth_meter.ts +125 -0
- package/src/framework/impure_capability.ts +5 -2
- package/src/framework/platform_admin.ts +4 -1
- package/src/framework/relations.ts +19 -0
- package/src/framework/workspaces.ts +42 -1
- package/src/index.ts +53 -0
- package/src/read.ts +22 -0
- package/src/read_projection_composition.ts +117 -0
- package/src/read_projection_manifest.ts +94 -0
- package/src/read_projection_namespace.ts +152 -0
- package/src/usd.ts +141 -10
- package/src/usd_layers.ts +82 -0
- package/src/value_object.ts +456 -0
- package/src/wire_encode.ts +34 -3
package/dart/lib/src/errors.dart
CHANGED
|
@@ -14,7 +14,111 @@
|
|
|
14
14
|
/// All extend [NomosBridgeError] so existing `catch (NomosBridgeError)` handlers still catch them.
|
|
15
15
|
library;
|
|
16
16
|
|
|
17
|
-
import 'webview_bridge.dart' show NomosBridgeError;
|
|
17
|
+
import 'webview_bridge.dart' show NomosBridgeError, NomosErrorInfo;
|
|
18
|
+
|
|
19
|
+
/// Mint the MOST SPECIFIC typed error for a structured refusal — the one construction point the bridge
|
|
20
|
+
/// uses everywhere it surfaces an `errorInfo`. A `nomos.custody.*` code becomes a [CustodyError] (typed,
|
|
21
|
+
/// recoverable, carrying the eviction/escalation decision fields); everything else stays the generic
|
|
22
|
+
/// [NomosBridgeError]. Apps `switch` on the type / `code`, never scrape prose.
|
|
23
|
+
NomosBridgeError nomosErrorFromInfo(NomosErrorInfo info) {
|
|
24
|
+
if (info.code.startsWith('nomos.custody.')) return CustodyError.fromInfo(info);
|
|
25
|
+
return NomosBridgeError(info.message, info: info);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// The custody phase a [CustodyError] was raised in.
|
|
29
|
+
enum CustodyPhase { fold, author, fetch, unknown }
|
|
30
|
+
|
|
31
|
+
/// Where the incomplete custody came from — the axis of the recovery decision.
|
|
32
|
+
enum CustodySource { localSnapshot, cloudPack, unknown }
|
|
33
|
+
|
|
34
|
+
/// A TYPED, RECOVERABLE CUSTODY ERROR — local custody (the clone/snapshot this holon folds from) is
|
|
35
|
+
/// missing history the operation needed (`nomos.custody.shallow_insufficient` /
|
|
36
|
+
/// `nomos.custody.missing_object`), and the in-client gap-fill repair could not complete it.
|
|
37
|
+
///
|
|
38
|
+
/// This replaces pattern-matching `shallow-insufficient` out of a stringly [NomosBridgeError]: the app
|
|
39
|
+
/// switches on [code] and decides from the structured fields whether to EVICT LOCAL CUSTODY
|
|
40
|
+
/// ([recommendedAction] == `quarantineLocalSnapshotAndReclone` — quarantine the snapshot, `inspectSnapshot`
|
|
41
|
+
/// / `recoverFrom`, reconnect fresh) or ESCALATE CLOUD CUSTODY (`refetchFullHistoryAndRetry` — the cloud
|
|
42
|
+
/// must serve the full chain). Trace IDs ride [NomosBridgeError.info.trace] as on every bridge error.
|
|
43
|
+
class CustodyError extends NomosBridgeError {
|
|
44
|
+
/// The stable machine code, e.g. `nomos.custody.shallow_insufficient`.
|
|
45
|
+
final String code;
|
|
46
|
+
|
|
47
|
+
/// The workspace whose custody is incomplete.
|
|
48
|
+
final String? workspace;
|
|
49
|
+
|
|
50
|
+
/// The ref being folded (e.g. `refs/heads/main`).
|
|
51
|
+
final String? ref;
|
|
52
|
+
|
|
53
|
+
/// The ref's head commit at the time of the fault, when resolvable.
|
|
54
|
+
final String? head;
|
|
55
|
+
|
|
56
|
+
/// The missing object id the fold stopped at, when the kernel named one.
|
|
57
|
+
final String? missingObject;
|
|
58
|
+
|
|
59
|
+
/// The operation phase that hit the gap: fold (read), author (offer), fetch (the repair itself).
|
|
60
|
+
final CustodyPhase phase;
|
|
61
|
+
|
|
62
|
+
/// Provenance of the incomplete custody: a restored local snapshot, or a cloud-served pack.
|
|
63
|
+
final CustodySource source;
|
|
64
|
+
|
|
65
|
+
/// True when a known recovery lane exists (it always does for custody gaps — see [recommendedAction]).
|
|
66
|
+
final bool recoverable;
|
|
67
|
+
|
|
68
|
+
/// The named remedy: `quarantineLocalSnapshotAndReclone` | `refetchFullHistoryAndRetry`.
|
|
69
|
+
final String recommendedAction;
|
|
70
|
+
|
|
71
|
+
CustodyError({
|
|
72
|
+
required this.code,
|
|
73
|
+
required String message,
|
|
74
|
+
this.workspace,
|
|
75
|
+
this.ref,
|
|
76
|
+
this.head,
|
|
77
|
+
this.missingObject,
|
|
78
|
+
this.phase = CustodyPhase.unknown,
|
|
79
|
+
this.source = CustodySource.unknown,
|
|
80
|
+
this.recoverable = true,
|
|
81
|
+
this.recommendedAction = 'refetchFullHistoryAndRetry',
|
|
82
|
+
NomosErrorInfo? info,
|
|
83
|
+
}) : super(message, info: info);
|
|
84
|
+
|
|
85
|
+
/// Decode the structured custody payload the client attaches (`nomos.custody.*` attributes).
|
|
86
|
+
factory CustodyError.fromInfo(NomosErrorInfo info) {
|
|
87
|
+
String? attr(String name) {
|
|
88
|
+
final v = info.attributes['nomos.custody.$name'] ?? info.attributes[name];
|
|
89
|
+
return v?.toString();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return CustodyError(
|
|
93
|
+
code: info.code,
|
|
94
|
+
message: info.message,
|
|
95
|
+
workspace: info.workspace,
|
|
96
|
+
ref: attr('ref'),
|
|
97
|
+
head: attr('head'),
|
|
98
|
+
missingObject: attr('missing_object') ?? attr('missingObject'),
|
|
99
|
+
phase: CustodyPhase.values.firstWhere(
|
|
100
|
+
(p) => p.name == attr('phase'),
|
|
101
|
+
orElse: () => CustodyPhase.unknown,
|
|
102
|
+
),
|
|
103
|
+
source: CustodySource.values.firstWhere(
|
|
104
|
+
(s) => s.name == attr('source'),
|
|
105
|
+
orElse: () => CustodySource.unknown,
|
|
106
|
+
),
|
|
107
|
+
recoverable: attr('recoverable') != 'false',
|
|
108
|
+
recommendedAction: attr('recommended_action') ?? attr('recommendedAction') ?? 'refetchFullHistoryAndRetry',
|
|
109
|
+
info: info,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// True when the remedy is to quarantine the local snapshot and re-clone (evict local custody).
|
|
114
|
+
bool get shouldEvictLocalCustody => recommendedAction == 'quarantineLocalSnapshotAndReclone';
|
|
115
|
+
|
|
116
|
+
@override
|
|
117
|
+
String toString() =>
|
|
118
|
+
'CustodyError($code${workspace != null ? ', workspace: $workspace' : ''}'
|
|
119
|
+
'${ref != null ? ', ref: $ref' : ''}${missingObject != null ? ', missing: $missingObject' : ''}'
|
|
120
|
+
', phase: ${phase.name}, source: ${source.name}, recommendedAction: $recommendedAction): $message';
|
|
121
|
+
}
|
|
18
122
|
|
|
19
123
|
/// The auth-provider access token the cloud was given was missing, malformed, expired, or rejected — the
|
|
20
124
|
/// cloud answered 401 / "invalid token" on a lane that requires a verified principal (every birth does).
|
|
@@ -17,6 +17,8 @@ library;
|
|
|
17
17
|
import 'dart:async';
|
|
18
18
|
import 'dart:math';
|
|
19
19
|
|
|
20
|
+
import 'errors.dart' show nomosErrorFromInfo;
|
|
21
|
+
|
|
20
22
|
/// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
|
|
21
23
|
const int kWebviewBridgeVersion = 1;
|
|
22
24
|
|
|
@@ -336,7 +338,7 @@ class NomosBridge {
|
|
|
336
338
|
final info = NomosErrorInfo.fromBridgeMessage(m,
|
|
337
339
|
fallbackMessage: 'the watch read failed',
|
|
338
340
|
fallbackOperation: 'watch');
|
|
339
|
-
ctrl.addError(
|
|
341
|
+
ctrl.addError(nomosErrorFromInfo(info));
|
|
340
342
|
} else {
|
|
341
343
|
ctrl.add((m['rows'] as List?) ?? const []);
|
|
342
344
|
}
|
|
@@ -349,7 +351,7 @@ class NomosBridge {
|
|
|
349
351
|
final info = NomosErrorInfo.fromBridgeMessage(m,
|
|
350
352
|
fallbackMessage: 'the compat watch failed',
|
|
351
353
|
fallbackOperation: 'watchCompatibility');
|
|
352
|
-
cctrl.addError(
|
|
354
|
+
cctrl.addError(nomosErrorFromInfo(info));
|
|
353
355
|
} else {
|
|
354
356
|
cctrl.add(((m['rows'] as Map?)?.cast<String, Object?>()) ?? const {});
|
|
355
357
|
}
|
|
@@ -410,7 +412,7 @@ class NomosBridge {
|
|
|
410
412
|
fallbackOperation: op,
|
|
411
413
|
fallbackTrace: trace,
|
|
412
414
|
fallbackMessage: 'the githolon refused the request');
|
|
413
|
-
completer.completeError(
|
|
415
|
+
completer.completeError(nomosErrorFromInfo(info));
|
|
414
416
|
}
|
|
415
417
|
}
|
|
416
418
|
|
|
@@ -931,7 +933,7 @@ class NomosBridge {
|
|
|
931
933
|
code: 'nomos.bridge.disposed',
|
|
932
934
|
operation: _pendingOps[entry.key],
|
|
933
935
|
trace: trace);
|
|
934
|
-
c.completeError(
|
|
936
|
+
c.completeError(nomosErrorFromInfo(info));
|
|
935
937
|
}
|
|
936
938
|
}
|
|
937
939
|
_pending.clear();
|
|
@@ -61,6 +61,36 @@ class FakeRunner implements BridgeTransport {
|
|
|
61
61
|
'attributes': {'test.case': 'refusal'}
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
|
+
} else if (intentJson.contains('"directiveId":"custodyGap"')) {
|
|
65
|
+
// The client's typed custody error: structured errorInfo, code=nomos.custody.*.
|
|
66
|
+
_incoming.add({
|
|
67
|
+
'v': kWebviewBridgeVersion,
|
|
68
|
+
'id': id,
|
|
69
|
+
'ok': false,
|
|
70
|
+
'error':
|
|
71
|
+
'shallow-insufficient: fold refs/heads/main: find commit: NotFound',
|
|
72
|
+
'errorInfo': {
|
|
73
|
+
'code': 'nomos.custody.shallow_insufficient',
|
|
74
|
+
'message':
|
|
75
|
+
'shallow-insufficient: fold refs/heads/main: find commit: NotFound',
|
|
76
|
+
'component': 'nomos.custody',
|
|
77
|
+
'operation': 'offer',
|
|
78
|
+
'workspace': 'ws-1',
|
|
79
|
+
'trace': message['trace'],
|
|
80
|
+
'attributes': {
|
|
81
|
+
'nomos.custody.ref': 'refs/heads/main',
|
|
82
|
+
'nomos.custody.head':
|
|
83
|
+
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
84
|
+
'nomos.custody.missing_object':
|
|
85
|
+
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
86
|
+
'nomos.custody.phase': 'author',
|
|
87
|
+
'nomos.custody.source': 'localSnapshot',
|
|
88
|
+
'nomos.custody.recoverable': true,
|
|
89
|
+
'nomos.custody.recommended_action':
|
|
90
|
+
'quarantineLocalSnapshotAndReclone',
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
64
94
|
} else {
|
|
65
95
|
_incoming.add(reply({'head': 'HEAD-offer'}));
|
|
66
96
|
}
|
|
@@ -420,6 +450,41 @@ void main() {
|
|
|
420
450
|
expect(e.info.toTelemetryAttributes()['nomos.operation'], 'offer');
|
|
421
451
|
});
|
|
422
452
|
|
|
453
|
+
test(
|
|
454
|
+
'a custody fault surfaces as the TYPED CustodyError (switch on code, never scrape prose)',
|
|
455
|
+
() async {
|
|
456
|
+
await bridge.ready;
|
|
457
|
+
Object? err;
|
|
458
|
+
try {
|
|
459
|
+
await bridge.offer(
|
|
460
|
+
intentBytes: base64Encode(utf8.encode(jsonEncode({
|
|
461
|
+
'v': 2,
|
|
462
|
+
'intent': {
|
|
463
|
+
'domain': 'x',
|
|
464
|
+
'directiveId': 'custodyGap',
|
|
465
|
+
'payload': {},
|
|
466
|
+
'domainHash': 'h'
|
|
467
|
+
}
|
|
468
|
+
}))));
|
|
469
|
+
} on NomosBridgeError catch (e) {
|
|
470
|
+
err = e; // still catchable as the generic bridge error
|
|
471
|
+
}
|
|
472
|
+
expect(err, isA<CustodyError>());
|
|
473
|
+
final e = err as CustodyError;
|
|
474
|
+
expect(e.code, 'nomos.custody.shallow_insufficient');
|
|
475
|
+
expect(e.workspace, 'ws-1');
|
|
476
|
+
expect(e.ref, 'refs/heads/main');
|
|
477
|
+
expect(e.head, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
478
|
+
expect(e.missingObject, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb');
|
|
479
|
+
expect(e.phase, CustodyPhase.author);
|
|
480
|
+
expect(e.source, CustodySource.localSnapshot);
|
|
481
|
+
expect(e.recoverable, isTrue);
|
|
482
|
+
expect(e.recommendedAction, 'quarantineLocalSnapshotAndReclone');
|
|
483
|
+
expect(e.shouldEvictLocalCustody, isTrue);
|
|
484
|
+
expect(e.info.trace.traceId, isNotEmpty); // trace IDs ride as on every bridge error
|
|
485
|
+
expect(e.message, contains('shallow-insufficient'));
|
|
486
|
+
});
|
|
487
|
+
|
|
423
488
|
test(
|
|
424
489
|
'legacy string-only runner errors still get operation and trace metadata',
|
|
425
490
|
() async {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/aggregate.ts
CHANGED
|
@@ -166,6 +166,16 @@ export interface AggregateHandle<
|
|
|
166
166
|
* `readModelAggregates`). Identified by OBJECT IDENTITY (the branded singleton), so a tenant
|
|
167
167
|
* aggregate that merely SHARES a name is never misclassified. OMITTED on tenant aggregates. */
|
|
168
168
|
readonly framework?: true;
|
|
169
|
+
/**
|
|
170
|
+
* READ-PROJECTION NAMESPACING opt-out (`read_projection_namespace.ts`). TRUE ⇒ this
|
|
171
|
+
* aggregate's type name stays GLOBAL (bare, un-namespaced) on every surface — for
|
|
172
|
+
* KERNEL-COUPLED framework aggregates whose bare name/instance-id convention the
|
|
173
|
+
* kernel hardcodes. Statically-named framework aggregates ride the explicit
|
|
174
|
+
* allowlist instead; this flag is the DYNAMICALLY-NAMED factory lane (e.g.
|
|
175
|
+
* `platformAdmin()`'s founder aggregate, impure-capability envelopes). NOT hashed
|
|
176
|
+
* itself — its EFFECT (the bare vs qualified name) is what the law carries.
|
|
177
|
+
*/
|
|
178
|
+
readonly global?: boolean;
|
|
169
179
|
/** Presence-only flag (hashed); OMITTED ENTIRELY when no invariant is declared. */
|
|
170
180
|
readonly hasInvariant?: true;
|
|
171
181
|
/** The executable body — ships in the engine bundle, NOT the manifest. */
|
|
@@ -230,7 +240,7 @@ export interface AudienceMethods<Id extends string, F extends Record<string, Fie
|
|
|
230
240
|
export function aggregate<const Id extends string, F extends Record<string, Field>>(
|
|
231
241
|
id: Id,
|
|
232
242
|
fields: F,
|
|
233
|
-
opts?: { invariant?: AggregateInvariantFn; invariantDeclassify?: string; cap?: number; visibility?: ReadVisibility },
|
|
243
|
+
opts?: { invariant?: AggregateInvariantFn; invariantDeclassify?: string; cap?: number; visibility?: ReadVisibility; global?: boolean },
|
|
234
244
|
): AggregateHandle<Id, F> & AudienceMethods<Id, F> {
|
|
235
245
|
const { stored, hasMany } = partitionFields(fields);
|
|
236
246
|
if (opts?.cap !== undefined && (!Number.isInteger(opts.cap) || opts.cap < 1)) {
|
|
@@ -279,6 +289,7 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
279
289
|
: {}),
|
|
280
290
|
...(opts?.invariantDeclassify !== undefined ? { invariantDeclassify: opts.invariantDeclassify } : {}),
|
|
281
291
|
...(opts?.cap !== undefined ? { cap: opts.cap } : {}),
|
|
292
|
+
...(opts?.global === true ? { global: true } : {}),
|
|
282
293
|
...(encryptedFields.length > 0 ? { encryptedFields } : {}),
|
|
283
294
|
...(metaFields.length > 0 ? { metaFields } : {}),
|
|
284
295
|
};
|
package/src/authoring.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type { AggregateHandle } from "./aggregate.js";
|
|
|
27
27
|
import type { Field } from "./fields.js";
|
|
28
28
|
import type { FieldOp, PlannedOp, PlannedEventMarker } from "./ops.js";
|
|
29
29
|
import { endorsementOf } from "./principal_evidence.js";
|
|
30
|
+
import { __qualifyAggregateType } from "./framework/relations.js";
|
|
30
31
|
|
|
31
32
|
// ── the per-dispatch authoring sink — library state the engine never sees (NOT a side channel out of
|
|
32
33
|
// the sandbox; it is internal to the directive runtime, drained synchronously by `executeDirectiveToIntent`). ──
|
|
@@ -121,7 +122,9 @@ class Ref implements AggregateRef {
|
|
|
121
122
|
|
|
122
123
|
/** Ask Nomos to CREATE a new aggregate of the handle's type → a fluent reference (id minted, no choice). */
|
|
123
124
|
export function create(agg: AggregateHandle): AggregateRef {
|
|
124
|
-
|
|
125
|
+
// READ-PROJECTION NAMESPACING: mint under the QUALIFIED type tag — the kernel's id-mint gate
|
|
126
|
+
// verifies a Create's id was minted for EXACTLY the declared (qualified) `__type`.
|
|
127
|
+
return new Ref(agg, mint(__qualifyAggregateType(agg.id)), "creates");
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
/** A reference to an EXISTING aggregate the dev already holds an id for (e.g. from the payload / a read). */
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE BIRTH METER (metered birth, slice A — `architecture/metered_birth_and_attested_reads.md`).
|
|
8
|
+
*
|
|
9
|
+
* THE ONE-HOP VOUCHING PRINCIPLE: trust is established BY INDUCTION AT BIRTH and verified LOCALLY —
|
|
10
|
+
* a parent vouches for its child IN ITS OWN CHAIN; nobody walks to root. The parent's own ledger
|
|
11
|
+
* already witnesses every birth it performs (the admitted `.births()` intent is IN the parent's
|
|
12
|
+
* chain), so the count of a parent's children is a PARENT-LOCAL fact and the existing
|
|
13
|
+
* same-workspace #58 captured-read lane covers it. NO cross-workspace reads anywhere in this slice.
|
|
14
|
+
*
|
|
15
|
+
* Three structural pieces, all era-ruled by the bundle (a pre-feature bundle carries none of this
|
|
16
|
+
* and replays byte-identical; a bundle recompiled under this DSL carries all of it by construction):
|
|
17
|
+
*
|
|
18
|
+
* A1 — THE BIRTH RECORD IS STRUCTURAL. Every `.births()` directive's intent ALSO writes a
|
|
19
|
+
* {@link NomosBirthRecord} event (one deterministic-id record per child; a re-birth of the
|
|
20
|
+
* same name UPDATES the record — Lww) in the SAME intent as the birth effect. The library
|
|
21
|
+
* appends it in `executeDirectiveToIntent` (the ONE plan-output assembly BOTH lanes — client
|
|
22
|
+
* authoring and the sealed engine — funnel through), so tenant law gets it by construction,
|
|
23
|
+
* zero tenant code. The kernel arm (`verify_births_witness` + the `nomosBirthRecordGate` era
|
|
24
|
+
* key) REFUSES a birth whose intent carries no record — ONLY for bundles declaring the key.
|
|
25
|
+
*
|
|
26
|
+
* A2 — THE BIRTH QUOTA IS PARENT-LOCAL LAW. The `.births()` lane auto-captures
|
|
27
|
+
* {@link birthRecordsByPrincipal} + the parent's allowance facts (the #58 captured-read lane,
|
|
28
|
+
* CAS-re-derived on every lane), and a synthesized framework `workspaceInvariant` (the exact
|
|
29
|
+
* WorkspaceQuota shape) refuses `birth-quota-exceeded` once the subject already holds its
|
|
30
|
+
* allowance. Allowance resolution: the latest {@link BirthGrant} for the subject if any, else
|
|
31
|
+
* the workspace's {@link BirthPolicy} singleton, else {@link DEFAULT_MAX_BIRTHS_PER_PRINCIPAL}.
|
|
32
|
+
*
|
|
33
|
+
* A3 — THE POOL CHAINS TO ROOT. A parent births a child WITH the child's own descendant budget
|
|
34
|
+
* (`buildGenesisChain`'s `descendantPool` threads a `seedBirthPolicy` genesis step), so every
|
|
35
|
+
* child is BORN BOUNDED: root bounds platforms, platforms bound homes, homes bound estates —
|
|
36
|
+
* the tree meters by induction. V1 BOUNDARY (deliberate): each node enforces ITS OWN
|
|
37
|
+
* count-quota only; cross-level pool CONSERVATION arithmetic (a child may not grant more than
|
|
38
|
+
* it holds) is v2 — stated in the design doc, not silently pretended here.
|
|
39
|
+
*
|
|
40
|
+
* THE METER SUBJECT: the payload-carried birth subject (`principal`, else `owner` — the SAME
|
|
41
|
+
* convention `birthChild`/`birthHome` already carry), a pure function of the committed payload so
|
|
42
|
+
* it is identical on author / edge admission / verify_chain. A `.births()` payload carrying NEITHER
|
|
43
|
+
* fails CLOSED (`birth-quota-missing-subject`) — un-meterable ⇒ un-born, the ratified doctrine.
|
|
44
|
+
*/
|
|
45
|
+
import { aggregate, instance, type AggregateHandle } from "./aggregate.js";
|
|
46
|
+
import { t } from "./fields.js";
|
|
47
|
+
import { Lww } from "./drivers.js";
|
|
48
|
+
import { query, type QueryDecl } from "./query.js";
|
|
49
|
+
import { __captureBirthMeterRead } from "./read.js";
|
|
50
|
+
import { set, withMarker, type PlannedOp } from "./ops.js";
|
|
51
|
+
import type { WorkspaceInvariantDecl, WorkspaceInvariantVerdict } from "./framework/workspace_invariant.js";
|
|
52
|
+
import type { BirthRecipe } from "./births.js";
|
|
53
|
+
|
|
54
|
+
// ── A1 — the structural birth record ───────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/** The deterministic instance-id prefix — `nomos-birth-record:<child>` (one record per child name;
|
|
57
|
+
* a re-birth of the same name updates the SAME record, Lww). The kernel's `nomosBirthRecordGate`
|
|
58
|
+
* era key carries this prefix so the wasm arm reads it from LAW, never hardcodes it. */
|
|
59
|
+
export const NOMOS_BIRTH_RECORD_PREFIX = "nomos-birth-record:";
|
|
60
|
+
|
|
61
|
+
/** The birth record's deterministic aggregate instance id for a child workspace name. */
|
|
62
|
+
export function birthRecordId(child: string): string {
|
|
63
|
+
return NOMOS_BIRTH_RECORD_PREFIX + child;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* NomosBirthRecord — THE STRUCTURAL BIRTH RECORD (A1). One row per child this workspace has
|
|
68
|
+
* birthed, written AUTOMATICALLY in the same intent as the birth effect (the `.births()` lane).
|
|
69
|
+
* The framework twin of `DomainInstallation`: a kernel-recognized, law-folded lifecycle fact.
|
|
70
|
+
*/
|
|
71
|
+
export const NomosBirthRecord = aggregate("NomosBirthRecord", {
|
|
72
|
+
/** The child workspace name the birth effect spawned. */
|
|
73
|
+
child: t.string().merge(Lww),
|
|
74
|
+
/** The birth SUBJECT — the payload-carried `principal`/`owner` the quota meters (see banner). */
|
|
75
|
+
principal: t.string().merge(Lww),
|
|
76
|
+
/** sha256 of the child's genesis chain JSON (the recipe the offer carried — content-addressed lineage). */
|
|
77
|
+
childGenesisHash: t.string().merge(Lww),
|
|
78
|
+
/** Caller-stamped birth time from the payload (`bornAt`, else `requestedAt`) — the timestamp doctrine. */
|
|
79
|
+
bornAt: t.string().merge(Lww),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/** BirthPolicy — the workspace's birth-allowance singleton (id `birth-policy:workspace`), the
|
|
83
|
+
* `CloudPolicy` shape one level down: the default max live births per subject in THIS workspace. */
|
|
84
|
+
export const BIRTH_POLICY_ID = "birth-policy:workspace";
|
|
85
|
+
export const BirthPolicy = aggregate("BirthPolicy", {
|
|
86
|
+
/** The query key (constant `"workspace"`) — a declared field so `birthPolicy` can index it. */
|
|
87
|
+
scope: t.string().merge(Lww),
|
|
88
|
+
maxBirthsPerPrincipal: t.int().merge(Lww),
|
|
89
|
+
setBy: t.string().merge(Lww),
|
|
90
|
+
/** Caller-stamped (ISO or epoch-ms string) — the timestamp doctrine. */
|
|
91
|
+
setAt: t.string().merge(Lww),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** BirthGrant — a per-subject allowance override (id `birth-grant:<principal>`), the `QuotaGrant`
|
|
95
|
+
* shape: the LATEST grant (max grantedAt) for a subject is the allowance in force. */
|
|
96
|
+
export function birthGrantId(principal: string): string {
|
|
97
|
+
return "birth-grant:" + principal;
|
|
98
|
+
}
|
|
99
|
+
export const BirthGrant = aggregate("BirthGrant", {
|
|
100
|
+
principal: t.string().merge(Lww),
|
|
101
|
+
maxBirths: t.int().merge(Lww),
|
|
102
|
+
grantedBy: t.string().merge(Lww),
|
|
103
|
+
/** epoch-ms — the recency tiebreak (latest grant wins), caller-stamped. */
|
|
104
|
+
grantedAt: t.int().merge(Lww),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── the meter's declared queries (the #58 captured-read recipes) ────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/** A subject's birth records — the quota gate counts them (parent-LOCAL; no cross-ws read). */
|
|
110
|
+
export const birthRecordsByPrincipal = query("birthRecordsByPrincipal")
|
|
111
|
+
.key("principal")
|
|
112
|
+
.returns(NomosBirthRecord);
|
|
113
|
+
|
|
114
|
+
/** The workspace's birth-policy singleton (`scope: "workspace"`). */
|
|
115
|
+
export const birthPolicy = query("birthPolicy").key("scope").returns(BirthPolicy);
|
|
116
|
+
|
|
117
|
+
/** A subject's allowance grants — the LATEST one overrides the BirthPolicy default. */
|
|
118
|
+
export const birthGrantByPrincipal = query("birthGrantByPrincipal")
|
|
119
|
+
.key("principal")
|
|
120
|
+
.returns(BirthGrant);
|
|
121
|
+
|
|
122
|
+
/** The three meter reads every `.births()` directive auto-declares (`nomosReadGate`). */
|
|
123
|
+
export const BIRTH_METER_QUERIES: readonly QueryDecl[] = [
|
|
124
|
+
birthRecordsByPrincipal,
|
|
125
|
+
birthPolicy,
|
|
126
|
+
birthGrantByPrincipal,
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* THE DEFAULT BIRTH ALLOWANCE when a workspace carries neither a BirthPolicy singleton nor a grant
|
|
131
|
+
* for the subject: a CONSERVATIVE floor for non-root workspaces (a home births a handful of estate
|
|
132
|
+
* children; a platform that needs more seeds a BirthPolicy at its own genesis — the A3 induction —
|
|
133
|
+
* or grants per-subject allowances). Root/platform governance already carries the 50-workspace
|
|
134
|
+
* `WorkspaceQuota`; THIS default only ever binds law that never set its own policy.
|
|
135
|
+
*/
|
|
136
|
+
export const DEFAULT_MAX_BIRTHS_PER_PRINCIPAL = 5;
|
|
137
|
+
|
|
138
|
+
// ── the meter subject + the auto-record/auto-capture halves the `.births()` lane calls ─────────────
|
|
139
|
+
|
|
140
|
+
/** The payload-carried birth SUBJECT the meter counts under: `principal`, else `owner`, else `""`
|
|
141
|
+
* (fail-closed at the invariant). A pure function of the committed payload — identical on every lane. */
|
|
142
|
+
export function birthMeterSubject(payload: Record<string, unknown>): string {
|
|
143
|
+
// The payload-carried subject convention, in priority order: `principal` (birthChild/createWorkspace),
|
|
144
|
+
// `owner` (birthHome), `ownerSubject` (the estate-birth shape). A pure function of the committed
|
|
145
|
+
// payload — identical on every lane; empty ⇒ the quota invariant fails closed.
|
|
146
|
+
for (const key of ["principal", "owner", "ownerSubject"]) {
|
|
147
|
+
const v = payload[key];
|
|
148
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
149
|
+
}
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The record's caller-stamped birth time off the payload (`bornAt`, else `requestedAt`, else `""`). */
|
|
154
|
+
export function birthMeterBornAt(payload: Record<string, unknown>): string {
|
|
155
|
+
const b = payload["bornAt"];
|
|
156
|
+
if (typeof b === "string" && b.length > 0) return b;
|
|
157
|
+
const r = payload["requestedAt"];
|
|
158
|
+
if (typeof r === "number" || (typeof r === "string" && r.length > 0)) return String(r);
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** sha256 via the ONE deterministic host port (`nomos.sha256` — the same global `homeNameOf` uses,
|
|
163
|
+
* provided by the sealed engine AND the client runtime, so both lanes hash identically). */
|
|
164
|
+
function sha256(text: string): string {
|
|
165
|
+
const host = (globalThis as { nomos?: { sha256?: (s: string) => string } }).nomos;
|
|
166
|
+
if (host === undefined || typeof host.sha256 !== "function") {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"birth-meter: the `nomos.sha256` host port is not available — a `.births()` dispatch needs the " +
|
|
169
|
+
"engine/client runtime (tests: stub `globalThis.nomos.sha256`).",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return host.sha256(text);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* CAPTURE the three meter reads on the write path (A2 — the plan side of the birth-quota gate; the
|
|
177
|
+
* exact `countQuotaReads` shape). Called by `executeDirectiveToIntent` for every `.births()` dispatch
|
|
178
|
+
* that planned ≥1 birth — the values ride `captured_ports.reads`, CAS-re-derived + byte-compared at
|
|
179
|
+
* the pre-apply position on every lane; the verdict is the synthesized invariant's.
|
|
180
|
+
*/
|
|
181
|
+
export function captureBirthMeterReads(subject: string): void {
|
|
182
|
+
__captureBirthMeterRead(birthRecordsByPrincipal, { principal: subject });
|
|
183
|
+
__captureBirthMeterRead(birthGrantByPrincipal, { principal: subject });
|
|
184
|
+
__captureBirthMeterRead(birthPolicy, { scope: "workspace" });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* THE AUTO-RECORD (A1): the `NomosBirthRecord` field ops for ONE planned birth, appended by
|
|
189
|
+
* `executeDirectiveToIntent` to the SAME intent that carries the birth effect. Deterministic-id
|
|
190
|
+
* upsert (Lww — a re-birth of the same child name updates the record), a fan-out sibling event
|
|
191
|
+
* exactly like `installDomain`'s `DomainInstallation` writes.
|
|
192
|
+
*/
|
|
193
|
+
export function planBirthRecordOps(
|
|
194
|
+
recipe: BirthRecipe,
|
|
195
|
+
subject: string,
|
|
196
|
+
bornAt: string,
|
|
197
|
+
): PlannedOp[] {
|
|
198
|
+
const rec = instance(NomosBirthRecord, birthRecordId(recipe.workspace));
|
|
199
|
+
return [
|
|
200
|
+
// ENSURE, not a bare update: the record id is deterministic and the FIRST birth of a child writes a
|
|
201
|
+
// row that does not yet exist — the referential gate refuses a Mutate of a non-existent id, so the
|
|
202
|
+
// event must lower `Ensure` (create-or-update), exactly how `installDomain` lowers its
|
|
203
|
+
// DomainInstallation singleton (`withMarker(..., "ensures")`). One marked op sets the whole event's
|
|
204
|
+
// marker; both lanes share THIS function, so the op shape is byte-identical by construction.
|
|
205
|
+
withMarker(set(rec, "child", recipe.workspace), "ensures"),
|
|
206
|
+
set(rec, "principal", subject),
|
|
207
|
+
set(rec, "childGenesisHash", sha256(JSON.stringify(recipe.genesisChain))),
|
|
208
|
+
set(rec, "bornAt", bornAt),
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── A2 — the quota verdict + the synthesized per-directive invariant ────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/** A captured-read row as `derive_captured_read` shapes it: `{id, data:{field:value}}`. */
|
|
215
|
+
type MeterRow = { id?: string; data?: Record<string, unknown> };
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* THE BIRTH-QUOTA VERDICT — the PURE predicate over the COMMITTED captured reads (the same
|
|
219
|
+
* `{queryId,args,result}` the plan recorded, CAS-re-derived on every lane). Counts the subject's
|
|
220
|
+
* birth records and compares against its allowance (the latest BirthGrant.maxBirths if any, else
|
|
221
|
+
* BirthPolicy.maxBirthsPerPrincipal, else {@link DEFAULT_MAX_BIRTHS_PER_PRINCIPAL}). The NEW birth's
|
|
222
|
+
* record is NOT yet in the pre-apply state, so `count < allowance` (strict) is the correct test. A
|
|
223
|
+
* subject-less birth fails CLOSED. Identical on author, edge admission, and verify_chain.
|
|
224
|
+
*/
|
|
225
|
+
export function birthQuotaVerdict(
|
|
226
|
+
reads: readonly { queryId: string; args: Record<string, string>; result: unknown }[],
|
|
227
|
+
subject: string,
|
|
228
|
+
): WorkspaceInvariantVerdict {
|
|
229
|
+
if (!subject) return { reject: "birth-quota-missing-subject" };
|
|
230
|
+
const rowsOf = (queryId: string, argKey: string, argValue: string): MeterRow[] => {
|
|
231
|
+
const hit = reads.find((r) => r.queryId === queryId && r.args[argKey] === argValue);
|
|
232
|
+
return Array.isArray(hit?.result) ? (hit!.result as MeterRow[]) : [];
|
|
233
|
+
};
|
|
234
|
+
const count = rowsOf(birthRecordsByPrincipal.id, "principal", subject).length;
|
|
235
|
+
// ALLOWANCE: the LATEST BirthGrant (max grantedAt) wins; else the BirthPolicy singleton; else the floor.
|
|
236
|
+
let allowance: number | undefined;
|
|
237
|
+
let latestGrantedAt = -Infinity;
|
|
238
|
+
for (const g of rowsOf(birthGrantByPrincipal.id, "principal", subject)) {
|
|
239
|
+
const at = Number(g.data?.grantedAt ?? -Infinity);
|
|
240
|
+
const max = Number(g.data?.maxBirths ?? NaN);
|
|
241
|
+
if (Number.isFinite(at) && at > latestGrantedAt && Number.isFinite(max)) {
|
|
242
|
+
latestGrantedAt = at;
|
|
243
|
+
allowance = max;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (allowance === undefined) {
|
|
247
|
+
const policyRows = rowsOf(birthPolicy.id, "scope", "workspace");
|
|
248
|
+
const max = Number(policyRows[0]?.data?.maxBirthsPerPrincipal ?? NaN);
|
|
249
|
+
if (Number.isFinite(max)) allowance = max;
|
|
250
|
+
}
|
|
251
|
+
if (allowance === undefined) allowance = DEFAULT_MAX_BIRTHS_PER_PRINCIPAL;
|
|
252
|
+
return count < allowance ? { accept: true } : { reject: "birth-quota-exceeded" };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The synthesized invariant's stable, deterministic id for a `.births()` directive. */
|
|
256
|
+
export function birthQuotaInvariantId(directiveId: string): string {
|
|
257
|
+
return `${directiveId}__birthQuota`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Synthesize THE BIRTH-QUOTA workspace invariant for ONE directive (A2). `[]` unless the directive
|
|
262
|
+
* declares `.births()` (the era rule — a birth-free directive synthesizes nothing, byte-identical).
|
|
263
|
+
* Applied at the SAME three sites the precondition desugaring rides (`composeDomainModule`, the
|
|
264
|
+
* engine slice build, the openapi decls) so the manifest's `nomosInvariantGate` entry and the sealed
|
|
265
|
+
* engine's executable body can never disagree. The verdict reads the row LIST off the captured reads
|
|
266
|
+
* (`ctx.reads` — the snapshot ref-set is empty; ref-resolution cannot enumerate), the exact
|
|
267
|
+
* WorkspaceQuota shape.
|
|
268
|
+
*/
|
|
269
|
+
export function synthesizeBirthQuotaInvariants(directive: {
|
|
270
|
+
id: string;
|
|
271
|
+
birthsChild?: boolean;
|
|
272
|
+
}): WorkspaceInvariantDecl[] {
|
|
273
|
+
if (directive.birthsChild !== true) return [];
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
id: birthQuotaInvariantId(directive.id),
|
|
277
|
+
on: directive.id,
|
|
278
|
+
reads: () => [],
|
|
279
|
+
assert: (_snapshots, ctx) => {
|
|
280
|
+
// A `.births()` plan that planned NO birth (e.g. birthChild without a genesis) captures no
|
|
281
|
+
// meter reads and consumes no allowance — accept without judging (nothing was born).
|
|
282
|
+
const reads = (ctx?.reads ?? []) as {
|
|
283
|
+
queryId: string;
|
|
284
|
+
args: Record<string, string>;
|
|
285
|
+
result: unknown;
|
|
286
|
+
}[];
|
|
287
|
+
if (!reads.some((r) => r.queryId === birthRecordsByPrincipal.id)) return { accept: true };
|
|
288
|
+
return birthQuotaVerdict(reads, birthMeterSubject(ctx?.intent ?? {}));
|
|
289
|
+
},
|
|
290
|
+
__isWorkspaceInvariant: true as const,
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The meter aggregates every `.births()`-declaring domain auto-ensures (the RelationTuple courtesy). */
|
|
296
|
+
export const BIRTH_METER_AGGREGATES: readonly AggregateHandle[] = [
|
|
297
|
+
NomosBirthRecord,
|
|
298
|
+
BirthPolicy,
|
|
299
|
+
BirthGrant,
|
|
300
|
+
] as unknown as readonly AggregateHandle[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE BIRTH-RECORD SANCTIONING DECLARATION (metered birth, slice A1 — the id-mint-gate half).
|
|
8
|
+
*
|
|
9
|
+
* The auto-appended `NomosBirthRecord` event is an ENSURE at a deterministic id
|
|
10
|
+
* (`nomos-birth-record:<child>`). The kernel's id-mint gate admits an Ensure-INTRODUCED typed id
|
|
11
|
+
* ONLY through the sanctioned upsert-by-deterministic-key lane: the CERTIFIED manifest must declare
|
|
12
|
+
* the type as an `ensures` TARGET — i.e. the law must carry a directive whose marker is `Ensure`
|
|
13
|
+
* targeting `NomosBirthRecord` (exactly how `installDomain` sanctions `DomainInstallation`, or the
|
|
14
|
+
* governance `setBirthPolicy` sanctions `BirthPolicy`). A tenant `.births()` law writes the record
|
|
15
|
+
* by construction but authors NO such directive — so this module supplies it, injected at the ONE
|
|
16
|
+
* module-merge seam every plane shares (compose / engine slice / self-describe / compile openapi),
|
|
17
|
+
* keeping the compiled openapi and the lump's `describe()` byte-consistent.
|
|
18
|
+
*
|
|
19
|
+
* {@link nomosBirthRecordEnsure} is DELIBERATELY USELESS TO DISPATCH: its plan writes NOTHING (zero
|
|
20
|
+
* ops — it cannot forge a record), it exists purely so the law's manifest/USD carries the
|
|
21
|
+
* `Ensure → NomosBirthRecord` declaration the id-mint gate reads. Era-ruled like everything else in
|
|
22
|
+
* the meter: only a `.births()`-declaring domain gains it; every other law is byte-identical.
|
|
23
|
+
*/
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import { directive } from "./directive.js";
|
|
26
|
+
import type { Directive } from "./directive.js";
|
|
27
|
+
import {
|
|
28
|
+
NomosBirthRecord,
|
|
29
|
+
BirthPolicy,
|
|
30
|
+
BirthGrant,
|
|
31
|
+
} from "./birth_meter.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The sanctioning declaration — an `ensures(NomosBirthRecord)` directive whose plan emits ZERO ops.
|
|
35
|
+
* Its only load-bearing property is its manifest/USD presence (marker `Ensure`, target
|
|
36
|
+
* `NomosBirthRecord`), which opens the id-mint gate's declared-ensures lane for the auto-appended
|
|
37
|
+
* birth record. Dispatching it is a harmless no-op (no events, nothing folds).
|
|
38
|
+
*/
|
|
39
|
+
export const nomosBirthRecordEnsure = directive("nomosBirthRecordEnsure")
|
|
40
|
+
.ensures(NomosBirthRecord)
|
|
41
|
+
.payload(z.object({}).strict())
|
|
42
|
+
.plan(() => []);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Inject the birth-meter's structural exports into a merged module-exports bag IFF it declares ≥1
|
|
46
|
+
* `.births()` directive (the RelationTuple "discoverable without re-exports" courtesy, at the one
|
|
47
|
+
* merge seam): the sanctioning {@link nomosBirthRecordEnsure} directive + the meter aggregates (so
|
|
48
|
+
* the engine registry / self-describe see the same surface the composed manifest carries). A
|
|
49
|
+
* birth-free bag is returned UNTOUCHED (the era rule — byte-identical). Existing keys are never
|
|
50
|
+
* overwritten (a law composing the framework governance surface keeps its own bindings).
|
|
51
|
+
*/
|
|
52
|
+
export function withBirthMeterExports<M extends Record<string, unknown>>(mod: M): M {
|
|
53
|
+
const declaresBirth = Object.values(mod).some(
|
|
54
|
+
(v) =>
|
|
55
|
+
v !== null &&
|
|
56
|
+
typeof v === "object" &&
|
|
57
|
+
(v as { birthsChild?: boolean }).birthsChild === true &&
|
|
58
|
+
typeof (v as Directive<unknown>).plan === "function",
|
|
59
|
+
);
|
|
60
|
+
if (!declaresBirth) return mod;
|
|
61
|
+
const out: Record<string, unknown> = { ...mod };
|
|
62
|
+
if (!("nomosBirthRecordEnsure" in out)) out["nomosBirthRecordEnsure"] = nomosBirthRecordEnsure;
|
|
63
|
+
if (!("NomosBirthRecord" in out)) out["NomosBirthRecord"] = NomosBirthRecord;
|
|
64
|
+
if (!("BirthPolicy" in out)) out["BirthPolicy"] = BirthPolicy;
|
|
65
|
+
if (!("BirthGrant" in out)) out["BirthGrant"] = BirthGrant;
|
|
66
|
+
return out as M;
|
|
67
|
+
}
|