@githolon/dsl 0.100.6 → 0.101.1
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 +6 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +78 -16
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +29 -3
- package/dart/test/webview_bridge_test.dart +274 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/errors.dart +136 -30
- package/dart_types/lib/src/surfaced.dart +48 -2
- package/dart_types/lib/src/wire.dart +3 -1
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +19 -0
- package/package.json +1 -1
- package/src/activity.ts +43 -4
- package/src/aggregate.ts +132 -22
- package/src/attested_core.ts +1 -1
- package/src/authoring.ts +119 -6
- package/src/birth_meter.ts +4 -4
- package/src/birth_meter_ensure.ts +9 -1
- package/src/births.ts +53 -22
- package/src/build_package.ts +55 -14
- package/src/business_birth.ts +48 -7
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +921 -82
- package/src/codegen_openapi.ts +28 -5
- package/src/codegen_proof.ts +107 -11
- package/src/codegen_ts.ts +961 -135
- package/src/compile_package_main.ts +155 -45
- package/src/conflicts.ts +252 -0
- package/src/describe_model.ts +448 -0
- package/src/directive.ts +5 -38
- package/src/drivers.ts +12 -4
- package/src/engine_entry.ts +4 -0
- package/src/evolve_preview.ts +72 -11
- package/src/fields.ts +201 -55
- package/src/framework/application_home.ts +103 -0
- package/src/framework/application_home_domain.ts +2 -0
- package/src/framework/attested_receipts.ts +2 -2
- package/src/framework/auth_provider_law.ts +51 -24
- package/src/framework/business_corrections.ts +170 -0
- package/src/framework/disclosure.ts +1 -1
- package/src/framework/domain_lifecycle.ts +250 -16
- package/src/framework/encryption_identity.ts +1 -1
- package/src/framework/encryption_scope.ts +21 -13
- package/src/framework/era_certification.ts +1 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +1 -1
- package/src/framework/home.ts +22 -9
- package/src/framework/identity.ts +5 -5
- package/src/framework/impure_capability.ts +2 -2
- package/src/framework/owned_custody.ts +561 -0
- package/src/framework/platform_admin.ts +1 -1
- package/src/framework/relations.ts +9 -1
- package/src/framework/repair.ts +1 -1
- package/src/framework/signing_identity.ts +2 -2
- package/src/framework/source_roster.ts +1 -1
- package/src/framework/workspaces.ts +17 -10
- package/src/generated_client_names.ts +39 -0
- package/src/index.ts +20 -15
- package/src/kernel_capabilities.ts +25 -1
- package/src/law_check.ts +4 -1
- package/src/manifest.ts +356 -63
- package/src/read_projection_manifest.ts +8 -0
- package/src/read_projection_namespace.ts +4 -0
- package/src/relation.ts +1 -1
- package/src/shape_compat.ts +47 -2
- package/src/stable_ids.ts +38 -6
- package/src/stable_ids_types.ts +30 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd.ts +62 -11
- package/src/usd_layers.ts +54 -3
- package/src/value_object.ts +23 -19
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +48 -4
- package/src/workspace_routing.ts +57 -0
- package/src/workspace_sharding.ts +9 -9
- package/src/certified_read.ts +0 -313
|
@@ -82,6 +82,13 @@ sealed class SurfacedValue<T> {
|
|
|
82
82
|
if (json is Map && json.containsKey(surfacedEnvelopeKey)) {
|
|
83
83
|
final env =
|
|
84
84
|
(json[surfacedEnvelopeKey] as Map).cast<String, dynamic>();
|
|
85
|
+
if (env['status'] == 'evolutionRequired') {
|
|
86
|
+
return SurfacedEvolutionRequired<T>(
|
|
87
|
+
targetSchema: env['targetSchema'] as String? ?? '',
|
|
88
|
+
evolution: ((env['evolution'] as Map?) ?? const {})
|
|
89
|
+
.cast<String, dynamic>(),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
85
92
|
final rawPending = (env['pending'] as List? ?? const []);
|
|
86
93
|
return SurfacedPending<T>(List.unmodifiable(rawPending.map((o) =>
|
|
87
94
|
SurfacedObservation<T>.fromJson(
|
|
@@ -141,29 +148,68 @@ class PendingConflictRow {
|
|
|
141
148
|
/// The competing observations (2+, bounded) — value/author/occurredAt.
|
|
142
149
|
final List<SurfacedObservation<Object?>> observations;
|
|
143
150
|
|
|
151
|
+
/// `ready` or `evolutionRequired`. Only ready rows carry typed observations.
|
|
152
|
+
final String status;
|
|
153
|
+
|
|
154
|
+
/// The current stable field-contract identity the merge result must target.
|
|
155
|
+
final String targetSchema;
|
|
156
|
+
|
|
157
|
+
/// Diagnostic evolution requirement for a mixed-schema pending set.
|
|
158
|
+
final Map<String, dynamic>? evolution;
|
|
159
|
+
|
|
160
|
+
/// Opaque framework identity of exactly these pending observations.
|
|
161
|
+
final String snapshot;
|
|
162
|
+
|
|
144
163
|
const PendingConflictRow({
|
|
145
164
|
required this.aggregateType,
|
|
146
165
|
required this.aggregateId,
|
|
147
166
|
required this.field,
|
|
148
167
|
required this.observations,
|
|
168
|
+
required this.status,
|
|
169
|
+
required this.targetSchema,
|
|
170
|
+
this.evolution,
|
|
171
|
+
required this.snapshot,
|
|
149
172
|
});
|
|
150
173
|
|
|
151
|
-
factory PendingConflictRow.fromJson(Map<String, dynamic> json)
|
|
152
|
-
|
|
174
|
+
factory PendingConflictRow.fromJson(Map<String, dynamic> json) {
|
|
175
|
+
final snapshot = json['snapshot'];
|
|
176
|
+
if (snapshot is! String || snapshot.isEmpty) {
|
|
177
|
+
throw StateError(
|
|
178
|
+
'conflicts need a Nomos runtime with immutable conflict snapshots; upgrade Nomos before merging');
|
|
179
|
+
}
|
|
180
|
+
return PendingConflictRow(
|
|
153
181
|
aggregateType: json['aggregateType'] as String,
|
|
154
182
|
aggregateId: json['aggregateId'] as String,
|
|
155
183
|
field: json['field'] as String,
|
|
184
|
+
status: json['status'] as String? ?? 'evolutionRequired',
|
|
185
|
+
targetSchema: json['targetSchema'] as String? ?? '',
|
|
186
|
+
evolution: (json['evolution'] as Map?)?.cast<String, dynamic>(),
|
|
187
|
+
snapshot: snapshot,
|
|
156
188
|
observations: List.unmodifiable(
|
|
157
189
|
(json['observations'] as List? ?? const []).map((o) =>
|
|
158
190
|
SurfacedObservation<Object?>.fromJson(
|
|
159
191
|
(o as Map).cast<String, dynamic>(), (v) => v))),
|
|
160
192
|
);
|
|
193
|
+
}
|
|
161
194
|
|
|
162
195
|
@override
|
|
163
196
|
String toString() =>
|
|
164
197
|
'PendingConflictRow($aggregateType:$aggregateId.$field, ${observations.length} observations)';
|
|
165
198
|
}
|
|
166
199
|
|
|
200
|
+
/// A Surfaced value retained safely but not yet expressible as the current generated
|
|
201
|
+
/// type. The business values remain inside Nomos until a declared deterministic
|
|
202
|
+
/// evolution projects them into [targetSchema].
|
|
203
|
+
final class SurfacedEvolutionRequired<T> extends SurfacedValue<T> {
|
|
204
|
+
final String targetSchema;
|
|
205
|
+
final Map<String, dynamic> evolution;
|
|
206
|
+
|
|
207
|
+
const SurfacedEvolutionRequired({
|
|
208
|
+
required this.targetSchema,
|
|
209
|
+
required this.evolution,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
167
213
|
/// The steady state: the field folded to one value (never conflicted, or a
|
|
168
214
|
/// resolution write superseded the pending set).
|
|
169
215
|
final class SurfacedResolved<T> extends SurfacedValue<T> {
|
|
@@ -43,7 +43,9 @@ class Hlc {
|
|
|
43
43
|
factory Hlc.fromJson(Map<String, dynamic> json) => Hlc(
|
|
44
44
|
physical: (json['physical'] as num).toInt(),
|
|
45
45
|
logical: (json['logical'] as num).toInt(),
|
|
46
|
-
replica:
|
|
46
|
+
replica: json['replica'] is String
|
|
47
|
+
? int.parse(json['replica'] as String)
|
|
48
|
+
: (json['replica'] as num).toInt(),
|
|
47
49
|
);
|
|
48
50
|
|
|
49
51
|
Map<String, dynamic> toJson() => {
|
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.7.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -46,6 +46,25 @@ void main() {
|
|
|
46
46
|
expect(v, const SurfacedResolved<String>('t'));
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
test('mixed-schema values stay withheld until field evolution is available', () {
|
|
50
|
+
final v = SurfacedValue<String>.fromJson({
|
|
51
|
+
'__surfaced': {
|
|
52
|
+
'status': 'evolutionRequired',
|
|
53
|
+
'targetSchema': 'title-v2',
|
|
54
|
+
'evolution': {
|
|
55
|
+
'sourceSchemas': ['title-v1'],
|
|
56
|
+
'authoredUnder': ['law-v1'],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}, (_) => throw StateError('must not cast an old value to the current type'));
|
|
60
|
+
expect(v, isA<SurfacedEvolutionRequired<String>>());
|
|
61
|
+
final blocked = v as SurfacedEvolutionRequired<String>;
|
|
62
|
+
expect(blocked.targetSchema, 'title-v2');
|
|
63
|
+
expect(blocked.evolution['sourceSchemas'], ['title-v1']);
|
|
64
|
+
expect(blocked.isPending, isFalse);
|
|
65
|
+
expect(blocked.valueOrNull, isNull);
|
|
66
|
+
});
|
|
67
|
+
|
|
49
68
|
test('value equality holds on both arms', () {
|
|
50
69
|
expect(const SurfacedResolved<String>('x'), const SurfacedResolved<String>('x'));
|
|
51
70
|
final p1 = SurfacedValue<String>.fromJson({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.101.1",
|
|
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/activity.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// NOMOS — DIRECTIVE ACTIVITY DESCRIPTORS (architecture/activity_descriptors.md).
|
|
2
|
-
//
|
|
1
|
+
// NOMOS — DIRECTIVE ACTIVITY DESCRIPTORS (architecture/activity_descriptors.md). Every public business
|
|
2
|
+
// directive receives a generated accountable baseline; `.activity(...)` can enrich its user-facing metadata,
|
|
3
|
+
// so a client can fold typed activity rows straight from the intent
|
|
3
4
|
// log — no string-enricher guessing meaning beside the truth. The kernel/ledger stays the audit truth; this
|
|
4
5
|
// is a pure CLIENT-SIDE human projection, declared where the event semantics live.
|
|
5
6
|
//
|
|
@@ -13,8 +14,7 @@
|
|
|
13
14
|
// params: { siteName: "name" }, // display value ← payload field path
|
|
14
15
|
// })
|
|
15
16
|
//
|
|
16
|
-
// NON-LAW, additive metadata (same class as `.doc()`):
|
|
17
|
-
// byte-identical in the canonical manifest.
|
|
17
|
+
// NON-LAW, additive metadata (same class as `.doc()`): custom prose never changes the canonical law.
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import type { AggregateHandle } from "./aggregate.js";
|
|
20
20
|
import { isContractHandle, type ContractHandle } from "./contract.js";
|
|
@@ -77,6 +77,45 @@ export function activityKey(kind: string, slot: "title" | "message"): string {
|
|
|
77
77
|
return `activity.${kind}.${slot}`;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* The framework-owned baseline story for a business directive that does not add a richer
|
|
82
|
+
* `.activity(...)` declaration. Accountability is not opt-in: every public business verb must remain
|
|
83
|
+
* visible in `activityLog()` / `describe()`. The fallback deliberately uses only declaration metadata
|
|
84
|
+
* and the verified author/time already carried by the intent, so it never guesses payload meaning.
|
|
85
|
+
*
|
|
86
|
+
* `.activity(...)` remains the optional polish layer for domain-specific prose, parameters, icons and
|
|
87
|
+
* navigation. It replaces this descriptor for that directive; it no longer decides whether the change
|
|
88
|
+
* exists in the human story at all.
|
|
89
|
+
*/
|
|
90
|
+
export function defaultActivityDescriptor(args: {
|
|
91
|
+
readonly domain: string;
|
|
92
|
+
readonly directiveId: string;
|
|
93
|
+
readonly description?: string;
|
|
94
|
+
readonly refs?: ActivityDescriptor["refs"];
|
|
95
|
+
}): ActivityDescriptor {
|
|
96
|
+
const title = args.directiveId
|
|
97
|
+
.replace(/[_-]+/g, " ")
|
|
98
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
99
|
+
.replace(/\s+/g, " ")
|
|
100
|
+
.trim()
|
|
101
|
+
.replace(/^./, (c) => c.toUpperCase());
|
|
102
|
+
const description = args.description?.trim();
|
|
103
|
+
const kind = `${args.domain}.${args.directiveId}`;
|
|
104
|
+
return {
|
|
105
|
+
kind,
|
|
106
|
+
title: title || args.directiveId,
|
|
107
|
+
message: description && description.length > 0
|
|
108
|
+
? `{actor}: ${description}`
|
|
109
|
+
: `{actor} performed “${title || args.directiveId}”`,
|
|
110
|
+
category: args.domain,
|
|
111
|
+
severity: "info",
|
|
112
|
+
refs: args.refs ?? [],
|
|
113
|
+
params: [],
|
|
114
|
+
titleKey: activityKey(kind, "title"),
|
|
115
|
+
messageKey: activityKey(kind, "message"),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
80
119
|
/** Interpolate `{name}` placeholders in a template from `vars` (a missing var renders as the empty string —
|
|
81
120
|
* the compile-time validator already guarantees every placeholder is declared, so this is defensive only).
|
|
82
121
|
* The ONE render helper shared by the TS + Dart renderers (each reimplements the same trivial substitution). */
|
package/src/aggregate.ts
CHANGED
|
@@ -14,7 +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
|
+
import { admitsNull, attributionField, t } from "./fields.js";
|
|
18
18
|
import { satisfiesContract, type ContractHandle } from "./contract.js";
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -63,8 +63,9 @@ const READ_CAP_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
|
|
|
63
63
|
* • `public` — anyone reads (plaintext). The deliberate exception.
|
|
64
64
|
* • `readableBy` — holders of `cap` read (gate-enforced). Sugar over the shipped read-visibility
|
|
65
65
|
* (`requires(cap)`); per-field `.secret()` escalates to team-E2E.
|
|
66
|
-
* • `owned` — the owner
|
|
67
|
-
*
|
|
66
|
+
* • `owned` — the owner reads; owner = the verified authoring principal, stamped into the
|
|
67
|
+
* obscure framework field. The read rule is record-grained so later sharing can
|
|
68
|
+
* extend one record without changing this declaration; content is operator-blind.
|
|
68
69
|
* `.public()`/`.readableBy(cap)`/`.owned()` are the fluent declarations on the aggregate handle.
|
|
69
70
|
*/
|
|
70
71
|
export type Audience =
|
|
@@ -72,9 +73,47 @@ export type Audience =
|
|
|
72
73
|
| { readonly kind: "readableBy"; readonly cap: string }
|
|
73
74
|
| { readonly kind: "owned"; readonly ownerField: string };
|
|
74
75
|
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Framework-private names used to lower `.owned()`. They are deliberately obscure: a business model is
|
|
78
|
+
* unlikely to want either spelling accidentally. If it does, `owned()` fails loudly rather than silently
|
|
79
|
+
* capturing the tenant's concept. They remain constants (rather than magic strings spread through codegen,
|
|
80
|
+
* USD and the gate) so the public declaration and every enforcement lane cannot drift.
|
|
81
|
+
*/
|
|
82
|
+
export const OWNER_FIELD = "__nomos_owner_principal_7c1f5e";
|
|
83
|
+
export const OWNED_READ_RELATION = "nomos_internal_owned_reader_7c1f5e";
|
|
84
|
+
export const OWNED_KEY_RELATION = "nomos_internal_owned_device_key_7c1f5e";
|
|
85
|
+
export const OWNED_KEY_PAYLOAD_FIELD = "__nomos_owned_device_key_subject_7c1f5e";
|
|
86
|
+
export const OWNED_CUSTODY_PAYLOAD_FIELD = "__nomos_owned_device_custody_7c1f5e";
|
|
87
|
+
/** Framework-carried record identities reserved by the client before an owned create is planned.
|
|
88
|
+
* The tenant payload never sees this field; replay consumes the same typed id deterministically. */
|
|
89
|
+
export const OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD = "__nomos_owned_pre_minted_records_7c1f5e";
|
|
90
|
+
export const BUSINESS_BIRTH_SEED_PAYLOAD_FIELD = "__nomos_birth_seed_payload_7c1f5e";
|
|
91
|
+
|
|
92
|
+
/** Framework-private, deterministic relation object that seats one active `.owned()` device per principal. */
|
|
93
|
+
export function ownedKeyObject(principal: string): string {
|
|
94
|
+
let hex = "";
|
|
95
|
+
// Pure UTF-8 encoding because the sealed QuickJS plan sandbox intentionally exposes no ambient
|
|
96
|
+
// TextEncoder. This matches Rust's `principal.as_bytes()` and browser TextEncoder for valid strings.
|
|
97
|
+
for (let i = 0; i < principal.length; i += 1) {
|
|
98
|
+
let cp = principal.charCodeAt(i);
|
|
99
|
+
if (cp >= 0xd800 && cp <= 0xdbff && i + 1 < principal.length) {
|
|
100
|
+
const low = principal.charCodeAt(i + 1);
|
|
101
|
+
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
102
|
+
cp = 0x10000 + ((cp - 0xd800) << 10) + (low - 0xdc00);
|
|
103
|
+
i += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const bytes = cp <= 0x7f
|
|
107
|
+
? [cp]
|
|
108
|
+
: cp <= 0x7ff
|
|
109
|
+
? [0xc0 | (cp >> 6), 0x80 | (cp & 0x3f)]
|
|
110
|
+
: cp <= 0xffff
|
|
111
|
+
? [0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f)]
|
|
112
|
+
: [0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f)];
|
|
113
|
+
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
114
|
+
}
|
|
115
|
+
return `nomos-owned-principal:${hex || "00"}`;
|
|
116
|
+
}
|
|
78
117
|
|
|
79
118
|
/**
|
|
80
119
|
* Declare an aggregate PRIVATE behind a capability — the value for
|
|
@@ -248,15 +287,20 @@ export interface AggregateHandle<
|
|
|
248
287
|
* otherwise (era-safe). Indexing/grouping/summing is restricted to these on an owned/secret aggregate.
|
|
249
288
|
*/
|
|
250
289
|
readonly metaFields?: string[];
|
|
290
|
+
/** Framework-managed stored fields that are law/projection inputs but are intentionally absent from the
|
|
291
|
+
* generated business model. Present only for synthesized surfaces such as `.owned()`; a tenant collision
|
|
292
|
+
* is refused before this list can be created. */
|
|
293
|
+
readonly internalFields?: readonly string[];
|
|
251
294
|
}
|
|
252
295
|
|
|
253
296
|
/** The fluent AUDIENCE declarations returned by {@link aggregate} (secure-by-default). Each returns a
|
|
254
297
|
* copy of the handle with the read audience set; you call exactly one at the declaration site. */
|
|
255
|
-
/** A settled aggregate handle
|
|
256
|
-
*
|
|
257
|
-
*
|
|
298
|
+
/** A settled aggregate handle. Audience is terminal, but the two audience-independent declarations
|
|
299
|
+
* remain composable in either order: `.public().dartName("Site").implements(Contract)` and
|
|
300
|
+
* `.implements(Contract).dartName("Site").public()` mean the same thing. */
|
|
258
301
|
export type NameableHandle<Id extends string, F extends Record<string, Field>> = AggregateHandle<Id, F> & {
|
|
259
|
-
|
|
302
|
+
implements(contract: ContractHandle): NameableHandle<Id, F>;
|
|
303
|
+
dartName(name: string): NameableHandle<Id, F>;
|
|
260
304
|
};
|
|
261
305
|
|
|
262
306
|
export interface AudienceMethods<Id extends string, F extends Record<string, Field>> {
|
|
@@ -264,9 +308,9 @@ export interface AudienceMethods<Id extends string, F extends Record<string, Fie
|
|
|
264
308
|
public(): NameableHandle<Id, F>;
|
|
265
309
|
/** Holders of `capability` may read (gate-enforced) — sugar over `requires(cap)`. */
|
|
266
310
|
readableBy(capability: string): NameableHandle<Id, F>;
|
|
267
|
-
/** The
|
|
268
|
-
* (
|
|
269
|
-
owned(
|
|
311
|
+
/** The verified creator owns this record. Nomos stamps ownership, seats its per-record read relation and
|
|
312
|
+
* encrypts every non-`.meta()` content field; tenant code never names the framework field/relation. */
|
|
313
|
+
owned(): NameableHandle<Id, F>;
|
|
270
314
|
/** DOMAIN CONTRACTS: assert this aggregate satisfies `contract`'s public shape (compile-checked — a
|
|
271
315
|
* mismatch throws naming the field). Chainable; records the contract so cross-domain refs can target it. */
|
|
272
316
|
implements(contract: ContractHandle): AggregateHandle<Id, F> & AudienceMethods<Id, F>;
|
|
@@ -285,6 +329,19 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
285
329
|
fields: F,
|
|
286
330
|
opts?: { invariant?: AggregateInvariantFn; invariantDeclassify?: string; cap?: number; visibility?: ReadVisibility; global?: boolean; attributed?: boolean },
|
|
287
331
|
): AggregateHandle<Id, F> & AudienceMethods<Id, F> {
|
|
332
|
+
for (const reserved of [
|
|
333
|
+
OWNER_FIELD,
|
|
334
|
+
OWNED_KEY_PAYLOAD_FIELD,
|
|
335
|
+
OWNED_CUSTODY_PAYLOAD_FIELD,
|
|
336
|
+
OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
|
|
337
|
+
BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
|
|
338
|
+
]) {
|
|
339
|
+
if (!Object.prototype.hasOwnProperty.call(fields, reserved)) continue;
|
|
340
|
+
throw new Error(
|
|
341
|
+
`aggregate '${id}': field '${reserved}' is reserved by Nomos ownership. Sorry mate, that ` +
|
|
342
|
+
`concept is already taken by the framework; give the business field a different name.`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
288
345
|
const { stored, hasMany } = partitionFields(fields);
|
|
289
346
|
// FOREIGN-REF SIBLING CHECK (pain point #4): a `t.foreignRef({ workspace, target })` field names a
|
|
290
347
|
// SIBLING string field (`workspaceField`) that holds the foreign workspace id — the target workspace is
|
|
@@ -336,10 +393,10 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
336
393
|
}
|
|
337
394
|
}
|
|
338
395
|
// SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): `.merge(Surfaced(inner))`
|
|
339
|
-
// is v1-scoped to
|
|
340
|
-
//
|
|
396
|
+
// is v1-scoped to scalar fields — the pending set holds competing business versions until a
|
|
397
|
+
// resolver produces one merged result. The factory already refuses a non-scalar INNER; this refuses the
|
|
341
398
|
// wrapper on a field whose VALUE isn't a wholesale-replace scalar (a set/map/counter/position
|
|
342
|
-
// field's value space has
|
|
399
|
+
// field's value space already has a compositional merge law) and on a null-admitting field
|
|
343
400
|
// (the tri-state NomosSlot and the pending envelope would fight over the same read surface).
|
|
344
401
|
for (const [name, f] of Object.entries(stored)) {
|
|
345
402
|
const field = f as Field;
|
|
@@ -368,7 +425,7 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
368
425
|
// can be attributed"). Each attributed field's wire driver is wrapped
|
|
369
426
|
// `{"Attributed": <inner>}` — the law-era mechanism (old kernels refuse it cleanly;
|
|
370
427
|
// unattributed fields/domains stay byte-identical).
|
|
371
|
-
const LWW_CLASS_KINDS = new Set(["Lww", "LwwCausal", "LwwBy"]);
|
|
428
|
+
const LWW_CLASS_KINDS = new Set(["FirstWriterWins", "Lww", "LwwCausal", "LwwBy"]);
|
|
372
429
|
const attributableDriver = (f: Field) => LWW_CLASS_KINDS.has((f.driver as { kind?: string }).kind ?? "");
|
|
373
430
|
const attributableKind = (f: Field) =>
|
|
374
431
|
f.kind !== "set" && f.kind !== "map" && f.kind !== "counter" && f.kind !== "position";
|
|
@@ -382,7 +439,7 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
382
439
|
}
|
|
383
440
|
if (!attributableDriver(field) || !attributableKind(field)) {
|
|
384
441
|
throw new Error(
|
|
385
|
-
`aggregate '${id}': field '${name}' is .attributed() but attribution needs
|
|
442
|
+
`aggregate '${id}': field '${name}' is .attributed() but attribution needs a wholesale scalar driver (FirstWriterWins/Lww/LwwCausal/LwwBy) — a '${field.kind}' field under '${(field.driver as { kind?: string }).kind}' has no single author`,
|
|
386
443
|
);
|
|
387
444
|
}
|
|
388
445
|
}
|
|
@@ -483,8 +540,58 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
483
540
|
visibility: ReadVisibility | undefined,
|
|
484
541
|
contracts: readonly string[] = [],
|
|
485
542
|
dartNameDecl: string | undefined = undefined,
|
|
486
|
-
): AggregateHandle<Id, F> & AudienceMethods<Id, F> =>
|
|
543
|
+
): AggregateHandle<Id, F> & AudienceMethods<Id, F> => {
|
|
544
|
+
const owned = audience?.kind === "owned";
|
|
545
|
+
let ownedFields: Record<string, Field> | undefined;
|
|
546
|
+
let ownedEncrypted: string[] | undefined;
|
|
547
|
+
let ownedInvariant: AggregateInvariantFn | undefined;
|
|
548
|
+
if (owned) {
|
|
549
|
+
ownedFields = { ...stored };
|
|
550
|
+
ownedEncrypted = [];
|
|
551
|
+
for (const [name, field] of Object.entries(stored)) {
|
|
552
|
+
if (field.isMeta === true) {
|
|
553
|
+
if (field.isEncrypted === true) {
|
|
554
|
+
throw new Error(
|
|
555
|
+
`aggregate '${id}': field '${name}' is both .meta() and .encrypted(); metadata is the ` +
|
|
556
|
+
`explicit plaintext exception on .owned() data, so choose one.`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (!isLwwClass(field.driver)) {
|
|
562
|
+
throw new Error(
|
|
563
|
+
`aggregate '${id}': .owned() encrypts content field '${name}', but its merge driver is not ` +
|
|
564
|
+
`Lww-class. Mark it .meta() to make the value deliberately plaintext, or model the ` +
|
|
565
|
+
`encrypted value as a scalar until encrypted collection merges are supported.`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
ownedFields[name] = field.isEncrypted === true ? field : field.encrypted();
|
|
569
|
+
ownedEncrypted.push(name);
|
|
570
|
+
}
|
|
571
|
+
// The owner is framework state, not a business field. First-writer-wins makes ownership immutable;
|
|
572
|
+
// `.fromBirth()` plus the composed invariant means even a hand-authored create cannot omit it.
|
|
573
|
+
ownedFields[OWNER_FIELD] = attributionField(t.string().fromBirth());
|
|
574
|
+
ownedEncrypted.sort();
|
|
575
|
+
ownedInvariant = (snapshot) => {
|
|
576
|
+
const owner = snapshot[OWNER_FIELD];
|
|
577
|
+
if (typeof owner !== "string" || owner.length === 0) {
|
|
578
|
+
return { reject: `${id} is .owned() — Nomos must stamp its verified creator` };
|
|
579
|
+
}
|
|
580
|
+
return composedInvariant !== undefined ? composedInvariant(snapshot) : { accept: true };
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return ({
|
|
487
585
|
...base,
|
|
586
|
+
...(ownedFields !== undefined ? { fields: ownedFields } : {}),
|
|
587
|
+
...(ownedEncrypted !== undefined ? { encryptedFields: ownedEncrypted } : {}),
|
|
588
|
+
...(owned ? {
|
|
589
|
+
hasInvariant: true as const,
|
|
590
|
+
invariant: ownedInvariant!,
|
|
591
|
+
fromBirthFields: [...fromBirthFields, OWNER_FIELD].sort(),
|
|
592
|
+
internalFields: [OWNER_FIELD],
|
|
593
|
+
...(userInvariant === undefined ? { invariantValueFree: true as const } : {}),
|
|
594
|
+
} : {}),
|
|
488
595
|
...(audience !== undefined ? { audience } : {}),
|
|
489
596
|
...(visibility !== undefined ? { visibility } : {}),
|
|
490
597
|
...(contracts.length > 0 ? { implementsContracts: contracts } : {}),
|
|
@@ -494,7 +601,9 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
494
601
|
const v = requires(cap); // validates the capability name
|
|
495
602
|
return build({ kind: "readableBy", cap: v.requires }, v, contracts, dartNameDecl);
|
|
496
603
|
},
|
|
497
|
-
owned: (
|
|
604
|
+
owned: () => {
|
|
605
|
+
return build({ kind: "owned", ownerField: OWNER_FIELD }, requires(OWNED_READ_RELATION), contracts, dartNameDecl);
|
|
606
|
+
},
|
|
498
607
|
// DOMAIN CONTRACTS: assert this aggregate satisfies `contractHandle`'s public shape — compile-checked
|
|
499
608
|
// NOW (a mismatch throws a typed error naming the field). Records the contract so cross-domain refs may
|
|
500
609
|
// target it. Chainable (`.implements(A).implements(B)`); the aggregate keeps its concrete fields.
|
|
@@ -513,7 +622,8 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
513
622
|
}
|
|
514
623
|
return build(audience, visibility, contracts, name);
|
|
515
624
|
},
|
|
516
|
-
|
|
625
|
+
}) as unknown as AggregateHandle<Id, F> & AudienceMethods<Id, F>;
|
|
626
|
+
};
|
|
517
627
|
|
|
518
628
|
// Legacy `{ visibility: requires(cap) }` IS `readableBy(cap)` — carry both so existing law is unchanged.
|
|
519
629
|
const initialAudience: Audience | undefined =
|
|
@@ -523,7 +633,7 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
523
633
|
|
|
524
634
|
/** Lww-class driver: a wholesale-replace merge (`Lww`, or `MapOf` wrapping an Lww-class inner). */
|
|
525
635
|
function isLwwClass(driver: { kind: string; inner?: { kind: string } }): boolean {
|
|
526
|
-
if (driver.kind === "Lww") return true;
|
|
636
|
+
if (driver.kind === "FirstWriterWins" || driver.kind === "Lww") return true;
|
|
527
637
|
if (driver.kind === "MapOf" && driver.inner) return isLwwClass(driver.inner as { kind: string });
|
|
528
638
|
return false;
|
|
529
639
|
}
|
package/src/attested_core.ts
CHANGED
|
@@ -350,7 +350,7 @@ export const NomosSourceFrontier = aggregate("NomosSourceFrontier", {
|
|
|
350
350
|
maxAttestedAtSeen: t.int().merge(Lww),
|
|
351
351
|
/** The source head oid at the highest-seen position (diagnostics/audit). */
|
|
352
352
|
headAtMax: t.string().merge(Lww),
|
|
353
|
-
});
|
|
353
|
+
}).public();
|
|
354
354
|
|
|
355
355
|
|
|
356
356
|
// ── the ensure() hook — breaks the read.ts → authoring.ts module-init cycle ────────────────────
|
package/src/authoring.ts
CHANGED
|
@@ -23,22 +23,64 @@
|
|
|
23
23
|
* resets it before the plan and drains it after, merging the authored ops with whatever the plan returns.
|
|
24
24
|
* So a `.plan` that fluently authors and `return []`s still emits its events — the recorded ops are real.
|
|
25
25
|
*/
|
|
26
|
-
import
|
|
26
|
+
import {
|
|
27
|
+
OWNED_KEY_RELATION,
|
|
28
|
+
OWNED_READ_RELATION,
|
|
29
|
+
ownedKeyObject,
|
|
30
|
+
type AggregateHandle,
|
|
31
|
+
} from "./aggregate.js";
|
|
27
32
|
import type { Field, FieldValue } from "./fields.js";
|
|
28
33
|
import type { AcceptedValue, FieldOp, PlannedOp, PlannedEventMarker } from "./ops.js";
|
|
29
|
-
import { endorsementOf } from "./principal_evidence.js";
|
|
30
|
-
import {
|
|
34
|
+
import { endorsementOf, principalEvidence } from "./principal_evidence.js";
|
|
35
|
+
import type { PrincipalEvidence } from "./principal_evidence.js";
|
|
36
|
+
import type { Ports } from "./ctx.js";
|
|
37
|
+
import { __qualifyAggregateType, writeTuple } from "./framework/relations.js";
|
|
31
38
|
import { __registerEnsure } from "./attested_core.js";
|
|
32
39
|
|
|
33
40
|
// ── the per-dispatch authoring sink — library state the engine never sees (NOT a side channel out of
|
|
34
41
|
// the sandbox; it is internal to the directive runtime, drained synchronously by `executeDirectiveToIntent`). ──
|
|
35
42
|
let sink: PlannedOp[] = [];
|
|
43
|
+
let authoringContext: {
|
|
44
|
+
principal: PrincipalEvidence;
|
|
45
|
+
principalRaw: string;
|
|
46
|
+
stamp: string;
|
|
47
|
+
ownedKeySubject: string;
|
|
48
|
+
ownedCustody?: string;
|
|
49
|
+
preMintedRecords: { type: string; id: string }[];
|
|
50
|
+
} | undefined;
|
|
36
51
|
/** Clear the sink before a plan runs (called by `executeDirectiveToIntent`). */
|
|
37
|
-
export function __resetAuthoring(
|
|
52
|
+
export function __resetAuthoring(
|
|
53
|
+
ctx?: Partial<Pick<Ports, "principal">>,
|
|
54
|
+
stamp = "0",
|
|
55
|
+
ownedKeySubject = `key:${"0".repeat(64)}`,
|
|
56
|
+
ownedCustody: string | null = "{\"nomosProbe\":true}",
|
|
57
|
+
preMintedRecords: readonly { type: string; id: string }[] = [],
|
|
58
|
+
): void {
|
|
38
59
|
sink = [];
|
|
60
|
+
// Compile/proof lanes call the internal reset without live ports; give those deterministic probe evidence
|
|
61
|
+
// so discovery sees the exact synthesized owner + relation writes. The real authoring lane always passes
|
|
62
|
+
// its captured ports below, and the executor binds that value to the verified signer before admission.
|
|
63
|
+
const principalRaw = typeof ctx?.principal === "function"
|
|
64
|
+
? endorsementOf(ctx.principal().asAttribution())?.principal ?? ""
|
|
65
|
+
: "user:nomos-probe";
|
|
66
|
+
authoringContext = {
|
|
67
|
+
principal: principalEvidence(principalRaw),
|
|
68
|
+
principalRaw,
|
|
69
|
+
stamp,
|
|
70
|
+
ownedKeySubject,
|
|
71
|
+
...(ownedCustody !== null ? { ownedCustody } : {}),
|
|
72
|
+
preMintedRecords: preMintedRecords.map((record) => ({ ...record })),
|
|
73
|
+
};
|
|
39
74
|
}
|
|
40
75
|
/** Take + clear the ops authored via the fluent surface during a plan (called by `executeDirectiveToIntent`). */
|
|
41
76
|
export function __drainAuthoring(): PlannedOp[] {
|
|
77
|
+
if (authoringContext !== undefined && authoringContext.preMintedRecords.length > 0) {
|
|
78
|
+
const remaining = authoringContext.preMintedRecords.map((record) => `${record.type}:${record.id}`).join(", ");
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Nomos reserved owned record identity ${remaining}, but the law did not consume it. ` +
|
|
81
|
+
`The generated client and installed law disagree about this create; recompile them together.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
42
84
|
const s = sink;
|
|
43
85
|
sink = [];
|
|
44
86
|
return s;
|
|
@@ -52,6 +94,28 @@ function mint(typeTag: string): string {
|
|
|
52
94
|
return (globalThis as unknown as { nomos: NomosMint }).nomos.mint(typeTag);
|
|
53
95
|
}
|
|
54
96
|
|
|
97
|
+
/** Consume the generated client's hidden, kernel-minted identity for an owned create. Keeping this in
|
|
98
|
+
* the plan input makes replay reproduce the exact id while letting the client bind ciphertext to the
|
|
99
|
+
* record before dispatch. A mismatched type is a loud framework/version error, never a reroute. */
|
|
100
|
+
function recordIdentity(typeTag: string): string {
|
|
101
|
+
const queued = authoringContext?.preMintedRecords;
|
|
102
|
+
if (queued === undefined || queued.length === 0) return mint(typeTag);
|
|
103
|
+
const record = queued.shift()!;
|
|
104
|
+
if (record.type !== typeTag) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Nomos reserved an owned '${record.type}' identity, but the law tried to create '${typeTag}'. ` +
|
|
107
|
+
`The generated client and installed law disagree; recompile them together.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (!record.id.startsWith(`${typeTag}_`)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Nomos reserved owned '${typeTag}' identity '${record.id}', but that id belongs to another type. ` +
|
|
113
|
+
`The generated client and installed law disagree; recompile them together.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return record.id;
|
|
117
|
+
}
|
|
118
|
+
|
|
55
119
|
/**
|
|
56
120
|
* `true` iff `F` is the WIDE, un-inferred field map (`Record<string, Field>`) — i.e. the ref
|
|
57
121
|
* came from an untyped handle. Used to DEGRADE the typed member signatures back to today's
|
|
@@ -197,7 +261,54 @@ export function create<Id extends string, F extends Record<string, Field>>(
|
|
|
197
261
|
): AggregateRef<F> {
|
|
198
262
|
// READ-PROJECTION NAMESPACING: mint under the QUALIFIED type tag — the kernel's id-mint gate
|
|
199
263
|
// verifies a Create's id was minted for EXACTLY the declared (qualified) `__type`.
|
|
200
|
-
|
|
264
|
+
const id = recordIdentity(__qualifyAggregateType(agg.id));
|
|
265
|
+
const ref = new Ref(agg, id, "creates");
|
|
266
|
+
seatOwned(ref, agg, "create");
|
|
267
|
+
return ref as unknown as AggregateRef<F>;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Lower the framework-owned half of an owned birth. `create()` and `ensure()` differ only in how the
|
|
271
|
+
* record id is chosen; when either one births a record, ownership and both seats must be byte-identical.
|
|
272
|
+
* Re-ensuring an existing owned record reasserts the same immutable owner/seat and is admitted only for
|
|
273
|
+
* that owner on the already-approved device. The executor judges existence and rejects any change. */
|
|
274
|
+
function seatOwned<Id extends string, F extends Record<string, Field>>(
|
|
275
|
+
ref: Ref,
|
|
276
|
+
agg: AggregateHandle<Id, F>,
|
|
277
|
+
operation: "create" | "ensure",
|
|
278
|
+
): void {
|
|
279
|
+
if (agg.audience?.kind === "owned") {
|
|
280
|
+
if (authoringContext === undefined) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`${operation}(${agg.id}): .owned() needs Nomos's verified authoring context. Run the plan through ` +
|
|
283
|
+
`the Nomos authoring lane; tenant code must never supply an owner.`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const { principal, principalRaw, stamp, ownedKeySubject, ownedCustody } = authoringContext;
|
|
287
|
+
// Ownership and its per-record read seat are part of the SAME intent as the business create. The owner
|
|
288
|
+
// is opaque evidence, not a payload string: the gate binds it to the verified author on every lane.
|
|
289
|
+
ref.set(agg.audience.ownerField, principal.asAttribution());
|
|
290
|
+
sink.push(
|
|
291
|
+
...writeTuple({
|
|
292
|
+
object: `${__qualifyAggregateType(agg.id)}:${ref.__id}`,
|
|
293
|
+
relation: OWNED_READ_RELATION,
|
|
294
|
+
subject: principal.asAuthoritySubject(
|
|
295
|
+
"Nomos seats the verified creator as the initial reader of their owned record",
|
|
296
|
+
),
|
|
297
|
+
grantedBy: principal.asAttribution(),
|
|
298
|
+
grantedAt: stamp,
|
|
299
|
+
}),
|
|
300
|
+
...(ownedCustody === undefined
|
|
301
|
+
? []
|
|
302
|
+
: writeTuple({
|
|
303
|
+
object: ownedKeyObject(principalRaw),
|
|
304
|
+
relation: OWNED_KEY_RELATION,
|
|
305
|
+
subject: ownedKeySubject,
|
|
306
|
+
grantedBy: principal.asAttribution(),
|
|
307
|
+
grantedAt: stamp,
|
|
308
|
+
ownedCustody,
|
|
309
|
+
})),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
201
312
|
}
|
|
202
313
|
|
|
203
314
|
/** A reference to an EXISTING aggregate the dev already holds an id for (e.g. from the payload / a read).
|
|
@@ -219,7 +330,9 @@ export function ensure<Id extends string, F extends Record<string, Field>>(
|
|
|
219
330
|
agg: AggregateHandle<Id, F>,
|
|
220
331
|
id: string,
|
|
221
332
|
): AggregateRef<F> {
|
|
222
|
-
|
|
333
|
+
const ref = new Ref(agg, id, "ensures");
|
|
334
|
+
seatOwned(ref, agg, "ensure");
|
|
335
|
+
return ref as unknown as AggregateRef<F>;
|
|
223
336
|
}
|
|
224
337
|
|
|
225
338
|
// Register ensure() for the foreign read lane (attested reads — see attested_core.ts; the
|