@githolon/dsl 0.67.0 → 0.68.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 +6 -0
- package/dart/lib/src/webview_bridge.dart +46 -2
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart_types/CHANGELOG.md +7 -0
- package/dart_types/lib/nomos_types.dart +1 -0
- package/dart_types/lib/src/activity.dart +19 -6
- package/dart_types/lib/src/foreign_ref.dart +6 -0
- package/dart_types/lib/src/nomos_ref.dart +75 -0
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/src/activity.ts +22 -4
- package/src/codegen_dart.ts +45 -6
- package/src/codegen_dart_client.ts +32 -43
- package/src/codegen_ts.ts +89 -62
- package/src/compile_package_main.ts +5 -4
- package/src/directive.ts +7 -2
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.45.0
|
|
2
|
+
|
|
3
|
+
- `offerOutcome(...)` — the one outcome-returning offer lane generated clients now call for EVERY directive:
|
|
4
|
+
returns `NomosOfferOutcome` with workspace-stamped `created` (`NomosCreated` gains `workspace` + a
|
|
5
|
+
`NomosRef get ref` view). Activity raw rows carry `created` + `workspace`. Requires `nomos_types >= 0.1.3`.
|
|
6
|
+
|
|
1
7
|
## 0.44.0
|
|
2
8
|
|
|
3
9
|
- Activity-feed runtime reads on `NomosBridge`/`NomosSession`: `activityLog({base, limit})` — the committed
|
|
@@ -23,6 +23,7 @@ import 'package:nomos_types/nomos_types.dart'
|
|
|
23
23
|
nomosErrorFromInfo,
|
|
24
24
|
NomosTraceContext,
|
|
25
25
|
NomosErrorInfo,
|
|
26
|
+
NomosRef,
|
|
26
27
|
NomosForeignReader,
|
|
27
28
|
NomosForeignWorkspaceReader,
|
|
28
29
|
ForeignAttestation;
|
|
@@ -104,7 +105,15 @@ class SyncStatus {
|
|
|
104
105
|
class NomosCreated {
|
|
105
106
|
final String type;
|
|
106
107
|
final String id;
|
|
107
|
-
|
|
108
|
+
|
|
109
|
+
/// The workspace the offer ran on (NomosRef). Additive — defaults to '' on older runners that
|
|
110
|
+
/// don't stamp it; then [ref] is workspace-blank (still a valid locator on the local plane).
|
|
111
|
+
final String workspace;
|
|
112
|
+
const NomosCreated({required this.type, required this.id, this.workspace = ''});
|
|
113
|
+
|
|
114
|
+
/// The canonical [NomosRef] view — the `{workspace,type,id}` triple, followable/serializable
|
|
115
|
+
/// (`nomos://<workspace>/<type>/<id>`). One reference, everywhere.
|
|
116
|
+
NomosRef get ref => NomosRef(workspace: workspace, type: type, id: id);
|
|
108
117
|
@override
|
|
109
118
|
String toString() => id;
|
|
110
119
|
}
|
|
@@ -604,6 +613,10 @@ class NomosOfferOutcome {
|
|
|
604
613
|
final String head;
|
|
605
614
|
final List<NomosCreated> created;
|
|
606
615
|
|
|
616
|
+
/// The workspace the offer ran on (NomosRef). Additive; '' on older runners. Each [created] ref already
|
|
617
|
+
/// carries this, so `outcome.created.first.ref` is the full followable `{workspace,type,id}` locator.
|
|
618
|
+
final String workspace;
|
|
619
|
+
|
|
607
620
|
/// The born child workspace name(s), when the offer ran a `.births()` directive (else empty). Each is a
|
|
608
621
|
/// LOCAL workspace on this device's plane, resident in the shared engine plane the moment it is born:
|
|
609
622
|
/// `bridge.session(name)` returns its handle, and `bridge.connect(session: name)` (no `restoreFrom`) mounts
|
|
@@ -616,6 +629,7 @@ class NomosOfferOutcome {
|
|
|
616
629
|
const NomosOfferOutcome({
|
|
617
630
|
required this.head,
|
|
618
631
|
required this.created,
|
|
632
|
+
this.workspace = '',
|
|
619
633
|
this.born = const [],
|
|
620
634
|
this.birthOutcomes = const [],
|
|
621
635
|
});
|
|
@@ -965,6 +979,32 @@ class NomosBridge {
|
|
|
965
979
|
List<Map<String, Object?>>? attestations,
|
|
966
980
|
bool? force,
|
|
967
981
|
String? session,
|
|
982
|
+
}) =>
|
|
983
|
+
offerOutcome(
|
|
984
|
+
domain: domain,
|
|
985
|
+
directiveId: directiveId,
|
|
986
|
+
payload: payload,
|
|
987
|
+
actor: actor,
|
|
988
|
+
domainHash: domainHash,
|
|
989
|
+
attestations: attestations,
|
|
990
|
+
force: force,
|
|
991
|
+
session: session,
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
/// THE ONE OUTCOME-RETURNING OFFER LANE — [offerDirective] answering the full typed [NomosOfferOutcome]
|
|
995
|
+
/// (head + law-minted `created` NomosRefs + births), for EVERY generated method (create or mutate). A pure
|
|
996
|
+
/// mutation returns `created: []`; a `.creates(...)` plan returns the minted aggregate id(s), each already
|
|
997
|
+
/// carrying the session [workspace] so `outcome.created.first.ref` is a complete followable NomosRef with no
|
|
998
|
+
/// query-after round-trip. [offerCreates] is the historical alias (identical behavior).
|
|
999
|
+
Future<NomosOfferOutcome> offerOutcome({
|
|
1000
|
+
required String domain,
|
|
1001
|
+
required String directiveId,
|
|
1002
|
+
required Object? payload,
|
|
1003
|
+
String? actor,
|
|
1004
|
+
String? domainHash,
|
|
1005
|
+
List<Map<String, Object?>>? attestations,
|
|
1006
|
+
bool? force,
|
|
1007
|
+
String? session,
|
|
968
1008
|
}) async {
|
|
969
1009
|
final r = _asMap(await _call('offerDirective', {
|
|
970
1010
|
'domain': domain,
|
|
@@ -992,13 +1032,17 @@ class NomosBridge {
|
|
|
992
1032
|
.map((m) => (m['workspace'] ?? '').toString())
|
|
993
1033
|
.where((s) => s.isNotEmpty),
|
|
994
1034
|
]..removeWhere((s) => s.isEmpty);
|
|
1035
|
+
// The offer ran on this workspace (NomosRef) — stamp every minted ref with it so it is a complete locator.
|
|
1036
|
+
final ws = (r['workspace'] ?? '').toString();
|
|
995
1037
|
return NomosOfferOutcome(
|
|
996
1038
|
head: r['head'].toString(),
|
|
1039
|
+
workspace: ws,
|
|
997
1040
|
created: ((r['created'] as List?) ?? const [])
|
|
998
1041
|
.whereType<Map>()
|
|
999
1042
|
.map((m) => NomosCreated(
|
|
1000
1043
|
type: (m['type'] ?? '').toString(),
|
|
1001
|
-
id: (m['id'] ?? '').toString()
|
|
1044
|
+
id: (m['id'] ?? '').toString(),
|
|
1045
|
+
workspace: ws))
|
|
1002
1046
|
.toList(),
|
|
1003
1047
|
born: bornNames,
|
|
1004
1048
|
birthOutcomes: births,
|
package/dart/pubspec.lock
CHANGED
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.
|
|
14
|
+
version: 0.45.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -20,7 +20,7 @@ dependencies:
|
|
|
20
20
|
# The pure-data core (wire types, offer codec, NomosReads, schema validation, error
|
|
21
21
|
# info). nomos_client depends on and re-exports it, so existing imports are unchanged.
|
|
22
22
|
# For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
|
|
23
|
-
nomos_types: ^0.1.
|
|
23
|
+
nomos_types: ^0.1.3
|
|
24
24
|
json_schema: ^5.2.2
|
|
25
25
|
crypto: ^3.0.0
|
|
26
26
|
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## 0.1.3
|
|
2
|
+
|
|
3
|
+
- `NomosRef` — THE one typed reference `{workspace, type, id}`: value semantics, the canonical
|
|
4
|
+
`nomos://<workspace>/<type>/<id>` uri (`uri`/`parse`, encode round-trip), `fromJson` (object or uri),
|
|
5
|
+
`resolve()` over the multi-workspace reader. `ActivityRef` gains `workspace` + a `.ref` view; `ForeignRef`
|
|
6
|
+
gains `.ref`. Every reference shape now converges on the one triple. Purely additive.
|
|
7
|
+
|
|
1
8
|
## 0.1.2
|
|
2
9
|
|
|
3
10
|
- Activity-feed runtime types: `ActivityEvent` (the shared activity row — kind/actor/occurredAt, rendered
|
|
@@ -8,22 +8,35 @@
|
|
|
8
8
|
// the generated pure `renderActivity` lives) AND the runtime `nomos_client` package resolve them.
|
|
9
9
|
library;
|
|
10
10
|
|
|
11
|
+
import 'nomos_ref.dart';
|
|
12
|
+
|
|
11
13
|
/// A declared navigation target on an activity row: the referenced aggregate/contract TYPE + the resolved id
|
|
12
14
|
/// (read from a payload field the directive named). Replaces the old enricher's blind id-probing — the app
|
|
13
15
|
/// knows exactly what `id` points at and can deep-link/resolve it.
|
|
14
16
|
class ActivityRef {
|
|
15
17
|
final String type;
|
|
16
18
|
final String id;
|
|
17
|
-
const ActivityRef({required this.type, required this.id});
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/// The source workspace (additive — empty on rows rendered before refs carried it).
|
|
21
|
+
final String workspace;
|
|
22
|
+
|
|
23
|
+
const ActivityRef({required this.type, required this.id, this.workspace = ''});
|
|
24
|
+
|
|
25
|
+
/// The canonical reference view (`nomos://<workspace>/<type>/<id>`).
|
|
26
|
+
NomosRef get ref => NomosRef(workspace: workspace, type: type, id: id);
|
|
27
|
+
|
|
28
|
+
factory ActivityRef.fromJson(Map<String, dynamic> j) => ActivityRef(
|
|
29
|
+
type: (j['type'] ?? '') as String,
|
|
30
|
+
id: (j['id'] ?? '') as String,
|
|
31
|
+
workspace: (j['workspace'] ?? '') as String);
|
|
32
|
+
Map<String, dynamic> toJson() =>
|
|
33
|
+
{'type': type, 'id': id, if (workspace.isNotEmpty) 'workspace': workspace};
|
|
22
34
|
|
|
23
35
|
@override
|
|
24
|
-
bool operator ==(Object other) =>
|
|
36
|
+
bool operator ==(Object other) =>
|
|
37
|
+
other is ActivityRef && other.type == type && other.id == id && other.workspace == workspace;
|
|
25
38
|
@override
|
|
26
|
-
int get hashCode => Object.hash(type, id);
|
|
39
|
+
int get hashCode => Object.hash(type, id, workspace);
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
/// Row prominence — a declared default the app may style or threshold on.
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// this barrel, and its `NomosBridge` implements [NomosForeignReader] over the device's multi-workspace plane.
|
|
15
15
|
library;
|
|
16
16
|
|
|
17
|
+
import 'nomos_ref.dart';
|
|
18
|
+
|
|
17
19
|
/// Reads a FOREIGN workspace on demand — the surface a [ForeignRef] resolves against. It is STRUCTURAL: any
|
|
18
20
|
/// object with this shape works. `nomos_client`'s `NomosBridge` implements it over the device's local
|
|
19
21
|
/// multi-workspace plane (`bridge.session(ws).queryById(id)`, mounting the workspace lazily); the TS twin is
|
|
@@ -111,6 +113,10 @@ class ForeignRef<T> {
|
|
|
111
113
|
/// The foreign aggregate type this locator points at.
|
|
112
114
|
final String type;
|
|
113
115
|
|
|
116
|
+
/// The canonical reference view (`nomos://<workspace>/<type>/<id>`) — a [ForeignRef] IS a typed view over
|
|
117
|
+
/// the one reference triple.
|
|
118
|
+
NomosRef get ref => NomosRef(workspace: workspace, type: type, id: id);
|
|
119
|
+
|
|
114
120
|
/// The target's decoder (the aggregate `fromJson` factory), applied to a resolved row's flat `data` map.
|
|
115
121
|
final T Function(Map<String, dynamic> data, [String id]) _decode;
|
|
116
122
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// NOMOS — THE ONE REFERENCE (architecture/nomos_ref.md). A [NomosRef] is the canonical, globally-unique,
|
|
2
|
+
// workspace-qualified identity of an aggregate instance: `{ workspace, type, id }`, with the stable string
|
|
3
|
+
// form `nomos://<workspace>/<type>/<id>`. It travels WITH the data by default — every generated read row,
|
|
4
|
+
// write outcome, and activity ref carries (or views as) one — so a dev holding a thing can always point at
|
|
5
|
+
// it: no client-side UUID pre-minting, no loose {workspaceId, id} string pairs, no re-query-and-diff.
|
|
6
|
+
//
|
|
7
|
+
// A NomosRef is a LOCATOR (like ForeignRef, which remains the TYPED per-field view): `resolve` is an
|
|
8
|
+
// informational read over the device's multi-workspace plane; law-grade attestation stays on the declared
|
|
9
|
+
// attested-read lane. Pure nomos_types — no bridge/lifecycle imports.
|
|
10
|
+
library;
|
|
11
|
+
|
|
12
|
+
import 'foreign_ref.dart';
|
|
13
|
+
|
|
14
|
+
/// The canonical typed reference to an aggregate instance: workspace + aggregate type + instance id.
|
|
15
|
+
/// Value semantics; `uri` is the stable interop string (`nomos://<workspace>/<type>/<id>`).
|
|
16
|
+
class NomosRef {
|
|
17
|
+
/// The workspace holding the instance.
|
|
18
|
+
final String workspace;
|
|
19
|
+
|
|
20
|
+
/// The aggregate TYPE (the read-projection type tag, e.g. `estate.EstateSite` or `EstateSite`).
|
|
21
|
+
final String type;
|
|
22
|
+
|
|
23
|
+
/// The instance id (law-minted or law-defined).
|
|
24
|
+
final String id;
|
|
25
|
+
|
|
26
|
+
const NomosRef({required this.workspace, required this.type, required this.id});
|
|
27
|
+
|
|
28
|
+
/// The stable string form — survives URLs, nav params, JSON, logs. Segments are URI-encoded so a
|
|
29
|
+
/// workspace/type/id containing `/` or spaces round-trips.
|
|
30
|
+
String get uri =>
|
|
31
|
+
'nomos://${Uri.encodeComponent(workspace)}/${Uri.encodeComponent(type)}/${Uri.encodeComponent(id)}';
|
|
32
|
+
|
|
33
|
+
/// Parse a `nomos://<workspace>/<type>/<id>` string (the inverse of [uri]). Throws [FormatException]
|
|
34
|
+
/// on anything else.
|
|
35
|
+
factory NomosRef.parse(String uri) {
|
|
36
|
+
final u = Uri.tryParse(uri);
|
|
37
|
+
if (u == null || u.scheme != 'nomos') {
|
|
38
|
+
throw FormatException('NomosRef.parse: not a nomos:// uri', uri);
|
|
39
|
+
}
|
|
40
|
+
// nomos://<ws>/<type>/<id> — the workspace lands as the authority (host), type/id as path segments.
|
|
41
|
+
final ws = Uri.decodeComponent(u.host);
|
|
42
|
+
final segs = u.pathSegments.map(Uri.decodeComponent).toList();
|
|
43
|
+
if (ws.isEmpty || segs.length != 2 || segs[0].isEmpty || segs[1].isEmpty) {
|
|
44
|
+
throw FormatException('NomosRef.parse: expected nomos://<workspace>/<type>/<id>', uri);
|
|
45
|
+
}
|
|
46
|
+
return NomosRef(workspace: ws, type: segs[0], id: segs[1]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Accepts the `{workspace,type,id}` object OR the `nomos://` uri string.
|
|
50
|
+
factory NomosRef.fromJson(Object? j) {
|
|
51
|
+
if (j is String) return NomosRef.parse(j);
|
|
52
|
+
final m = (j as Map).cast<String, dynamic>();
|
|
53
|
+
return NomosRef(
|
|
54
|
+
workspace: (m['workspace'] ?? '') as String,
|
|
55
|
+
type: (m['type'] ?? '') as String,
|
|
56
|
+
id: (m['id'] ?? '') as String,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Map<String, dynamic> toJson() => {'workspace': workspace, 'type': type, 'id': id};
|
|
61
|
+
|
|
62
|
+
/// Informational lookup of the referenced instance over the device's multi-workspace plane (the same
|
|
63
|
+
/// reader `ForeignRef.resolve` uses — `bridge.foreignReader()` in nomos_client). Returns the raw
|
|
64
|
+
/// `{type,id,data}` rows for the id in the ref's workspace (empty ⇒ not found / not visible).
|
|
65
|
+
Future<List<Map<String, dynamic>>> resolve(NomosForeignReader nomos) =>
|
|
66
|
+
nomos.foreignWorkspace(workspace).queryById(id);
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
bool operator ==(Object other) =>
|
|
70
|
+
other is NomosRef && other.workspace == workspace && other.type == type && other.id == id;
|
|
71
|
+
@override
|
|
72
|
+
int get hashCode => Object.hash(workspace, type, id);
|
|
73
|
+
@override
|
|
74
|
+
String toString() => uri;
|
|
75
|
+
}
|
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.1.
|
|
14
|
+
version: 0.1.3
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.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/activity.ts
CHANGED
|
@@ -27,7 +27,9 @@ export type ActivitySeverity = "info" | "success" | "warning" | "critical";
|
|
|
27
27
|
export interface ActivityRefInput {
|
|
28
28
|
/** The target type — an aggregate handle or a `contract(...)` (reuse contracts for the nav target). */
|
|
29
29
|
readonly type: AggregateHandle | ContractHandle;
|
|
30
|
-
/** The payload field path holding the target's id (dotted for nested, e.g. `"location.siteId"`)
|
|
30
|
+
/** The payload field path holding the target's id (dotted for nested, e.g. `"location.siteId"`), OR the
|
|
31
|
+
* sentinel `"@created"` — the id the LAW MINTED for this intent's created aggregate of `type` (read from
|
|
32
|
+
* the sealed Create events at render time; the fix for "a .creates payload carries no id"). */
|
|
31
33
|
readonly from: string;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -106,7 +108,12 @@ function payloadFieldNames(schema: unknown): Set<string> | undefined {
|
|
|
106
108
|
* declared param; every ref/param `from` ROOT names a real payload field (when the payload shape is
|
|
107
109
|
* introspectable). Returns the serializable {@link ActivityDescriptor}. Pure.
|
|
108
110
|
*/
|
|
109
|
-
export function normalizeActivity(
|
|
111
|
+
export function normalizeActivity(
|
|
112
|
+
directiveId: string,
|
|
113
|
+
input: ActivityInput,
|
|
114
|
+
payloadSchema?: unknown,
|
|
115
|
+
createdAggregateIds?: readonly string[],
|
|
116
|
+
): ActivityDescriptor {
|
|
110
117
|
const where = `directive '${directiveId}' .activity(...)`;
|
|
111
118
|
if (typeof input.kind !== "string" || input.kind.trim().length === 0) {
|
|
112
119
|
throw new Error(`${where}: 'kind' must be a non-empty stable string (e.g. "site.created").`);
|
|
@@ -148,10 +155,21 @@ export function normalizeActivity(directiveId: string, input: ActivityInput, pay
|
|
|
148
155
|
}
|
|
149
156
|
return { name, type: targetId as string, from: r.from };
|
|
150
157
|
});
|
|
151
|
-
//
|
|
158
|
+
// "@created" refs: the id comes from the intent's sealed Create events, not the payload — so the
|
|
159
|
+
// directive must actually CREATE that aggregate type (when the creates-set is known, enforce it).
|
|
160
|
+
for (const r of refs) {
|
|
161
|
+
if (r.from === "@created" && createdAggregateIds !== undefined && !createdAggregateIds.includes(r.type)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`${where}: ref '${r.name}' uses "@created" but this directive does not .creates ${r.type} ` +
|
|
164
|
+
`(creates: ${createdAggregateIds.length > 0 ? createdAggregateIds.join(", ") : "nothing"}). ` +
|
|
165
|
+
`"@created" binds to the law-minted id of a created aggregate.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Best-effort payload-field existence check (root of a dotted path). "@created" is not a payload path.
|
|
152
170
|
const fields = payloadFieldNames(payloadSchema);
|
|
153
171
|
if (fields !== undefined) {
|
|
154
|
-
for (const { name, from } of [...params, ...refs.map((r) => ({ name: `ref ${r.name}`, from: r.from }))]) {
|
|
172
|
+
for (const { name, from } of [...params, ...refs.filter((r) => r.from !== "@created").map((r) => ({ name: `ref ${r.name}`, from: r.from }))]) {
|
|
155
173
|
const root = from.split(".")[0]!;
|
|
156
174
|
if (!fields.has(root)) {
|
|
157
175
|
throw new Error(
|
package/src/codegen_dart.ts
CHANGED
|
@@ -1197,6 +1197,10 @@ function emitAggregate(
|
|
|
1197
1197
|
` /// indexed/by-id accessor matched on (e.g. the siteId). Empty string only when`,
|
|
1198
1198
|
` /// the aggregate is built from a bare \`data\` map with no row id (e.g. a unit test).`,
|
|
1199
1199
|
` final ${idType} aggregateId;`,
|
|
1200
|
+
` /// THE ONE REFERENCE (architecture/nomos_ref.md): this instance's canonical, workspace-qualified`,
|
|
1201
|
+
` /// [NomosRef] (\`nomos://<workspace>/<type>/<id>\`) — composed at decode from the session workspace +`,
|
|
1202
|
+
` /// the row's type/id. Workspace is '' when decoded off the bare bridge (the client accessors fill it).`,
|
|
1203
|
+
` final NomosRef ref;`,
|
|
1200
1204
|
...specs.flatMap((s) => {
|
|
1201
1205
|
const doc = s.nullReason !== undefined ? [` /// null semantic: ${s.nullReason}`] : [];
|
|
1202
1206
|
// NULL SEMANTICS: a null-admitting field is the sealed tri-state slot (Omitted / ExplicitNull /
|
|
@@ -1208,6 +1212,7 @@ function emitAggregate(
|
|
|
1208
1212
|
].join("\n");
|
|
1209
1213
|
const ctorParams = [
|
|
1210
1214
|
` this.aggregateId = const ${idType}.fromRow(''),`,
|
|
1215
|
+
` this.ref = const NomosRef(workspace: '', type: '${agg.id}', id: ''),`,
|
|
1211
1216
|
...specs.map(
|
|
1212
1217
|
// A required scalar stays `required`; an optional scalar OR a collection is not
|
|
1213
1218
|
// `required` (an absent optional scalar is null; an absent collection is empty).
|
|
@@ -1229,6 +1234,7 @@ function emitAggregate(
|
|
|
1229
1234
|
// threaded from the row id (GAP 2) — defaulting to '' for a bare-map call.
|
|
1230
1235
|
const parseLines = [
|
|
1231
1236
|
` aggregateId: ${aggregateIdFromRowExpr(agg.id, "id")},`,
|
|
1237
|
+
` ref: NomosRef(workspace: workspace, type: type.isEmpty ? '${agg.id}' : type, id: id),`,
|
|
1232
1238
|
...specs.map((s) =>
|
|
1233
1239
|
s.nullAdmits === true
|
|
1234
1240
|
? ` ${s.name}: NomosSlot.decode<${stripNullable(s.dartType)}>(data, '${s.wireName}', ${slotDecodeCallback(s, "aggregate")}),`
|
|
@@ -1281,7 +1287,10 @@ function emitAggregate(
|
|
|
1281
1287
|
` /// decoded to bare JSON), carrying the kernel aggregate [id] from the row \`id\``,
|
|
1282
1288
|
` /// (defaults to '' for a bare-map call). PARTIAL folds: every scalar decodes`,
|
|
1283
1289
|
` /// \`null\` when absent (never throws); absent collections decode empty.`,
|
|
1284
|
-
`
|
|
1290
|
+
` ///`,
|
|
1291
|
+
` /// [workspace] + [type] compose the canonical [ref] (both optional — the client accessors thread the`,
|
|
1292
|
+
` /// session workspace + the row type; a bare decode leaves workspace '' and falls back to the type '${agg.id}').`,
|
|
1293
|
+
` factory ${className}.fromJson(Map<String, dynamic> data, [String id = '', String workspace = '', String type = '']) {`,
|
|
1285
1294
|
` return ${className}(`,
|
|
1286
1295
|
parseLines,
|
|
1287
1296
|
` );`,
|
|
@@ -2034,9 +2043,14 @@ const dartLit = (s: string): string => `'${s.replace(/\\/g, "\\\\").replace(/'/g
|
|
|
2034
2043
|
function emitDartActivityArm(e: PackageActivityEntry): string[] {
|
|
2035
2044
|
const { directiveId, desc } = e;
|
|
2036
2045
|
const paramEntries = desc.params.map((p) => ` ${dartLit(p.name)}: _activityPayloadPath(payload, ${dartLit(p.from)}),`);
|
|
2037
|
-
const refEntries = desc.refs.map(
|
|
2038
|
-
|
|
2039
|
-
|
|
2046
|
+
const refEntries = desc.refs.map((r) => {
|
|
2047
|
+
// A `from: "@created"` ref reads the id the LAW MINTED (the row's `created` list, first type match); any
|
|
2048
|
+
// other `from` is a payload path. Every ref carries the session workspace (the row's `workspace`).
|
|
2049
|
+
const id = r.from === "@created"
|
|
2050
|
+
? `_activityCreatedId(created, ${dartLit(r.type)})`
|
|
2051
|
+
: `_activityPayloadPath(payload, ${dartLit(r.from)})`;
|
|
2052
|
+
return ` ${dartLit(r.name)}: ActivityRef(workspace: workspace, type: ${dartLit(r.type)}, id: ${id}),`;
|
|
2053
|
+
});
|
|
2040
2054
|
return [
|
|
2041
2055
|
` case ${dartLit(directiveId)}:`,
|
|
2042
2056
|
` {`,
|
|
@@ -2113,12 +2127,28 @@ export function generateDartActivityRegistry(entries: readonly PackageActivityEn
|
|
|
2113
2127
|
` return cur == null ? '' : '\$cur';`,
|
|
2114
2128
|
`}`,
|
|
2115
2129
|
``,
|
|
2130
|
+
`/// The id the LAW minted for a \`from: "@created"\` ref — the first entry of the row's \`created\` list`,
|
|
2131
|
+
`/// whose type matches the declared aggregate (the runtime type is uuid-stripped + may be namespaced, so`,
|
|
2132
|
+
`/// match on equality OR \`<domain>.<Type>\` suffix). '' when the holon returned no created list (era-safe).`,
|
|
2133
|
+
`String _activityCreatedId(List<Map<String, dynamic>> created, String declared) {`,
|
|
2134
|
+
` for (final c in created) {`,
|
|
2135
|
+
` final t = (c['type'] ?? '') as String;`,
|
|
2136
|
+
` if (t == declared || t.endsWith('.\$declared')) return (c['id'] ?? '') as String;`,
|
|
2137
|
+
` }`,
|
|
2138
|
+
` return '';`,
|
|
2139
|
+
`}`,
|
|
2140
|
+
``,
|
|
2116
2141
|
`/// Fold ONE committed-intent raw row into a typed [ActivityEvent], or null when the directive declares`,
|
|
2117
|
-
`/// no \`.activity(...)\`. rawRow = { intentId, domain, directiveId, actorSubject, occurredAt, payload
|
|
2142
|
+
`/// no \`.activity(...)\`. rawRow = { intentId, domain, directiveId, actorSubject, occurredAt, payload,`,
|
|
2143
|
+
`/// created, workspace } — \`created\` (\`[{type,id}]\`) backs \`from: "@created"\` refs; \`workspace\` stamps them.`,
|
|
2118
2144
|
`ActivityEvent? renderActivity(Map<String, dynamic> rawRow) {`,
|
|
2119
2145
|
` final directiveId = (rawRow['directiveId'] ?? '') as String;`,
|
|
2120
2146
|
` final actorSubject = (rawRow['actorSubject'] ?? '') as String;`,
|
|
2147
|
+
` final workspace = (rawRow['workspace'] ?? '') as String;`,
|
|
2121
2148
|
` final payload = ((rawRow['payload'] as Map?)?.cast<String, dynamic>()) ?? const <String, dynamic>{};`,
|
|
2149
|
+
` final created = ((rawRow['created'] as List?) ?? const <dynamic>[])`,
|
|
2150
|
+
` .map((e) => (e as Map).cast<String, dynamic>())`,
|
|
2151
|
+
` .toList();`,
|
|
2122
2152
|
` switch (directiveId) {`,
|
|
2123
2153
|
...sorted.flatMap(emitDartActivityArm),
|
|
2124
2154
|
` default:`,
|
|
@@ -2524,6 +2554,10 @@ function emitReadModel(
|
|
|
2524
2554
|
` /// The kernel aggregate id (the \`{type,id,data}\` row's \`id\`, e.g. the siteId).`,
|
|
2525
2555
|
` /// Empty string only when built from a bare \`data\` map with no row id (a unit test).`,
|
|
2526
2556
|
` final ${idType} aggregateId;`,
|
|
2557
|
+
` /// THE ONE REFERENCE (architecture/nomos_ref.md): this row's canonical, workspace-qualified [NomosRef]`,
|
|
2558
|
+
` /// (\`nomos://<workspace>/<type>/<id>\`) — composed at decode from the session workspace + the row's`,
|
|
2559
|
+
` /// type/id. The generated client accessors thread the session workspace; a bare decode leaves it ''.`,
|
|
2560
|
+
` final NomosRef ref;`,
|
|
2527
2561
|
...specs.flatMap((s) => {
|
|
2528
2562
|
const doc = s.nullReason !== undefined ? [` /// null semantic: ${s.nullReason}`] : [];
|
|
2529
2563
|
return s.nullAdmits === true
|
|
@@ -2541,6 +2575,7 @@ function emitReadModel(
|
|
|
2541
2575
|
].join("\n");
|
|
2542
2576
|
const ctorParams = [
|
|
2543
2577
|
` this.aggregateId = const ${idType}.fromRow(''),`,
|
|
2578
|
+
` this.ref = const NomosRef(workspace: '', type: '${agg.id}', id: ''),`,
|
|
2544
2579
|
...specs.map((s) =>
|
|
2545
2580
|
// A required field is `required`; an optional scalar, a collection, OR a
|
|
2546
2581
|
// permissive json leaf (already nullable `Object?`) takes its default/null.
|
|
@@ -2558,6 +2593,7 @@ function emitReadModel(
|
|
|
2558
2593
|
|
|
2559
2594
|
const parseLines = [
|
|
2560
2595
|
` aggregateId: ${aggregateIdFromRowExpr(agg.id, "id")},`,
|
|
2596
|
+
` ref: NomosRef(workspace: workspace, type: type.isEmpty ? '${agg.id}' : type, id: id),`,
|
|
2561
2597
|
...specs.map((s) =>
|
|
2562
2598
|
s.nullAdmits === true
|
|
2563
2599
|
? ` ${s.name}: NomosSlot.decode<${stripNullable(readModelDartType(s))}>(data, '${s.wireName}', ${slotDecodeCallback(s, "readModel")}),`
|
|
@@ -2591,7 +2627,10 @@ function emitReadModel(
|
|
|
2591
2627
|
` /// already decoded to bare/structured JSON), carrying the kernel aggregate [id] from`,
|
|
2592
2628
|
` /// the row \`id\` (defaults to '' for a bare-map call). PARTIAL folds: every scalar`,
|
|
2593
2629
|
` /// decodes \`null\` when absent (never throws); absent collections decode empty.`,
|
|
2594
|
-
`
|
|
2630
|
+
` ///`,
|
|
2631
|
+
` /// [workspace] + [type] compose the canonical [ref] (both optional — the client accessors thread the`,
|
|
2632
|
+
` /// session workspace + the row type; a bare decode leaves workspace '' and falls back to the type '${agg.id}').`,
|
|
2633
|
+
` factory ${className}.fromProjected(Map<String, dynamic> data, [String id = '', String workspace = '', String type = '']) {`,
|
|
2595
2634
|
` return ${className}(`,
|
|
2596
2635
|
parseLines,
|
|
2597
2636
|
` );`,
|
|
@@ -87,10 +87,11 @@ function dartKeyParam(key: string): string {
|
|
|
87
87
|
return safeMethod(safe);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/** Parse ONE bridge row (`{type,id,data}`) into a typed read model
|
|
90
|
+
/** Parse ONE bridge row (`{type,id,data}`) into a typed read model, composing the canonical `.ref` from the
|
|
91
|
+
* session workspace (`_ws`, a getter on the client) + the row's type/id. Works for a `Map` (one-shot
|
|
91
92
|
* `query`/`queryById` rows) or a `dynamic` (`watch` stream rows) — indexing resolves on both. */
|
|
92
93
|
function rowToModel(model: string, rowVar: string): string {
|
|
93
|
-
return `${model}.fromProjected((${rowVar}['data'] as Map).cast<String, dynamic>(), (${rowVar}['id'] ?? '').toString())`;
|
|
94
|
+
return `${model}.fromProjected((${rowVar}['data'] as Map).cast<String, dynamic>(), (${rowVar}['id'] ?? '').toString(), _ws, (${rowVar}['type'] ?? '').toString())`;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
export interface DartBridgeClientOptions {
|
|
@@ -199,16 +200,19 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
199
200
|
// E2E SEALING (the sealing client): a directive with `.encrypted()` payload fields SEALS them via the
|
|
200
201
|
// bound [keyStore] before dispatch (a missing key throws [EncryptionKeyUnavailable] — never a plaintext
|
|
201
202
|
// write). One async block covers creates + mutate + gated + stamps; `seal: false` is the escape hatch.
|
|
203
|
+
// THE ONE WRITE OUTCOME (architecture/nomos_ref.md): EVERY directive method returns Future<NomosOfferOutcome>
|
|
204
|
+
// via the ONE lane `bridge.offerOutcome` — `outcome.created` carries every minted aggregate (each with its
|
|
205
|
+
// typed `.ref`), `outcome.head` the new local head. The breaking cut: creates AND non-creates alike.
|
|
206
|
+
const createsDoc = (d as { marker?: string }).marker === "creates"
|
|
207
|
+
? ` The LAW mints the ${(d as { aggregateId?: string }).aggregateId ?? "aggregate"} id inside the sealed intent — it rides back on \`outcome.created\` (no id in the payload, no query-after).`
|
|
208
|
+
: "";
|
|
209
|
+
const stampsDoc = stamps.length > 0 ? `\n /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.` : "";
|
|
202
210
|
if (encMap?.directives?.[d.id] !== undefined) {
|
|
203
|
-
const isCreates = (d as { marker?: string }).marker === "creates";
|
|
204
|
-
const retType = isCreates ? "NomosOfferOutcome" : "String";
|
|
205
|
-
const offerFn = isCreates ? "offerCreates" : "offerDirective";
|
|
206
211
|
methods.push(
|
|
207
|
-
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the
|
|
208
|
-
` /// Encrypted field(s) are SEALED via the bound [keyStore] before dispatch (pass \`seal: false\` to skip)
|
|
209
|
-
...(stamps.length > 0 ? [` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.`] : []),
|
|
212
|
+
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}`,
|
|
213
|
+
` /// Encrypted field(s) are SEALED via the bound [keyStore] before dispatch (pass \`seal: false\` to skip).${stampsDoc}`,
|
|
210
214
|
...(gateDoc ? [gateDoc] : []),
|
|
211
|
-
` Future
|
|
215
|
+
` Future<NomosOfferOutcome> ${method}(${payloadClass} payload, {bool seal = true}) async {`,
|
|
212
216
|
...(gateGuard ? [gateGuard] : []),
|
|
213
217
|
` final data = payload.toPayloadJson();`,
|
|
214
218
|
...(stamps.length > 0
|
|
@@ -218,64 +222,42 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
218
222
|
]
|
|
219
223
|
: []),
|
|
220
224
|
` await _sealPayload(data, ${dartStr(d.id)}, seal);`,
|
|
221
|
-
` return bridge
|
|
225
|
+
` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: data, actor: actor, domainHash: domainHash);`,
|
|
222
226
|
` }`,
|
|
223
227
|
``,
|
|
224
228
|
);
|
|
225
229
|
continue;
|
|
226
230
|
}
|
|
227
|
-
if (
|
|
228
|
-
// LAW-MINTED ID RETURN: a `.creates` plan mints its aggregate id inside the sealed intent (the payload
|
|
229
|
-
// carries none — callers never supply ids). The bridge's offerCreates hands the minted ids back with the
|
|
230
|
-
// head, so the create method returns the typed id with NO query-after round-trip.
|
|
231
|
+
if (stamps.length > 0) {
|
|
231
232
|
methods.push(
|
|
232
|
-
` /// Author \`${domain}/${d.id}\`
|
|
233
|
-
` /// intent; it returns here (\`outcome.created\`) with the new local head. No id in the payload, no query-after.`,
|
|
234
|
-
...(stamps.length > 0 ? [` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.`] : []),
|
|
233
|
+
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}${stampsDoc}`,
|
|
235
234
|
...(gateDoc ? [gateDoc] : []),
|
|
236
235
|
` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) {`,
|
|
237
236
|
...(gateGuard ? [gateGuard] : []),
|
|
238
237
|
` final data = payload.toPayloadJson();`,
|
|
239
|
-
...(stamps.length > 0
|
|
240
|
-
? [
|
|
241
|
-
` final now = DateTime.now().toUtc().toIso8601String();`,
|
|
242
|
-
...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
|
|
243
|
-
]
|
|
244
|
-
: []),
|
|
245
|
-
` return bridge.offerCreates(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: data, actor: actor, domainHash: domainHash);`,
|
|
246
|
-
` }`,
|
|
247
|
-
``,
|
|
248
|
-
);
|
|
249
|
-
} else if (stamps.length > 0) {
|
|
250
|
-
methods.push(
|
|
251
|
-
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
|
|
252
|
-
` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.`,
|
|
253
|
-
...(gateDoc ? [gateDoc] : []),
|
|
254
|
-
` Future<String> ${method}(${payloadClass} payload) {`,
|
|
255
|
-
...(gateGuard ? [gateGuard] : []),
|
|
256
|
-
` final data = payload.toPayloadJson();`,
|
|
257
238
|
` final now = DateTime.now().toUtc().toIso8601String();`,
|
|
258
239
|
...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
|
|
259
|
-
` return bridge.
|
|
240
|
+
` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: data, actor: actor, domainHash: domainHash);`,
|
|
260
241
|
` }`,
|
|
261
242
|
``,
|
|
262
243
|
);
|
|
263
244
|
} else if (gateGuard) {
|
|
264
245
|
// A gated directive with no `…At` sugar still needs a block body for the throw guard.
|
|
265
246
|
methods.push(
|
|
266
|
-
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the
|
|
247
|
+
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}`,
|
|
267
248
|
gateDoc,
|
|
268
|
-
` Future<
|
|
249
|
+
` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) {`,
|
|
269
250
|
gateGuard,
|
|
270
|
-
` return bridge.
|
|
251
|
+
` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: payload.toPayloadJson(), actor: actor, domainHash: domainHash);`,
|
|
271
252
|
` }`,
|
|
272
253
|
``,
|
|
273
254
|
);
|
|
274
255
|
} else {
|
|
275
256
|
methods.push(
|
|
276
|
-
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the
|
|
277
|
-
|
|
278
|
-
`
|
|
257
|
+
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}`,
|
|
258
|
+
...(gateDoc ? [gateDoc] : []),
|
|
259
|
+
` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) =>`,
|
|
260
|
+
` bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: payload.toPayloadJson(), actor: actor, domainHash: domainHash);`,
|
|
279
261
|
``,
|
|
280
262
|
);
|
|
281
263
|
}
|
|
@@ -440,7 +422,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
440
422
|
`// client file gives an app the whole typed surface.`,
|
|
441
423
|
...(opts.typesImportUri !== undefined ? [`export '${opts.typesImportUri}';`] : [`export '${mod.name}.dart';`]),
|
|
442
424
|
...(opts.typesImportUri === undefined && opts.frameworkImportUri !== undefined ? [`export '${opts.frameworkImportUri}';`] : []),
|
|
443
|
-
`export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion, NomosCompat, NomosCompatMember${encPresent ? ", NomosScopeKey, NomosKeyStore, EncryptionKeyUnavailable, WrappedKeyStore, NomosEncryption, NomosEpochResult, EncryptionMemberKeyMissing, EncryptionRosterMismatch" : ""};`,
|
|
425
|
+
`export '${supportImport}' show NomosBridge, NomosSession, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion, NomosCompat, NomosCompatMember, NomosOfferOutcome, NomosCreated, NomosRef${encPresent ? ", NomosScopeKey, NomosKeyStore, EncryptionKeyUnavailable, WrappedKeyStore, NomosEncryption, NomosEpochResult, EncryptionMemberKeyMissing, EncryptionRosterMismatch" : ""};`,
|
|
444
426
|
``,
|
|
445
427
|
`/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
|
|
446
428
|
`const String ${hashConst} = ${dartStr(opts.domainHash)};`,
|
|
@@ -490,6 +472,13 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
490
472
|
: []),
|
|
491
473
|
` ${className}(this.bridge, {this.domainHash = ${hashConst}, this.actor${encPresent ? ", this.keyStore" : ""}});`,
|
|
492
474
|
``,
|
|
475
|
+
` /// The session workspace this client is bound to (from the [NomosSession] handle, when constructed with`,
|
|
476
|
+
` /// one) — the \`workspace\` of every read row's canonical [NomosRef]. '' on a bare (non-session) bridge.`,
|
|
477
|
+
` String get _ws {`,
|
|
478
|
+
` final b = bridge;`,
|
|
479
|
+
` return b is NomosSession ? b.workspace : '';`,
|
|
480
|
+
` }`,
|
|
481
|
+
``,
|
|
493
482
|
...(encPresent ? dartEncClassMembers(encMap!) : []),
|
|
494
483
|
// THE OWNER CEREMONY (slice 2): grantAndWrap / revokeAndRotate / rotate — the operator-blind key-epoch
|
|
495
484
|
// handshake as one call. Bind `actor` (the owner) to author the fan-out. Only for a domain with a scope.
|
package/src/codegen_ts.ts
CHANGED
|
@@ -679,7 +679,14 @@ function emitTsActivityBlock(entries: readonly PackageActivityEntry[]): string[]
|
|
|
679
679
|
const { directiveId, desc } = e;
|
|
680
680
|
const params = desc.params.map((p) => `${_actLit(p.name)}: _activityPayloadPath(row.payload, ${_actLit(p.from)})`).join(", ");
|
|
681
681
|
const refs = desc.refs
|
|
682
|
-
.map((r) =>
|
|
682
|
+
.map((r) => {
|
|
683
|
+
// A `from: "@created"` ref reads the id the LAW MINTED (the row's `created` list, first type match);
|
|
684
|
+
// any other `from` is a payload path. Every ref carries the session workspace (the row's `workspace`).
|
|
685
|
+
const id = r.from === "@created"
|
|
686
|
+
? `_activityCreatedId(row.created, ${_actLit(r.type)})`
|
|
687
|
+
: `_activityPayloadPath(row.payload, ${_actLit(r.from)})`;
|
|
688
|
+
return `${_actLit(r.name)}: { workspace: row.workspace ?? "", type: ${_actLit(r.type)}, id: ${id} }`;
|
|
689
|
+
})
|
|
683
690
|
.join(", ");
|
|
684
691
|
return [
|
|
685
692
|
` ${_actLit(directiveId)}: (row: ActivityRawRow): ActivityEvent => {`,
|
|
@@ -699,8 +706,9 @@ function emitTsActivityBlock(entries: readonly PackageActivityEntry[]): string[]
|
|
|
699
706
|
};
|
|
700
707
|
return [
|
|
701
708
|
`// ── activity feed (architecture/activity_descriptors.md) — typed rows folded from the intent log ──`,
|
|
702
|
-
`/** A declared navigation target on an activity row
|
|
703
|
-
`
|
|
709
|
+
`/** A declared navigation target on an activity row: the canonical [NomosRef] triple (workspace + type +`,
|
|
710
|
+
` * resolved id). A \`from: "@created"\` ref resolves the id the LAW minted; others a payload path. */`,
|
|
711
|
+
`export interface ActivityRef { workspace: string; type: string; id: string }`,
|
|
704
712
|
`/** A rendered activity row — produced by \`renderActivity\` from a directive's declared \`.activity(...)\`. */`,
|
|
705
713
|
`export interface ActivityEvent {`,
|
|
706
714
|
` eventId: string; kind: string; actorSubject: string; occurredAt: string;`,
|
|
@@ -711,7 +719,7 @@ function emitTsActivityBlock(entries: readonly PackageActivityEntry[]): string[]
|
|
|
711
719
|
` domain: string; directiveId: string;`,
|
|
712
720
|
`}`,
|
|
713
721
|
`/** The runtime's per-committed-intent RAW ROW (the shape \`holon.watchActivity\`/\`activityLog\` yield). */`,
|
|
714
|
-
`export interface ActivityRawRow { intentId: string; domain: string; directiveId: string; actorSubject: string; occurredAt: string; payload: Record<string, unknown
|
|
722
|
+
`export interface ActivityRawRow { intentId: string; domain: string; directiveId: string; actorSubject: string; occurredAt: string; payload: Record<string, unknown>; created?: { type: string; id: string }[]; workspace?: string }`,
|
|
715
723
|
`/** A NARRATIVE over the activity log (for an in-app LLM): the filtered events + a plain-English \`transcript\``,
|
|
716
724
|
` * (ONE LINE PER EVENT, oldest-first, \`"<occurredAt> — <title>: <message>"\`) — grounding text over the`,
|
|
717
725
|
` * VERIFIED log, never raw payloads. Produced by the client's \`describe\` / \`describeHistory\`. */`,
|
|
@@ -729,6 +737,14 @@ function emitTsActivityBlock(entries: readonly PackageActivityEntry[]): string[]
|
|
|
729
737
|
` }`,
|
|
730
738
|
` return cur === null || cur === undefined ? "" : String(cur);`,
|
|
731
739
|
`}`,
|
|
740
|
+
`/** The id the LAW minted for a \`from: "@created"\` ref — the first entry of the row's \`created\` list whose`,
|
|
741
|
+
` * type matches the declared aggregate (the runtime type is uuid-stripped + may be namespaced, so match on`,
|
|
742
|
+
` * equality OR \`<domain>.<Type>\` suffix). "" when the holon returned no created list (era-safe). */`,
|
|
743
|
+
`function _activityCreatedId(created: { type: string; id: string }[] | undefined, declared: string): string {`,
|
|
744
|
+
` if (created === undefined) return "";`,
|
|
745
|
+
` for (const c of created) if (c.type === declared || c.type.endsWith("." + declared)) return c.id;`,
|
|
746
|
+
` return "";`,
|
|
747
|
+
`}`,
|
|
732
748
|
`/** Interpolate \`{name}\` placeholders in a template from vars (a missing var renders empty). */`,
|
|
733
749
|
`function _renderActivityTemplate(template: string, vars: Record<string, string>): string {`,
|
|
734
750
|
` return template.replace(/\\{([a-zA-Z0-9_]+)\\}/g, (_m, name: string) => vars[name] ?? "");`,
|
|
@@ -813,8 +829,46 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
813
829
|
``,
|
|
814
830
|
]
|
|
815
831
|
: []),
|
|
816
|
-
`/**
|
|
817
|
-
`
|
|
832
|
+
`/** THE ONE REFERENCE (architecture/nomos_ref.md) — the canonical, workspace-qualified identity of an`,
|
|
833
|
+
` * aggregate instance: \`{ workspace, type, id }\`, with the stable interop string`,
|
|
834
|
+
` * \`nomos://<workspace>/<type>/<id>\`. Carried on every read \`Row.ref\` + write outcome + activity ref. */`,
|
|
835
|
+
`export interface NomosRef { workspace: string; type: string; id: string }`,
|
|
836
|
+
`/** The stable \`nomos://<workspace>/<type>/<id>\` string form (each segment URI-encoded — a workspace/`,
|
|
837
|
+
` * type/id containing \`/\` or spaces round-trips). Survives URLs, nav params, JSON, logs. */`,
|
|
838
|
+
`export function nomosRefUri(ref: NomosRef): string {`,
|
|
839
|
+
` return "nomos://" + encodeURIComponent(ref.workspace) + "/" + encodeURIComponent(ref.type) + "/" + encodeURIComponent(ref.id);`,
|
|
840
|
+
`}`,
|
|
841
|
+
`/** Parse a \`nomos://<workspace>/<type>/<id>\` string back into a [NomosRef] (the inverse of [nomosRefUri]);`,
|
|
842
|
+
` * throws on anything else. */`,
|
|
843
|
+
`export function parseNomosRef(uri: string): NomosRef {`,
|
|
844
|
+
" const m = /^nomos:\\/\\/([^/]+)\\/([^/]+)\\/([^/]+)$/.exec(uri);",
|
|
845
|
+
` if (m === null) throw new Error("parseNomosRef: expected nomos://<workspace>/<type>/<id>: " + uri);`,
|
|
846
|
+
` return { workspace: decodeURIComponent(m[1]!), type: decodeURIComponent(m[2]!), id: decodeURIComponent(m[3]!) };`,
|
|
847
|
+
`}`,
|
|
848
|
+
`/** One projection row: the aggregate's wire type, its id, the folded fields, and the canonical [NomosRef]`,
|
|
849
|
+
` * (composed at decode from the session workspace + the row's type/id — additive, the \`data\` is untouched). */`,
|
|
850
|
+
`export interface Row<T> { type: string; id: string; data: T; ref: NomosRef }`,
|
|
851
|
+
`/** Stamp each row's canonical [NomosRef] from the session \`ws\` + the row's type/id (additive — already-`,
|
|
852
|
+
` * stamped rows pass through). */`,
|
|
853
|
+
`function _stampRefs<T>(ws: string, rows: Row<T>[]): Row<T>[] {`,
|
|
854
|
+
` return rows.map((r) => (r.ref !== undefined ? r : { ...r, ref: { workspace: ws, type: r.type, id: r.id } }));`,
|
|
855
|
+
`}`,
|
|
856
|
+
`function _stampRefsP<T>(ws: string, p: Promise<Row<T>[]>): Promise<Row<T>[]> { return p.then((rows) => _stampRefs(ws, rows)); }`,
|
|
857
|
+
`/** THE TYPED WRITE OUTCOME (architecture/nomos_ref.md) — every generated directive method returns this.`,
|
|
858
|
+
` * \`created\` is EVERY aggregate the plan minted (typed [NomosRef]s, composed with the session workspace;`,
|
|
859
|
+
` * \`[]\` on a holon too old to return them — never a fake id). \`target\` is the addressed instance for a`,
|
|
860
|
+
` * front-door-mint lane (statically known); omitted otherwise. \`head\` is the new local head. */`,
|
|
861
|
+
`export interface NomosOutcome { head: string; created: NomosRef[]; target?: NomosRef }`,
|
|
862
|
+
`/** Fold a holon offer result (a bare head, or \`{ head, created }\` under returnCreated) into a [NomosOutcome]`,
|
|
863
|
+
` * — composing each created \`{type,id}\` with the session \`ws\`. \`target\` rides through when the caller`,
|
|
864
|
+
` * statically knows the addressed instance (the front-door mint). */`,
|
|
865
|
+
`async function _outcome(ws: string, r: Promise<string | { head: string; created?: { type: string; id: string }[] }>, target?: NomosRef): Promise<NomosOutcome> {`,
|
|
866
|
+
` const v = await r;`,
|
|
867
|
+
` const obj = typeof v === "object" && v !== null;`,
|
|
868
|
+
` const head = obj ? v.head : String(v);`,
|
|
869
|
+
` const created = (obj ? v.created ?? [] : []).map((c) => ({ workspace: ws, type: c.type, id: c.id }));`,
|
|
870
|
+
` return target !== undefined ? { head, created, target } : { head, created };`,
|
|
871
|
+
`}`,
|
|
818
872
|
// MULTI-WORKSPACE LOCATORS (t.foreignRef): a TYPED cross-workspace edge — the foreign aggregate's id +
|
|
819
873
|
// the foreign workspace id (from a sibling field). NOT an atomic FK: it does not clone/open the other
|
|
820
874
|
// holon; `resolve`/`watch` open it ON DEMAND over a realm (openRealm's `.workspace(ws)` surface).
|
|
@@ -899,6 +953,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
899
953
|
` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean }): Promise<string>;`,
|
|
900
954
|
` /** Opaque-bytes offer — retained for carriers that already hold an encoded envelope (back-compat). */`,
|
|
901
955
|
` offer(intentBytes: string | Uint8Array): Promise<string>;`,
|
|
956
|
+
` /** The workspace this holon is bound to — the \`workspace\` of every [NomosRef] this client composes. */`,
|
|
957
|
+
` workspace: string;`,
|
|
902
958
|
...(encPresent
|
|
903
959
|
? [
|
|
904
960
|
` /** \`keys\` (E2E sealing): the reader's held scope keys, injected so encrypted fields decrypt;`,
|
|
@@ -1341,8 +1397,12 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1341
1397
|
const foreignRefFields: { agg: string; field: string; workspaceField: string; target: string }[] = [];
|
|
1342
1398
|
// wrap a read accessor return expression through the foreignRef row-hydrator (identity + byte-identical
|
|
1343
1399
|
// when the package declares no foreignRef, so non-foreignRef packages are unchanged).
|
|
1344
|
-
|
|
1345
|
-
|
|
1400
|
+
// Every read accessor's rows get their canonical `.ref` stamped from `holon.workspace` (additive); a
|
|
1401
|
+
// foreignRef-declaring package ALSO hydrates its foreignRef fields into typed locators first.
|
|
1402
|
+
const wrapP = (expr: string): string =>
|
|
1403
|
+
`_stampRefsP(holon.workspace, ${foreignRefFields.length > 0 ? `_hydrateP(${expr})` : expr})`;
|
|
1404
|
+
const wrapRows = (expr: string): string =>
|
|
1405
|
+
`_stampRefs(holon.workspace, ${foreignRefFields.length > 0 ? `_hydrateRows(${expr})` : expr})`;
|
|
1346
1406
|
for (const [typeId, agg] of [...aggByType].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1347
1407
|
out.push(`export interface ${typeId}Data {`);
|
|
1348
1408
|
for (const [name, field] of Object.entries(agg.fields)) {
|
|
@@ -1663,6 +1723,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1663
1723
|
`export type ${payloadName} = ${tsTypeOfPayload(schema, stamps, mint ? [mint.field] : [], placed, lazyReg, d.id)};`,
|
|
1664
1724
|
``,
|
|
1665
1725
|
);
|
|
1726
|
+
// THE ONE WRITE OUTCOME (architecture/nomos_ref.md): EVERY directive method returns Promise<NomosOutcome>
|
|
1727
|
+
// — `created` (typed refs of all minted aggregates, from returnCreated) + `head`, plus `target` for the
|
|
1728
|
+
// front-door-mint lane (the one statically-known addressed instance). One async block covers every case.
|
|
1666
1729
|
if (mint !== undefined) {
|
|
1667
1730
|
const mintOpts =
|
|
1668
1731
|
mint.homeKeyField !== undefined
|
|
@@ -1675,65 +1738,31 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1675
1738
|
: `{ ...${pv}, ${mint.field} }`;
|
|
1676
1739
|
methods.push(
|
|
1677
1740
|
` /** Author \`${domain}/${d.id}\`: the front-door MINTS \`${mint.field}\` (route-tagged) when omitted`,
|
|
1678
|
-
` * — ids stay internal plumbing — and returns
|
|
1679
|
-
` ${d.id}: async (${paramSig}): Promise<
|
|
1741
|
+
` * — ids stay internal plumbing — and returns the outcome (\`target\` = the minted ${dd.aggregateId ?? "aggregate"}).${gate ? ` GATED: needs \`{ actor }\` (\`${gate.relation}\`).` : ""} */`,
|
|
1742
|
+
` ${d.id}: async (${paramSig}): Promise<NomosOutcome> => {`,
|
|
1680
1743
|
...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
|
|
1681
1744
|
...(sealPrelude ? [sealPrelude] : []),
|
|
1682
1745
|
` const ${mint.field} = payload.${mint.field} ?? (await (holon as NomosRoutedHolon).mint(${JSON.stringify(mint.mintType)}${mintOpts}));`,
|
|
1683
|
-
`
|
|
1684
|
-
` return { head, ${mint.field} };`,
|
|
1746
|
+
` return _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash, actor, returnCreated: true }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>, { workspace: holon.workspace, type: ${JSON.stringify(dd.aggregateId ?? mint.mintType)}, id: ${mint.field} });`,
|
|
1685
1747
|
` },`,
|
|
1686
1748
|
);
|
|
1687
1749
|
continue;
|
|
1688
1750
|
}
|
|
1689
1751
|
const offerPayload = stamps.length ? `stampNow(${pv}, ${JSON.stringify(stamps)})` : pv;
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
);
|
|
1705
|
-
continue;
|
|
1706
|
-
}
|
|
1707
|
-
const offerCall = `holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor })`;
|
|
1708
|
-
if (wantSeal) {
|
|
1709
|
-
// SEALING directive (optionally also gated): an ASYNC block — seal the encrypted fields via the
|
|
1710
|
-
// bound { keyStore } (+ throw MissingOfferAuthority first if gated) before dispatch.
|
|
1711
|
-
methods.push(
|
|
1712
|
-
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
|
|
1713
|
-
` * Encrypted field(s) are SEALED via the bound { keyStore } before dispatch (pass { seal: false } to skip).${gate ? ` GATED: needs \`{ actor }\` (\`${gate.relation}\`).` : ""} */`,
|
|
1714
|
-
` ${d.id}: async (${paramSig}): Promise<string> => {`,
|
|
1715
|
-
...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
|
|
1716
|
-
sealPrelude,
|
|
1717
|
-
` return ${offerCall};`,
|
|
1718
|
-
` },`,
|
|
1719
|
-
);
|
|
1720
|
-
} else if (gateGuard) {
|
|
1721
|
-
// GATED directive: a block body that throws MissingOfferAuthority when no actor is bound — an
|
|
1722
|
-
// actorless gated offer can't be constructed (the war: the gate is structural, not just kernel-side).
|
|
1723
|
-
methods.push(
|
|
1724
|
-
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
|
|
1725
|
-
` * GATED: needs \`{ actor }\` (the \`${gate!.relation}\` relation) — throws MissingOfferAuthority otherwise. */`,
|
|
1726
|
-
` ${d.id}: (payload: ${payloadName}): Promise<string> => {`,
|
|
1727
|
-
gateGuard.replace(/\n$/, ""),
|
|
1728
|
-
` return ${offerCall};`,
|
|
1729
|
-
` },`,
|
|
1730
|
-
);
|
|
1731
|
-
} else {
|
|
1732
|
-
methods.push(
|
|
1733
|
-
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head. */`,
|
|
1734
|
-
` ${d.id}: (payload: ${payloadName}) => ${offerCall},`,
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1752
|
+
const outcomeDoc =
|
|
1753
|
+
d.marker === "creates"
|
|
1754
|
+
? `Author \`${domain}/${d.id}\`: the LAW mints the ${d.aggregateId ?? "aggregate"} id inside the sealed intent; it rides back on \`outcome.created\` — no id in the payload, no query-after.`
|
|
1755
|
+
: `Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOutcome] (\`head\` + any \`created\` refs).`;
|
|
1756
|
+
const sealDoc = wantSeal ? " Encrypted field(s) are SEALED via the bound { keyStore } before dispatch (pass { seal: false } to skip)." : "";
|
|
1757
|
+
const gateDoc = gate ? ` GATED: needs \`{ actor }\` (\`${gate.relation}\`) — throws MissingOfferAuthority otherwise.` : "";
|
|
1758
|
+
methods.push(
|
|
1759
|
+
` /** ${outcomeDoc}${sealDoc}${gateDoc} */`,
|
|
1760
|
+
` ${d.id}: async (${paramSig}): Promise<NomosOutcome> => {`,
|
|
1761
|
+
...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
|
|
1762
|
+
...(sealPrelude ? [sealPrelude] : []),
|
|
1763
|
+
` return _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor, returnCreated: true }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>);`,
|
|
1764
|
+
` },`,
|
|
1765
|
+
);
|
|
1737
1766
|
}
|
|
1738
1767
|
|
|
1739
1768
|
for (const sp of (mod.spatials ?? []) as { id: string; of: string; on: string }[]) {
|
|
@@ -1806,9 +1835,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1806
1835
|
: ` list${agg.id}s: () => ${wrapP(`holon.query("aggregatesByType", { __type: ${JSON.stringify(agg.id)} }) as Promise<Row<${agg.id}Data>[]>`)},`,
|
|
1807
1836
|
` /** Watch one ${agg.id} by aggregate id (local reactive read; returns stop()). */`,
|
|
1808
1837
|
` watch${agg.id}ById: (id: string, cb: (rows: Row<${agg.id}Data>[]) => void, opts?: { intervalMs?: number }) =>`,
|
|
1809
|
-
|
|
1810
|
-
? ` holon.watchById(id, ((rows: Row<${agg.id}Data>[]) => cb(_hydrateRows(rows))) as (rows: unknown) => void, opts),`
|
|
1811
|
-
: ` holon.watchById(id, cb as (rows: unknown) => void, opts),`,
|
|
1838
|
+
` holon.watchById(id, ((rows: Row<${agg.id}Data>[]) => cb(${wrapRows("rows")})) as (rows: unknown) => void, opts),`,
|
|
1812
1839
|
);
|
|
1813
1840
|
}
|
|
1814
1841
|
|
|
@@ -431,6 +431,7 @@ const DART_SUPPORT_CORE_SRC = [
|
|
|
431
431
|
// model with a `t.foreignRef` field references `ForeignRef<Target>` through the barrel, so it must vendor.
|
|
432
432
|
"foreign_ref.dart",
|
|
433
433
|
"activity.dart",
|
|
434
|
+
"nomos_ref.dart",
|
|
434
435
|
] as const;
|
|
435
436
|
// The RUNTIME support files, vendored from `nomos_client` (dsl/dart/lib/src): the async WebView
|
|
436
437
|
// bridge the generated `<name>.client.dart` drives, and the provider surface. Their
|
|
@@ -861,10 +862,10 @@ function emitDart(
|
|
|
861
862
|
/** The pure-core support import generated TYPES files carry in layered-packages mode. */
|
|
862
863
|
const DART_TYPES_IMPORT = `import 'package:nomos_types/nomos_types.dart';`;
|
|
863
864
|
/** Version constraints the scaffolded pubspecs pin — move together with the Dart packages' releases. */
|
|
864
|
-
// FLOOR = the release carrying every symbol generated code may reference (0.1.
|
|
865
|
-
const NOMOS_TYPES_DEP = "^0.1.
|
|
866
|
-
// FLOOR = the release carrying the bridge methods generated clients call (0.
|
|
867
|
-
const NOMOS_CLIENT_DEP = "^0.
|
|
865
|
+
// FLOOR = the release carrying every symbol generated code may reference (0.1.3: NomosRef).
|
|
866
|
+
const NOMOS_TYPES_DEP = "^0.1.3";
|
|
867
|
+
// FLOOR = the release carrying the bridge methods generated clients call (0.45.0: offerOutcome/NomosCreated.ref).
|
|
868
|
+
const NOMOS_CLIENT_DEP = "^0.45.0";
|
|
868
869
|
|
|
869
870
|
const isPubPackageName = (s: string): boolean => /^[a-z][a-z0-9_]*$/.test(s);
|
|
870
871
|
|
package/src/directive.ts
CHANGED
|
@@ -830,8 +830,13 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
|
830
830
|
},
|
|
831
831
|
activity(input: ActivityInput): RequirableDirective<P> {
|
|
832
832
|
// Normalize + validate against this directive's payload (unknown template var / missing payload field
|
|
833
|
-
// refuse here).
|
|
834
|
-
|
|
833
|
+
// refuse here). "@created" refs are checked against the aggregates this directive CREATES (the primary
|
|
834
|
+
// .creates target + any .alsoCreates). Non-law, additive — stored like `description`.
|
|
835
|
+
const createdIds = [
|
|
836
|
+
...(base.marker === "creates" ? [base.aggregateId] : []),
|
|
837
|
+
...(base.secondaryOutputs ?? []).filter((o) => o.marker === "creates").map((o) => o.aggregateId),
|
|
838
|
+
];
|
|
839
|
+
return makeRequirable({ ...base, activityDescriptor: normalizeActivity(base.id, input, base.payloadSchema, createdIds) });
|
|
835
840
|
},
|
|
836
841
|
readsCertified(reads: Record<string, CertifiedReadDecl>): RequirableDirective<P> {
|
|
837
842
|
// Accumulate onto any prior declared CertifiedReads (a repeated local name's later decl
|