@githolon/dsl 0.70.2 → 0.72.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 +4 -0
- package/dart/lib/src/session_bound.dart +309 -0
- package/dart/lib/src/test_harness.dart +136 -4
- package/dart/lib/src/webview_bridge.dart +173 -27
- package/dart/pubspec.yaml +1 -1
- package/dart/test/cluster_attested_test.dart +0 -1
- package/dart/test/session_bound_test.dart +255 -0
- package/dart_types/CHANGELOG.md +8 -0
- package/dart_types/lib/nomos_types.dart +6 -0
- package/dart_types/lib/src/principal.dart +56 -0
- package/dart_types/lib/src/surfaced.dart +208 -0
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +69 -0
- package/package.json +1 -1
- package/src/aggregate.ts +64 -7
- package/src/build_package.ts +22 -0
- package/src/codegen_dart.ts +155 -31
- package/src/codegen_dart_client.ts +274 -15
- package/src/codegen_openapi.ts +20 -0
- package/src/codegen_proof.ts +5 -1
- package/src/codegen_ts.ts +166 -1
- package/src/compile_package_main.ts +126 -9
- package/src/directive.ts +40 -0
- package/src/drivers.ts +63 -2
- package/src/idof_lint.ts +144 -0
- package/src/index.ts +1 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd_layers.ts +5 -0
- package/src/wire.ts +4 -1
|
@@ -0,0 +1,56 @@
|
|
|
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 QuickJS engine}.
|
|
4
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/// BRANDED IDENTITY TYPES for the generated cross-workspace birth methods.
|
|
7
|
+
///
|
|
8
|
+
/// A `.births()` directive's generated wrapper takes an `actor` (WHO births) and an optional
|
|
9
|
+
/// `authorSecret` (the signing key). Both were previously bare strings — two adjacent same-shaped
|
|
10
|
+
/// params that a caller can transpose with NO compiler error and a silent runtime failure (the same
|
|
11
|
+
/// bug class as a raw `domain`/`directiveId` swap). These wrappers make the transposition a type
|
|
12
|
+
/// error: `NomosPrincipal` and `NomosDeviceSecret` are distinct types, so passing one where the other
|
|
13
|
+
/// belongs won't compile. The generated method unwraps `.wire` to call the (unchanged) runtime
|
|
14
|
+
/// primitive `NomosBridge.createWorkspace(actor: String, authorSecret: String?, …)`.
|
|
15
|
+
|
|
16
|
+
/// A verified principal identity (`user:<uid>` on the wire). Construct with [NomosPrincipal.user]
|
|
17
|
+
/// from a bare uid, or [NomosPrincipal.parse] from a `user:<uid>` wire string.
|
|
18
|
+
class NomosPrincipal {
|
|
19
|
+
/// The bare uid (no `user:` prefix).
|
|
20
|
+
final String uid;
|
|
21
|
+
const NomosPrincipal._(this.uid);
|
|
22
|
+
|
|
23
|
+
/// From a bare uid (e.g. an auth-provider user id) — the common case.
|
|
24
|
+
factory NomosPrincipal.user(String uid) => NomosPrincipal._(uid);
|
|
25
|
+
|
|
26
|
+
/// From a `user:<uid>` wire string — throws [ArgumentError] on any other shape.
|
|
27
|
+
factory NomosPrincipal.parse(String wire) {
|
|
28
|
+
if (!wire.startsWith('user:')) {
|
|
29
|
+
throw ArgumentError('NomosPrincipal.parse: expected "user:<uid>", got "$wire"');
|
|
30
|
+
}
|
|
31
|
+
return NomosPrincipal._(wire.substring(5));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// The wire form the kernel seals as the captured principal: `user:<uid>`.
|
|
35
|
+
String get wire => 'user:$uid';
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
bool operator ==(Object other) => other is NomosPrincipal && other.uid == uid;
|
|
39
|
+
@override
|
|
40
|
+
int get hashCode => uid.hashCode;
|
|
41
|
+
@override
|
|
42
|
+
String toString() => wire;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// A device signing secret (base64). A distinct type from [NomosPrincipal] so it can't be swapped
|
|
46
|
+
/// with `actor`; its [toString] NEVER leaks the secret (redacted), so it is safe in logs/errors.
|
|
47
|
+
class NomosDeviceSecret {
|
|
48
|
+
final String _base64;
|
|
49
|
+
const NomosDeviceSecret(this._base64);
|
|
50
|
+
|
|
51
|
+
/// The base64 wire form handed to the (unchanged) runtime primitive.
|
|
52
|
+
String get wire => _base64;
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
String toString() => 'NomosDeviceSecret(***)';
|
|
56
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
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} on V8 + WASI-shim, byte-identical everywhere.
|
|
5
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
6
|
+
|
|
7
|
+
/// SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md) — the
|
|
8
|
+
/// user-resolvable third merge posture. A field declared `.merge(Surfaced(inner))`
|
|
9
|
+
/// reads as the PLAIN scalar once resolved, or — while genuinely concurrent writes
|
|
10
|
+
/// await a human pick — as the pending envelope
|
|
11
|
+
/// `{"__surfaced":{"pending":[{value, author, occurredAt}, ...]}}` (ALWAYS 2+
|
|
12
|
+
/// competing observations — a single-observation set read-collapses to the plain
|
|
13
|
+
/// scalar; bounded, cap 8).
|
|
14
|
+
///
|
|
15
|
+
/// The generated read models type such a field as `SurfacedValue<T>?`; app code
|
|
16
|
+
/// branches on [SurfacedValue.isPending] (or the generated `has…PendingConflict`
|
|
17
|
+
/// accessor) and resolves the conflict with an ORDINARY next write — a domain
|
|
18
|
+
/// directive that `set()`s the field. No transformation happens here: this types
|
|
19
|
+
/// the kernel's exact read-collapse JSON.
|
|
20
|
+
library;
|
|
21
|
+
|
|
22
|
+
import 'wire.dart' show Hlc;
|
|
23
|
+
|
|
24
|
+
/// The pending-envelope discriminator key in the read-collapse JSON.
|
|
25
|
+
const String surfacedEnvelopeKey = '__surfaced';
|
|
26
|
+
|
|
27
|
+
/// The JSON key of an observation's writer principal — pinned in ONE place
|
|
28
|
+
/// (kernel b215430d confirmed it as exactly `author`, a nullable uid string).
|
|
29
|
+
const String surfacedAuthorKey = 'author';
|
|
30
|
+
|
|
31
|
+
/// One competing observation retained on a pending Surfaced field: the written
|
|
32
|
+
/// [value], the HLC stamp it was written at, and the writer principal ([author],
|
|
33
|
+
/// NULLABLE — an observation folded where no intent context existed carries none).
|
|
34
|
+
class SurfacedObservation<T> {
|
|
35
|
+
final T value;
|
|
36
|
+
final Hlc occurredAt;
|
|
37
|
+
final String? author;
|
|
38
|
+
|
|
39
|
+
const SurfacedObservation({
|
|
40
|
+
required this.value,
|
|
41
|
+
required this.occurredAt,
|
|
42
|
+
this.author,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/// Decode one `{value, occurredAt, author?}` entry; [decodeT] turns the raw
|
|
46
|
+
/// JSON `value` leaf into the field's typed Dart value.
|
|
47
|
+
factory SurfacedObservation.fromJson(
|
|
48
|
+
Map<String, dynamic> json,
|
|
49
|
+
T Function(dynamic) decodeT,
|
|
50
|
+
) =>
|
|
51
|
+
SurfacedObservation(
|
|
52
|
+
value: decodeT(json['value']),
|
|
53
|
+
occurredAt: Hlc.fromJson(
|
|
54
|
+
(json['occurredAt'] as Map).cast<String, dynamic>()),
|
|
55
|
+
author: json[surfacedAuthorKey] as String?,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
@override
|
|
59
|
+
bool operator ==(Object other) =>
|
|
60
|
+
other is SurfacedObservation<T> &&
|
|
61
|
+
other.value == value &&
|
|
62
|
+
other.occurredAt == occurredAt &&
|
|
63
|
+
other.author == author;
|
|
64
|
+
|
|
65
|
+
@override
|
|
66
|
+
int get hashCode => Object.hash(value, occurredAt, author);
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
String toString() =>
|
|
70
|
+
'SurfacedObservation(value: $value, occurredAt: $occurredAt, author: $author)';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// A Surfaced field's read value: [SurfacedResolved] (the plain scalar — the
|
|
74
|
+
/// steady state, byte-identical to any other field) or [SurfacedPending] (the
|
|
75
|
+
/// kernel retained BOTH sides of a genuinely concurrent write for a human pick).
|
|
76
|
+
sealed class SurfacedValue<T> {
|
|
77
|
+
const SurfacedValue();
|
|
78
|
+
|
|
79
|
+
/// Decode a raw read-collapse leaf: the `{"__surfaced":{...}}` envelope becomes
|
|
80
|
+
/// [SurfacedPending]; any other value is the resolved scalar (via [decodeT]).
|
|
81
|
+
factory SurfacedValue.fromJson(dynamic json, T Function(dynamic) decodeT) {
|
|
82
|
+
if (json is Map && json.containsKey(surfacedEnvelopeKey)) {
|
|
83
|
+
final env =
|
|
84
|
+
(json[surfacedEnvelopeKey] as Map).cast<String, dynamic>();
|
|
85
|
+
final rawPending = (env['pending'] as List? ?? const []);
|
|
86
|
+
return SurfacedPending<T>(List.unmodifiable(rawPending.map((o) =>
|
|
87
|
+
SurfacedObservation<T>.fromJson(
|
|
88
|
+
(o as Map).cast<String, dynamic>(), decodeT))));
|
|
89
|
+
}
|
|
90
|
+
return SurfacedResolved<T>(decodeT(json));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Tolerant field decode off a flat projection `data` map: absent/null ⇒ null
|
|
94
|
+
/// (the PARTIAL-FOLD doctrine — a freshly-folded aggregate may not carry the
|
|
95
|
+
/// field yet); present ⇒ [SurfacedValue.fromJson].
|
|
96
|
+
static SurfacedValue<T>? decode<T>(
|
|
97
|
+
Map<String, dynamic> data,
|
|
98
|
+
String key,
|
|
99
|
+
T Function(dynamic) decodeT,
|
|
100
|
+
) {
|
|
101
|
+
final raw = data[key];
|
|
102
|
+
if (raw == null) return null;
|
|
103
|
+
return SurfacedValue<T>.fromJson(raw, decodeT);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// True while the field awaits a resolution (competing observations retained).
|
|
107
|
+
bool get isPending => this is SurfacedPending<T>;
|
|
108
|
+
|
|
109
|
+
/// The resolved scalar, or null while pending.
|
|
110
|
+
T? get valueOrNull {
|
|
111
|
+
final self = this;
|
|
112
|
+
return self is SurfacedResolved<T> ? self.value : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// The competing observations while pending; const empty once resolved.
|
|
116
|
+
List<SurfacedObservation<T>> get pending {
|
|
117
|
+
final self = this;
|
|
118
|
+
return self is SurfacedPending<T>
|
|
119
|
+
? self.observations
|
|
120
|
+
: const [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// ONE row of the workspace-wide `pendingConflicts()` read (reserved id
|
|
125
|
+
/// `nomosPendingConflicts`): a (aggregate, field) whose folded state awaits a
|
|
126
|
+
/// human pick. MAINTAINED like counts/sums (fold-time, O(pending) to read) and
|
|
127
|
+
/// DERIVED, NOT DRAINED — a resolution write folding in removes the row by
|
|
128
|
+
/// derivation, never a consumer acking it. The workspace-wide surface is
|
|
129
|
+
/// HETEROGENEOUS, so each observation's `value` is `Object?` here — the
|
|
130
|
+
/// per-field typed seam stays on the read models ([SurfacedValue]).
|
|
131
|
+
class PendingConflictRow {
|
|
132
|
+
/// The aggregate's logical type (e.g. `Doc`).
|
|
133
|
+
final String aggregateType;
|
|
134
|
+
|
|
135
|
+
/// The conflicted instance's aggregate id.
|
|
136
|
+
final String aggregateId;
|
|
137
|
+
|
|
138
|
+
/// The Surfaced field awaiting a resolution.
|
|
139
|
+
final String field;
|
|
140
|
+
|
|
141
|
+
/// The competing observations (2+, bounded) — value/author/occurredAt.
|
|
142
|
+
final List<SurfacedObservation<Object?>> observations;
|
|
143
|
+
|
|
144
|
+
const PendingConflictRow({
|
|
145
|
+
required this.aggregateType,
|
|
146
|
+
required this.aggregateId,
|
|
147
|
+
required this.field,
|
|
148
|
+
required this.observations,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
factory PendingConflictRow.fromJson(Map<String, dynamic> json) =>
|
|
152
|
+
PendingConflictRow(
|
|
153
|
+
aggregateType: json['aggregateType'] as String,
|
|
154
|
+
aggregateId: json['aggregateId'] as String,
|
|
155
|
+
field: json['field'] as String,
|
|
156
|
+
observations: List.unmodifiable(
|
|
157
|
+
(json['observations'] as List? ?? const []).map((o) =>
|
|
158
|
+
SurfacedObservation<Object?>.fromJson(
|
|
159
|
+
(o as Map).cast<String, dynamic>(), (v) => v))),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
@override
|
|
163
|
+
String toString() =>
|
|
164
|
+
'PendingConflictRow($aggregateType:$aggregateId.$field, ${observations.length} observations)';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// The steady state: the field folded to one value (never conflicted, or a
|
|
168
|
+
/// resolution write superseded the pending set).
|
|
169
|
+
final class SurfacedResolved<T> extends SurfacedValue<T> {
|
|
170
|
+
final T value;
|
|
171
|
+
const SurfacedResolved(this.value);
|
|
172
|
+
|
|
173
|
+
@override
|
|
174
|
+
bool operator ==(Object other) =>
|
|
175
|
+
other is SurfacedResolved<T> && other.value == value;
|
|
176
|
+
|
|
177
|
+
@override
|
|
178
|
+
int get hashCode => Object.hash('SurfacedResolved', value);
|
|
179
|
+
|
|
180
|
+
@override
|
|
181
|
+
String toString() => 'SurfacedResolved($value)';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// The conflict state: the bounded competing observations (ALWAYS 2+ — a
|
|
185
|
+
/// single-observation set read-collapses to the plain scalar; cap 8,
|
|
186
|
+
/// deterministically pruned by the kernel). Show them; let a human pick; write
|
|
187
|
+
/// the pick back with an ordinary domain directive.
|
|
188
|
+
final class SurfacedPending<T> extends SurfacedValue<T> {
|
|
189
|
+
final List<SurfacedObservation<T>> observations;
|
|
190
|
+
const SurfacedPending(this.observations);
|
|
191
|
+
|
|
192
|
+
@override
|
|
193
|
+
bool operator ==(Object other) {
|
|
194
|
+
if (other is! SurfacedPending<T>) return false;
|
|
195
|
+
if (other.observations.length != observations.length) return false;
|
|
196
|
+
for (var i = 0; i < observations.length; i++) {
|
|
197
|
+
if (other.observations[i] != observations[i]) return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@override
|
|
203
|
+
int get hashCode =>
|
|
204
|
+
Object.hashAll(['SurfacedPending', ...observations]);
|
|
205
|
+
|
|
206
|
+
@override
|
|
207
|
+
String toString() => 'SurfacedPending($observations)';
|
|
208
|
+
}
|
package/dart_types/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Pure kernel wire/data types for Nomos Dart frontends — no transport, no bridge,
|
|
12
12
|
no runtime. The hand-written serde-JSON-compatible types plus the offer codec,
|
|
13
13
|
reactive read engine, schema validation, and data-only error/trace types.
|
|
14
|
-
version: 0.
|
|
14
|
+
version: 0.2.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import 'package:nomos_types/nomos_types.dart';
|
|
2
|
+
import 'package:test/test.dart';
|
|
3
|
+
|
|
4
|
+
void main() {
|
|
5
|
+
group('SurfacedValue', () {
|
|
6
|
+
test('a plain scalar decodes RESOLVED', () {
|
|
7
|
+
final v = SurfacedValue<String>.fromJson('hello', (x) => x as String);
|
|
8
|
+
expect(v, isA<SurfacedResolved<String>>());
|
|
9
|
+
expect(v.isPending, isFalse);
|
|
10
|
+
expect(v.valueOrNull, 'hello');
|
|
11
|
+
expect(v.pending, isEmpty);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('the __surfaced envelope decodes PENDING (value + occurredAt + nullable author)', () {
|
|
15
|
+
final v = SurfacedValue<String>.fromJson({
|
|
16
|
+
'__surfaced': {
|
|
17
|
+
'pending': [
|
|
18
|
+
{
|
|
19
|
+
'value': 'a',
|
|
20
|
+
'occurredAt': {'physical': 10, 'logical': 1, 'replica': 7},
|
|
21
|
+
'author': 'user:ada',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
'value': 'b',
|
|
25
|
+
'occurredAt': {'physical': 11, 'logical': 0, 'replica': 9},
|
|
26
|
+
// no author — an observation folded without intent context.
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
}, (x) => x as String);
|
|
31
|
+
expect(v, isA<SurfacedPending<String>>());
|
|
32
|
+
expect(v.isPending, isTrue);
|
|
33
|
+
expect(v.valueOrNull, isNull);
|
|
34
|
+
expect(v.pending, hasLength(2));
|
|
35
|
+
expect(v.pending[0].value, 'a');
|
|
36
|
+
expect(v.pending[0].author, 'user:ada');
|
|
37
|
+
expect(v.pending[0].occurredAt, const Hlc(physical: 10, logical: 1, replica: 7));
|
|
38
|
+
expect(v.pending[1].value, 'b');
|
|
39
|
+
expect(v.pending[1].author, isNull);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('decode() off a flat data map: absent/null => null (partial-fold doctrine)', () {
|
|
43
|
+
expect(SurfacedValue.decode<String>({}, 'title', (x) => x as String), isNull);
|
|
44
|
+
expect(SurfacedValue.decode<String>({'title': null}, 'title', (x) => x as String), isNull);
|
|
45
|
+
final v = SurfacedValue.decode<String>({'title': 't'}, 'title', (x) => x as String);
|
|
46
|
+
expect(v, const SurfacedResolved<String>('t'));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('value equality holds on both arms', () {
|
|
50
|
+
expect(const SurfacedResolved<String>('x'), const SurfacedResolved<String>('x'));
|
|
51
|
+
final p1 = SurfacedValue<String>.fromJson({
|
|
52
|
+
'__surfaced': {
|
|
53
|
+
'pending': [
|
|
54
|
+
{'value': 'a', 'occurredAt': {'physical': 1, 'logical': 0, 'replica': 1}},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
}, (x) => x as String);
|
|
58
|
+
final p2 = SurfacedValue<String>.fromJson({
|
|
59
|
+
'__surfaced': {
|
|
60
|
+
'pending': [
|
|
61
|
+
{'value': 'a', 'occurredAt': {'physical': 1, 'logical': 0, 'replica': 1}},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
}, (x) => x as String);
|
|
65
|
+
expect(p1, p2);
|
|
66
|
+
expect(p1.hashCode, p2.hashCode);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.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
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* string: a typo'd handle is a compile error.
|
|
15
15
|
*/
|
|
16
16
|
import type { Field } from "./fields.js";
|
|
17
|
+
import { admitsNull } from "./fields.js";
|
|
17
18
|
import { satisfiesContract, type ContractHandle } from "./contract.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -160,6 +161,14 @@ export interface AggregateHandle<
|
|
|
160
161
|
* checked). Codegen marks the aggregate as implementing them so cross-domain refs can target the CONTRACT
|
|
161
162
|
* instead of this concrete type. Additive metadata (not hashed law). OMITTED when it implements none. */
|
|
162
163
|
readonly implementsContracts?: readonly string[];
|
|
164
|
+
/** THE DECLARED DART PRODUCT NAME (`.dartName("Site")`, architecture/dart_naming.md). When set, EVERY
|
|
165
|
+
* Dart codegen site that would otherwise derive a name from the aggregate WIRE id (the concrete
|
|
166
|
+
* aggregate class, the `<stem>ReadModel` projection class, the `<stem>Id` typed-id class, the
|
|
167
|
+
* `read/watch<Name>ById` accessors, and `ForeignRef<Target>` targets pointing at this aggregate) uses
|
|
168
|
+
* this name instead. Pure codegen PRESENTATION — the wire id / stable ids / projection type tag are
|
|
169
|
+
* UNCHANGED, and TS codegen stays id-derived. Additive, NON-law (never hashed — like `implementsContracts`);
|
|
170
|
+
* OMITTED when unset (a package with no `.dartName()` emits byte-identical Dart). */
|
|
171
|
+
readonly dartClassName?: string;
|
|
163
172
|
/**
|
|
164
173
|
* FRAMEWORK-SHARED brand (Dart-codegen hygiene). Set (by `framework/framework_aggregates.ts`)
|
|
165
174
|
* ONLY on the framework-owned aggregate singletons declared in `dsl/src/framework/` — most
|
|
@@ -232,19 +241,34 @@ export interface AggregateHandle<
|
|
|
232
241
|
|
|
233
242
|
/** The fluent AUDIENCE declarations returned by {@link aggregate} (secure-by-default). Each returns a
|
|
234
243
|
* copy of the handle with the read audience set; you call exactly one at the declaration site. */
|
|
244
|
+
/** A settled aggregate handle that STILL accepts the cosmetic `.dartName()` (so it chains after a
|
|
245
|
+
* terminal audience declaration — `aggregate(...).public().dartName("Site")`), but not another
|
|
246
|
+
* audience choice. `.dartName()` itself returns the full fluent surface (so either order works). */
|
|
247
|
+
export type NameableHandle<Id extends string, F extends Record<string, Field>> = AggregateHandle<Id, F> & {
|
|
248
|
+
dartName(name: string): AggregateHandle<Id, F> & AudienceMethods<Id, F>;
|
|
249
|
+
};
|
|
250
|
+
|
|
235
251
|
export interface AudienceMethods<Id extends string, F extends Record<string, Field>> {
|
|
236
252
|
/** Anyone may read (plaintext) — the deliberate public exception. */
|
|
237
|
-
public():
|
|
253
|
+
public(): NameableHandle<Id, F>;
|
|
238
254
|
/** Holders of `capability` may read (gate-enforced) — sugar over `requires(cap)`. */
|
|
239
|
-
readableBy(capability: string):
|
|
255
|
+
readableBy(capability: string): NameableHandle<Id, F>;
|
|
240
256
|
/** The owner (+ per-record grantees) read; owner = the verified author, stamped into `ownerField`
|
|
241
257
|
* (default `__owner`); operator-blind. NOTE: full owned read-scoping + E2E land in the next slice. */
|
|
242
|
-
owned(ownerField?: string):
|
|
258
|
+
owned(ownerField?: string): NameableHandle<Id, F>;
|
|
243
259
|
/** DOMAIN CONTRACTS: assert this aggregate satisfies `contract`'s public shape (compile-checked — a
|
|
244
260
|
* mismatch throws naming the field). Chainable; records the contract so cross-domain refs can target it. */
|
|
245
261
|
implements(contract: ContractHandle): AggregateHandle<Id, F> & AudienceMethods<Id, F>;
|
|
262
|
+
/** DART PRODUCT NAME: declare the Dart-side name for this aggregate (`.dartName("Site")`) — Dart codegen
|
|
263
|
+
* only, non-law, additive (see {@link AggregateHandle.dartName}). Validated NOW: `name` must be a legal
|
|
264
|
+
* Dart type identifier (`/^[A-Z][A-Za-z0-9_]*$/`). Cross-aggregate collision is validated at compile.
|
|
265
|
+
* Chainable; re-declaration overwrites. */
|
|
266
|
+
dartName(name: string): AggregateHandle<Id, F> & AudienceMethods<Id, F>;
|
|
246
267
|
}
|
|
247
268
|
|
|
269
|
+
/** A legal Dart TYPE identifier for `.dartName()` — PascalCase-ish (leading upper, then word chars). */
|
|
270
|
+
const DART_TYPE_NAME_RE = /^[A-Z][A-Za-z0-9_]*$/;
|
|
271
|
+
|
|
248
272
|
export function aggregate<const Id extends string, F extends Record<string, Field>>(
|
|
249
273
|
id: Id,
|
|
250
274
|
fields: F,
|
|
@@ -300,6 +324,27 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
300
324
|
}
|
|
301
325
|
}
|
|
302
326
|
}
|
|
327
|
+
// SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): `.merge(Surfaced(inner))`
|
|
328
|
+
// is v1-scoped to Lww-class SCALAR fields — the pending set holds competing scalar values a
|
|
329
|
+
// human picks between. The factory already refuses a non-Lww-class INNER; this refuses the
|
|
330
|
+
// wrapper on a field whose VALUE isn't a wholesale-replace scalar (a set/map/counter/position
|
|
331
|
+
// field's value space has no "pick one of two scalars" meaning) and on a null-admitting field
|
|
332
|
+
// (the tri-state NomosSlot and the pending envelope would fight over the same read surface).
|
|
333
|
+
for (const [name, f] of Object.entries(stored)) {
|
|
334
|
+
const field = f as Field;
|
|
335
|
+
if ((field.driver as { kind?: string }).kind !== "Surfaced") continue;
|
|
336
|
+
if (field.kind === "set" || field.kind === "map" || field.kind === "counter" || field.kind === "position") {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`aggregate '${id}': field '${name}' is .merge(Surfaced(...)) but its kind is '${field.kind}' — Surfaced wraps Lww-class SCALAR fields only in v1 (a '${field.kind}' field already has a conflict-free structural merge)`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (admitsNull(field)) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`aggregate '${id}': field '${name}' is .merge(Surfaced(...)) but also admits null (t.nullable/t.clearable) — Surfaced + the tri-state null slot is not supported in v1; drop one of the two`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
303
348
|
// The `.meta()` fields (plaintext-metadata opt-out on owned aggregates) — recorded for the compiler.
|
|
304
349
|
const metaFields = Object.entries(stored)
|
|
305
350
|
.filter(([, f]) => (f as Field).isMeta === true)
|
|
@@ -363,24 +408,36 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
363
408
|
audience: Audience | undefined,
|
|
364
409
|
visibility: ReadVisibility | undefined,
|
|
365
410
|
contracts: readonly string[] = [],
|
|
411
|
+
dartNameDecl: string | undefined = undefined,
|
|
366
412
|
): AggregateHandle<Id, F> & AudienceMethods<Id, F> => ({
|
|
367
413
|
...base,
|
|
368
414
|
...(audience !== undefined ? { audience } : {}),
|
|
369
415
|
...(visibility !== undefined ? { visibility } : {}),
|
|
370
416
|
...(contracts.length > 0 ? { implementsContracts: contracts } : {}),
|
|
371
|
-
|
|
417
|
+
...(dartNameDecl !== undefined ? { dartClassName: dartNameDecl } : {}),
|
|
418
|
+
public: () => build({ kind: "public" }, undefined, contracts, dartNameDecl),
|
|
372
419
|
readableBy: (cap: string) => {
|
|
373
420
|
const v = requires(cap); // validates the capability name
|
|
374
|
-
return build({ kind: "readableBy", cap: v.requires }, v, contracts);
|
|
421
|
+
return build({ kind: "readableBy", cap: v.requires }, v, contracts, dartNameDecl);
|
|
375
422
|
},
|
|
376
|
-
owned: (ownerField: string = OWNER_FIELD) => build({ kind: "owned", ownerField }, undefined, contracts),
|
|
423
|
+
owned: (ownerField: string = OWNER_FIELD) => build({ kind: "owned", ownerField }, undefined, contracts, dartNameDecl),
|
|
377
424
|
// DOMAIN CONTRACTS: assert this aggregate satisfies `contractHandle`'s public shape — compile-checked
|
|
378
425
|
// NOW (a mismatch throws a typed error naming the field). Records the contract so cross-domain refs may
|
|
379
426
|
// target it. Chainable (`.implements(A).implements(B)`); the aggregate keeps its concrete fields.
|
|
380
427
|
implements: (contractHandle: ContractHandle) => {
|
|
381
428
|
const verdict = satisfiesContract(id, stored as Record<string, Field>, contractHandle);
|
|
382
429
|
if (!verdict.ok) throw new Error(verdict.reason);
|
|
383
|
-
return build(audience, visibility, contracts.includes(contractHandle.id) ? contracts : [...contracts, contractHandle.id]);
|
|
430
|
+
return build(audience, visibility, contracts.includes(contractHandle.id) ? contracts : [...contracts, contractHandle.id], dartNameDecl);
|
|
431
|
+
},
|
|
432
|
+
// DART PRODUCT NAME: validate the identifier shape NOW; store it (Dart codegen only, non-law).
|
|
433
|
+
dartName: (name: string) => {
|
|
434
|
+
if (!DART_TYPE_NAME_RE.test(name)) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`aggregate '${id}': .dartName(${JSON.stringify(name)}) must be a legal Dart type identifier ` +
|
|
437
|
+
`matching ${DART_TYPE_NAME_RE} (PascalCase — a leading uppercase letter, then letters/digits/underscore)`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
return build(audience, visibility, contracts, name);
|
|
384
441
|
},
|
|
385
442
|
});
|
|
386
443
|
|
package/src/build_package.ts
CHANGED
|
@@ -77,6 +77,8 @@ import { expandCapabilityExports } from "./capability_exports.js";
|
|
|
77
77
|
import { RelationTuple } from "./framework/relations.js";
|
|
78
78
|
import { parentAttestedBirthMarker } from "./births.js";
|
|
79
79
|
import { discoverUnsanctionedEnsures, sanctioningEnsureDirective } from "./secondary_ensure.js";
|
|
80
|
+
import { assertIdOfLint } from "./idof_lint.js";
|
|
81
|
+
import { assertSurfacedResolvers } from "./surfaced_lint.js";
|
|
80
82
|
|
|
81
83
|
// ─────────────────────────────────────────────────────────────────────────────────
|
|
82
84
|
// composeDomainModule — the named-keys `module(...)`
|
|
@@ -388,6 +390,23 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
388
390
|
}
|
|
389
391
|
: null;
|
|
390
392
|
|
|
393
|
+
// THE PAYLOAD-REF idOf LINT (dart_codegen_parity.md P2 — ref ergonomics): plan-probe every
|
|
394
|
+
// directive and refuse a directive whose payload feeds a ref/foreignRef field from a
|
|
395
|
+
// provably-bare (unbranded) `z.string()` — see `idof_lint.ts` for the exact, deliberately
|
|
396
|
+
// narrow scope. Compile-time only, no manifest/wire/hash effect.
|
|
397
|
+
assertIdOfLint(directives, spec.domain ?? spec.name);
|
|
398
|
+
|
|
399
|
+
// THE SURFACED-RESOLVER LINT (surfaced_lint.ts — the fail-closed teeth): every
|
|
400
|
+
// `.merge(Surfaced(...))` field must have EXACTLY ONE `.resolves()` directive, whose plan
|
|
401
|
+
// provably set()s the field from a payload field. Returns the resolver bindings the typed
|
|
402
|
+
// `resolve…` helper codegen consumes. Compile-time only — no manifest/wire/hash effect;
|
|
403
|
+
// a Surfaced-free domain runs zero checks (byte-identical).
|
|
404
|
+
const surfacedResolvers = assertSurfacedResolvers(
|
|
405
|
+
aggregates,
|
|
406
|
+
directives,
|
|
407
|
+
spec.domain ?? spec.name,
|
|
408
|
+
);
|
|
409
|
+
|
|
391
410
|
return {
|
|
392
411
|
name: spec.name,
|
|
393
412
|
domain: spec.domain ?? spec.name,
|
|
@@ -398,6 +417,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
398
417
|
// ROLE SUGAR codegen metadata — NOT law (not hashed): drives the `canEdit<Agg>`/`canView<Agg>`
|
|
399
418
|
// UI-hint helpers. Omit-when-empty (a role-free domain is byte-identical).
|
|
400
419
|
...(roles.length > 0 ? { roles } : {}),
|
|
420
|
+
// SURFACED RESOLVER BINDINGS — NOT law (not hashed, like `roles`): drives the generated
|
|
421
|
+
// typed `resolve…` helpers + the `pendingConflicts` accessor. Omit-when-empty (era rule).
|
|
422
|
+
...(surfacedResolvers.length > 0 ? { surfacedResolvers } : {}),
|
|
401
423
|
...(queries.length > 0 ? { queries } : {}),
|
|
402
424
|
...(counts.length > 0 ? { counts } : {}),
|
|
403
425
|
...(spatials.length > 0 ? { spatials } : {}),
|