@githolon/dsl 0.65.1 → 0.67.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 +8 -0
- package/dart/lib/src/webview_bridge.dart +25 -0
- 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 +160 -0
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/src/activity.ts +179 -0
- package/src/aggregate.ts +21 -3
- package/src/build_package.ts +20 -0
- package/src/codegen_dart.ts +235 -1
- package/src/codegen_dart_client.ts +65 -0
- package/src/codegen_ts.ts +156 -2
- package/src/compile_package_main.ts +118 -11
- package/src/contract.ts +110 -0
- package/src/directive.ts +23 -0
- package/src/fields.ts +9 -2
- package/src/index.ts +2 -0
- package/src/usd.ts +61 -0
- package/src/usd_layers.ts +40 -0
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 0.44.0
|
|
2
|
+
|
|
3
|
+
- Activity-feed runtime reads on `NomosBridge`/`NomosSession`: `activityLog({base, limit})` — the committed
|
|
4
|
+
intent log as raw rows `{intentId, domain, directiveId, actorSubject, occurredAt, payload}` (decoded
|
|
5
|
+
payload; the verified author; the physical HLC) — and `watchActivity({base})`, a stream re-deriving the
|
|
6
|
+
feed on every sync tick. Generic (raw Maps); the generated `renderActivity` (from directives' declared
|
|
7
|
+
`.activity()` metadata) types them into `ActivityEvent`s. Requires `nomos_types >= 0.1.2`.
|
|
8
|
+
|
|
1
9
|
## 0.43.0
|
|
2
10
|
|
|
3
11
|
- `NomosBridge.foreignReader({cloud})` — a `NomosForeignReader` backed by the device's multi-workspace bridge
|
|
@@ -1168,6 +1168,26 @@ class NomosBridge {
|
|
|
1168
1168
|
.toList();
|
|
1169
1169
|
}
|
|
1170
1170
|
|
|
1171
|
+
/// RUNTIME activity feed — per-committed-intent rows (newest-first) from the local ledger, each carrying
|
|
1172
|
+
/// `{intentId, domain, directiveId, actorSubject, occurredAt, payload}` (payload = the DECODED directive
|
|
1173
|
+
/// payload). `base` = a commit-oid cursor (only intents newer than it); omitted = from the tip bounded by
|
|
1174
|
+
/// `limit` (default 200). Generic raw rows — the typed shaping is the codegen `renderActivity`.
|
|
1175
|
+
Future<List<Map<String, dynamic>>> activityLog(
|
|
1176
|
+
{String? base, int? limit}) async {
|
|
1177
|
+
final r = _asMap(await _call('activityLog', {
|
|
1178
|
+
if (base != null) 'base': base,
|
|
1179
|
+
if (limit != null) 'limit': limit,
|
|
1180
|
+
}));
|
|
1181
|
+
return ((r['rows'] as List?) ?? const [])
|
|
1182
|
+
.map((e) => (e as Map).cast<String, dynamic>())
|
|
1183
|
+
.toList();
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/// Watch the RUNTIME activity feed: a stream that re-emits the newest-first activity rows on every local
|
|
1187
|
+
/// change (doorbell/sync tick). `base` = a commit-oid cursor. Cancelling drops the watch on the runner.
|
|
1188
|
+
Stream<List<dynamic>> watchActivity({String? base, String? session}) =>
|
|
1189
|
+
_watchStream(kind: 'activity', base: base, session: session);
|
|
1190
|
+
|
|
1171
1191
|
/// Mint an ed25519 DEVICE KEY in-kernel → `{secret, public}` (base64). The SECRET stays on this device
|
|
1172
1192
|
/// (persist it in secure storage); pass it to [connect] as `authorSecret` (or per-offer `signWith`) so the
|
|
1173
1193
|
/// holon SIGNS each intent. The PUBLIC half is what gets enrolled so the gate re-verifies the signature on
|
|
@@ -1594,6 +1614,7 @@ class NomosBridge {
|
|
|
1594
1614
|
String? id,
|
|
1595
1615
|
String? spatialId,
|
|
1596
1616
|
NomosBbox? bbox,
|
|
1617
|
+
String? base,
|
|
1597
1618
|
String? session}) {
|
|
1598
1619
|
final subId = 'w${++_seq}';
|
|
1599
1620
|
late final StreamController<List<dynamic>> ctrl;
|
|
@@ -1606,6 +1627,7 @@ class NomosBridge {
|
|
|
1606
1627
|
if (id != null) 'id': id,
|
|
1607
1628
|
if (spatialId != null) 'spatialId': spatialId,
|
|
1608
1629
|
if (bbox != null) 'bbox': bbox.toJson(),
|
|
1630
|
+
if (base != null) 'base': base,
|
|
1609
1631
|
if (session != null) 'session': session,
|
|
1610
1632
|
}),
|
|
1611
1633
|
onCancel: () async {
|
|
@@ -1709,6 +1731,9 @@ class NomosSession extends NomosBridge {
|
|
|
1709
1731
|
[String? session]) =>
|
|
1710
1732
|
parent.watchSpatial(spatialId, bbox, session ?? workspace);
|
|
1711
1733
|
@override
|
|
1734
|
+
Stream<List<dynamic>> watchActivity({String? base, String? session}) =>
|
|
1735
|
+
parent.watchActivity(base: base, session: session ?? workspace);
|
|
1736
|
+
@override
|
|
1712
1737
|
Stream<Map<String, Object?>> watchCompatibility(
|
|
1713
1738
|
Map<String, Object?> requirement,
|
|
1714
1739
|
[String? session]) =>
|
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.44.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.2
|
|
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.2
|
|
2
|
+
|
|
3
|
+
- Activity-feed runtime types: `ActivityEvent` (the shared activity row — kind/actor/occurredAt, rendered
|
|
4
|
+
title+message, stable `titleKey`/`messageKey` + params for localization, typed `refs`, category/icon/
|
|
5
|
+
severity, domain/directiveId provenance), `ActivityRef`, `ActivitySeverity`, `renderActivityTemplate`.
|
|
6
|
+
Consumed by the generated `renderActivity`/`describe`/`describeHistory`. Purely additive.
|
|
7
|
+
|
|
1
8
|
## 0.1.1
|
|
2
9
|
|
|
3
10
|
- Add `t.foreignRef` runtime types: `ForeignRef<T>` (a typed cross-workspace locator), `NomosForeignReader` /
|
|
@@ -31,3 +31,4 @@ export 'src/errors.dart';
|
|
|
31
31
|
// `NomosForeignReader`/`NomosForeignWorkspaceReader` read surfaces + the named resolve outcomes. Pure data +
|
|
32
32
|
// abstract interfaces (no bridge) — so both the layered TYPES package and nomos_client resolve them.
|
|
33
33
|
export 'src/foreign_ref.dart';
|
|
34
|
+
export 'src/activity.dart';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// NOMOS — the ACTIVITY-FEED runtime types (directive `.activity(...)` descriptors). The shared, tenant-
|
|
2
|
+
// agnostic shape a client renders from the intent log: a directive DECLARES its activity metadata (kind,
|
|
3
|
+
// message template, refs, params) and the generated `renderActivity` turns each committed intent into an
|
|
4
|
+
// [ActivityEvent] — no string-enricher guessing meaning beside the truth. The kernel/ledger stays the audit
|
|
5
|
+
// truth; this is a pure client-side human projection.
|
|
6
|
+
//
|
|
7
|
+
// These live in the pure `nomos_types` core (no bridge/lifecycle) so BOTH the layered TYPES package (where
|
|
8
|
+
// the generated pure `renderActivity` lives) AND the runtime `nomos_client` package resolve them.
|
|
9
|
+
library;
|
|
10
|
+
|
|
11
|
+
/// A declared navigation target on an activity row: the referenced aggregate/contract TYPE + the resolved id
|
|
12
|
+
/// (read from a payload field the directive named). Replaces the old enricher's blind id-probing — the app
|
|
13
|
+
/// knows exactly what `id` points at and can deep-link/resolve it.
|
|
14
|
+
class ActivityRef {
|
|
15
|
+
final String type;
|
|
16
|
+
final String id;
|
|
17
|
+
const ActivityRef({required this.type, required this.id});
|
|
18
|
+
|
|
19
|
+
factory ActivityRef.fromJson(Map<String, dynamic> j) =>
|
|
20
|
+
ActivityRef(type: (j['type'] ?? '') as String, id: (j['id'] ?? '') as String);
|
|
21
|
+
Map<String, dynamic> toJson() => {'type': type, 'id': id};
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
bool operator ==(Object other) => other is ActivityRef && other.type == type && other.id == id;
|
|
25
|
+
@override
|
|
26
|
+
int get hashCode => Object.hash(type, id);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Row prominence — a declared default the app may style or threshold on.
|
|
30
|
+
enum ActivitySeverity {
|
|
31
|
+
info,
|
|
32
|
+
success,
|
|
33
|
+
warning,
|
|
34
|
+
critical;
|
|
35
|
+
|
|
36
|
+
static ActivitySeverity fromWire(String? s) {
|
|
37
|
+
switch (s) {
|
|
38
|
+
case 'success':
|
|
39
|
+
return ActivitySeverity.success;
|
|
40
|
+
case 'warning':
|
|
41
|
+
return ActivitySeverity.warning;
|
|
42
|
+
case 'critical':
|
|
43
|
+
return ActivitySeverity.critical;
|
|
44
|
+
default:
|
|
45
|
+
return ActivitySeverity.info;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
String get wire => name;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// A rendered activity row — the tenant-agnostic "tiny shared activity contract". Produced by the generated
|
|
53
|
+
/// `renderActivity(intentMeta, payload)` from a directive's declared `.activity(...)` descriptor. The UI
|
|
54
|
+
/// consumes this directly (title/message already interpolated; `refs` for navigation; `titleKey`/`messageKey`
|
|
55
|
+
/// + `params` for later localization).
|
|
56
|
+
class ActivityEvent {
|
|
57
|
+
/// The intent id — a stable per-event identity (dedupe/order key).
|
|
58
|
+
final String eventId;
|
|
59
|
+
|
|
60
|
+
/// The declared STABLE event kind (e.g. `"site.created"`).
|
|
61
|
+
final String kind;
|
|
62
|
+
|
|
63
|
+
/// The intent's author (principal) — the "who".
|
|
64
|
+
final String actorSubject;
|
|
65
|
+
|
|
66
|
+
/// When it happened (the intent's physical HLC timestamp, ISO-8601 when available).
|
|
67
|
+
final String occurredAt;
|
|
68
|
+
|
|
69
|
+
/// The rendered title (template interpolated); [titleKey] + [params] localize it later.
|
|
70
|
+
final String title;
|
|
71
|
+
|
|
72
|
+
/// The rendered message (template interpolated); [messageKey] + [params] localize it later.
|
|
73
|
+
final String message;
|
|
74
|
+
|
|
75
|
+
/// Stable i18n key for the title (`activity.<kind>.title`).
|
|
76
|
+
final String titleKey;
|
|
77
|
+
|
|
78
|
+
/// Stable i18n key for the message (`activity.<kind>.message`).
|
|
79
|
+
final String messageKey;
|
|
80
|
+
|
|
81
|
+
/// The interpolation params (name → display value, read from the payload) — for localized re-rendering.
|
|
82
|
+
final Map<String, String> params;
|
|
83
|
+
|
|
84
|
+
/// Declared navigation targets (name → typed ref).
|
|
85
|
+
final Map<String, ActivityRef> refs;
|
|
86
|
+
|
|
87
|
+
/// Optional grouping category (e.g. `"estate"`).
|
|
88
|
+
final String? category;
|
|
89
|
+
|
|
90
|
+
/// Optional default icon token (app-interpreted).
|
|
91
|
+
final String? icon;
|
|
92
|
+
|
|
93
|
+
/// Row prominence (default [ActivitySeverity.info]).
|
|
94
|
+
final ActivitySeverity severity;
|
|
95
|
+
|
|
96
|
+
/// Provenance — the domain + directive that produced this event.
|
|
97
|
+
final String domain;
|
|
98
|
+
final String directiveId;
|
|
99
|
+
|
|
100
|
+
const ActivityEvent({
|
|
101
|
+
required this.eventId,
|
|
102
|
+
required this.kind,
|
|
103
|
+
required this.actorSubject,
|
|
104
|
+
required this.occurredAt,
|
|
105
|
+
required this.title,
|
|
106
|
+
required this.message,
|
|
107
|
+
required this.titleKey,
|
|
108
|
+
required this.messageKey,
|
|
109
|
+
required this.params,
|
|
110
|
+
required this.refs,
|
|
111
|
+
required this.severity,
|
|
112
|
+
required this.domain,
|
|
113
|
+
required this.directiveId,
|
|
114
|
+
this.category,
|
|
115
|
+
this.icon,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
factory ActivityEvent.fromJson(Map<String, dynamic> j) => ActivityEvent(
|
|
119
|
+
eventId: (j['eventId'] ?? '') as String,
|
|
120
|
+
kind: (j['kind'] ?? '') as String,
|
|
121
|
+
actorSubject: (j['actorSubject'] ?? '') as String,
|
|
122
|
+
occurredAt: (j['occurredAt'] ?? '') as String,
|
|
123
|
+
title: (j['title'] ?? '') as String,
|
|
124
|
+
message: (j['message'] ?? '') as String,
|
|
125
|
+
titleKey: (j['titleKey'] ?? '') as String,
|
|
126
|
+
messageKey: (j['messageKey'] ?? '') as String,
|
|
127
|
+
params: ((j['params'] as Map?)?.cast<String, dynamic>() ?? const {})
|
|
128
|
+
.map((k, v) => MapEntry(k, '$v')),
|
|
129
|
+
refs: ((j['refs'] as Map?)?.cast<String, dynamic>() ?? const {})
|
|
130
|
+
.map((k, v) => MapEntry(k, ActivityRef.fromJson((v as Map).cast<String, dynamic>()))),
|
|
131
|
+
category: j['category'] as String?,
|
|
132
|
+
icon: j['icon'] as String?,
|
|
133
|
+
severity: ActivitySeverity.fromWire(j['severity'] as String?),
|
|
134
|
+
domain: (j['domain'] ?? '') as String,
|
|
135
|
+
directiveId: (j['directiveId'] ?? '') as String,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
Map<String, dynamic> toJson() => {
|
|
139
|
+
'eventId': eventId,
|
|
140
|
+
'kind': kind,
|
|
141
|
+
'actorSubject': actorSubject,
|
|
142
|
+
'occurredAt': occurredAt,
|
|
143
|
+
'title': title,
|
|
144
|
+
'message': message,
|
|
145
|
+
'titleKey': titleKey,
|
|
146
|
+
'messageKey': messageKey,
|
|
147
|
+
'params': params,
|
|
148
|
+
'refs': refs.map((k, v) => MapEntry(k, v.toJson())),
|
|
149
|
+
if (category != null) 'category': category,
|
|
150
|
+
if (icon != null) 'icon': icon,
|
|
151
|
+
'severity': severity.wire,
|
|
152
|
+
'domain': domain,
|
|
153
|
+
'directiveId': directiveId,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Interpolate `{name}` placeholders in [template] from [vars] — the render helper the generated
|
|
158
|
+
/// `renderActivity` uses. A missing var renders empty (the compiler already validated every placeholder).
|
|
159
|
+
String renderActivityTemplate(String template, Map<String, String> vars) =>
|
|
160
|
+
template.replaceAllMapped(RegExp(r'\{([a-zA-Z0-9_]+)\}'), (m) => vars[m.group(1)] ?? '');
|
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.2
|
|
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.67.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
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// NOMOS — DIRECTIVE ACTIVITY DESCRIPTORS (architecture/activity_descriptors.md). A directive OPT-IN declares
|
|
2
|
+
// its own user-facing activity metadata, so a client can fold typed activity rows straight from the intent
|
|
3
|
+
// log — no string-enricher guessing meaning beside the truth. The kernel/ledger stays the audit truth; this
|
|
4
|
+
// is a pure CLIENT-SIDE human projection, declared where the event semantics live.
|
|
5
|
+
//
|
|
6
|
+
// directive("createSite").creates(EstateSite).payload(SitePayload)
|
|
7
|
+
// .activity({
|
|
8
|
+
// kind: "site.created", // STABLE event id (seeds the i18n keys)
|
|
9
|
+
// title: "Created site",
|
|
10
|
+
// message: "{actor} created {siteName}", // {actor} + declared params
|
|
11
|
+
// category: "estate", icon: "add_location", severity: "info",
|
|
12
|
+
// refs: { site: { type: EstateSite, from: "siteId" } }, // navigation target (type + payload path)
|
|
13
|
+
// params: { siteName: "name" }, // display value ← payload field path
|
|
14
|
+
// })
|
|
15
|
+
//
|
|
16
|
+
// NON-LAW, additive metadata (same class as `.doc()`): a directive that never calls `.activity()` is
|
|
17
|
+
// byte-identical in the canonical manifest.
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
20
|
+
import { isContractHandle, type ContractHandle } from "./contract.js";
|
|
21
|
+
|
|
22
|
+
/** How prominent an activity row is — a declared default the app may style/threshold on. */
|
|
23
|
+
export type ActivitySeverity = "info" | "success" | "warning" | "critical";
|
|
24
|
+
|
|
25
|
+
/** A declared navigation target: the referenced aggregate/contract TYPE + the payload field path holding its
|
|
26
|
+
* id. Replaces the enricher's blind id-probing — the reference is declared, not guessed. */
|
|
27
|
+
export interface ActivityRefInput {
|
|
28
|
+
/** The target type — an aggregate handle or a `contract(...)` (reuse contracts for the nav target). */
|
|
29
|
+
readonly type: AggregateHandle | ContractHandle;
|
|
30
|
+
/** The payload field path holding the target's id (dotted for nested, e.g. `"location.siteId"`). */
|
|
31
|
+
readonly from: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The `.activity({...})` input (declarative — field PATHS, never functions, so it generates to Dart too). */
|
|
35
|
+
export interface ActivityInput {
|
|
36
|
+
/** STABLE event id (e.g. `"site.created"`) — the wire kind + the seed for the i18n keys. */
|
|
37
|
+
readonly kind: string;
|
|
38
|
+
/** Default (English) title; falls back to `kind` if omitted. */
|
|
39
|
+
readonly title?: string;
|
|
40
|
+
/** Message template — `{actor}` + any declared `params` name in braces (e.g. `"{actor} created {siteName}"`). */
|
|
41
|
+
readonly message: string;
|
|
42
|
+
/** Optional grouping category (e.g. `"estate"`). */
|
|
43
|
+
readonly category?: string;
|
|
44
|
+
/** Optional default icon token (app-interpreted). */
|
|
45
|
+
readonly icon?: string;
|
|
46
|
+
/** Row prominence (default `"info"`). */
|
|
47
|
+
readonly severity?: ActivitySeverity;
|
|
48
|
+
/** Navigation targets: name → { target type, payload path to its id }. */
|
|
49
|
+
readonly refs?: Record<string, ActivityRefInput>;
|
|
50
|
+
/** Display values: template-param name → payload field path (dotted ok). */
|
|
51
|
+
readonly params?: Record<string, string>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The NORMALIZED, serializable descriptor stored on the directive + emitted to codegen/IR. */
|
|
55
|
+
export interface ActivityDescriptor {
|
|
56
|
+
readonly kind: string;
|
|
57
|
+
readonly title: string;
|
|
58
|
+
readonly message: string;
|
|
59
|
+
readonly category?: string;
|
|
60
|
+
readonly icon?: string;
|
|
61
|
+
readonly severity: ActivitySeverity;
|
|
62
|
+
/** Refs sorted by name; `type` is the target's wire id (a string). */
|
|
63
|
+
readonly refs: readonly { readonly name: string; readonly type: string; readonly from: string }[];
|
|
64
|
+
/** Params sorted by name. */
|
|
65
|
+
readonly params: readonly { readonly name: string; readonly from: string }[];
|
|
66
|
+
/** Stable i18n keys derived from `kind` (`activity.<kind>.title` / `.message`). */
|
|
67
|
+
readonly titleKey: string;
|
|
68
|
+
readonly messageKey: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const SEVERITIES: ReadonlySet<string> = new Set(["info", "success", "warning", "critical"]);
|
|
72
|
+
|
|
73
|
+
/** The i18n key for a `kind` + slot — stable, derived (never hand-authored). */
|
|
74
|
+
export function activityKey(kind: string, slot: "title" | "message"): string {
|
|
75
|
+
return `activity.${kind}.${slot}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Interpolate `{name}` placeholders in a template from `vars` (a missing var renders as the empty string —
|
|
79
|
+
* the compile-time validator already guarantees every placeholder is declared, so this is defensive only).
|
|
80
|
+
* The ONE render helper shared by the TS + Dart renderers (each reimplements the same trivial substitution). */
|
|
81
|
+
export function renderActivityTemplate(template: string, vars: Record<string, string>): string {
|
|
82
|
+
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_m, name: string) => vars[name] ?? "");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The `{placeholder}` names referenced by a template. */
|
|
86
|
+
function templateVars(template: string): string[] {
|
|
87
|
+
const out: string[] = [];
|
|
88
|
+
for (const m of template.matchAll(/\{([a-zA-Z0-9_]+)\}/g)) out.push(m[1]!);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Top-level payload field names (best-effort — a plain `z.object`); `undefined` when the schema isn't a
|
|
93
|
+
* destructurable object (validation of `from` roots is then skipped, staying permissive). */
|
|
94
|
+
function payloadFieldNames(schema: unknown): Set<string> | undefined {
|
|
95
|
+
const def = (schema as { _def?: { shape?: unknown }; def?: { shape?: unknown } })?._def
|
|
96
|
+
?? (schema as { def?: { shape?: unknown } })?.def;
|
|
97
|
+
const shape = (def as { shape?: unknown })?.shape;
|
|
98
|
+
const resolved = typeof shape === "function" ? (shape as () => Record<string, unknown>)() : shape;
|
|
99
|
+
if (resolved === undefined || resolved === null || typeof resolved !== "object") return undefined;
|
|
100
|
+
return new Set(Object.keys(resolved as Record<string, unknown>));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate + normalize an `.activity({...})` input at author time (throws a typed error naming the problem).
|
|
105
|
+
* Checks: `kind`/`message` non-empty; `severity` valid; every message/title `{placeholder}` is `actor` or a
|
|
106
|
+
* declared param; every ref/param `from` ROOT names a real payload field (when the payload shape is
|
|
107
|
+
* introspectable). Returns the serializable {@link ActivityDescriptor}. Pure.
|
|
108
|
+
*/
|
|
109
|
+
export function normalizeActivity(directiveId: string, input: ActivityInput, payloadSchema?: unknown): ActivityDescriptor {
|
|
110
|
+
const where = `directive '${directiveId}' .activity(...)`;
|
|
111
|
+
if (typeof input.kind !== "string" || input.kind.trim().length === 0) {
|
|
112
|
+
throw new Error(`${where}: 'kind' must be a non-empty stable string (e.g. "site.created").`);
|
|
113
|
+
}
|
|
114
|
+
if (typeof input.message !== "string" || input.message.trim().length === 0) {
|
|
115
|
+
throw new Error(`${where}: 'message' must be a non-empty template string.`);
|
|
116
|
+
}
|
|
117
|
+
const severity = input.severity ?? "info";
|
|
118
|
+
if (!SEVERITIES.has(severity)) {
|
|
119
|
+
throw new Error(`${where}: 'severity' must be one of info|success|warning|critical (got '${severity}').`);
|
|
120
|
+
}
|
|
121
|
+
const params = Object.entries(input.params ?? {}).map(([name, from]) => {
|
|
122
|
+
if (typeof from !== "string" || from.length === 0) {
|
|
123
|
+
throw new Error(`${where}: param '${name}' must map to a payload field path string.`);
|
|
124
|
+
}
|
|
125
|
+
return { name, from };
|
|
126
|
+
});
|
|
127
|
+
const paramNames = new Set(params.map((p) => p.name));
|
|
128
|
+
// Every {placeholder} must be `actor` or a declared param — an undeclared var is a typo, refuse it.
|
|
129
|
+
for (const tmpl of [input.title ?? "", input.message]) {
|
|
130
|
+
for (const v of templateVars(tmpl)) {
|
|
131
|
+
if (v !== "actor" && !paramNames.has(v)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`${where}: template references '{${v}}' but no such param is declared (known: actor${
|
|
134
|
+
paramNames.size > 0 ? ", " + [...paramNames].join(", ") : ""
|
|
135
|
+
}). Declare it in params or fix the template.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const refs = Object.entries(input.refs ?? {}).map(([name, r]) => {
|
|
141
|
+
const targetId = (r?.type as { id?: unknown })?.id;
|
|
142
|
+
const isTarget = typeof targetId === "string" && ((r.type as { __isAggregateHandle?: unknown }).__isAggregateHandle === true || isContractHandle(r.type));
|
|
143
|
+
if (!isTarget) {
|
|
144
|
+
throw new Error(`${where}: ref '${name}'.type must be an aggregate handle or a contract(...) (got ${typeof r?.type}).`);
|
|
145
|
+
}
|
|
146
|
+
if (typeof r.from !== "string" || r.from.length === 0) {
|
|
147
|
+
throw new Error(`${where}: ref '${name}'.from must be a payload field path string.`);
|
|
148
|
+
}
|
|
149
|
+
return { name, type: targetId as string, from: r.from };
|
|
150
|
+
});
|
|
151
|
+
// Best-effort payload-field existence check (root of a dotted path).
|
|
152
|
+
const fields = payloadFieldNames(payloadSchema);
|
|
153
|
+
if (fields !== undefined) {
|
|
154
|
+
for (const { name, from } of [...params, ...refs.map((r) => ({ name: `ref ${r.name}`, from: r.from }))]) {
|
|
155
|
+
const root = from.split(".")[0]!;
|
|
156
|
+
if (!fields.has(root)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`${where}: ${name} reads payload field '${root}' which the payload does not declare ` +
|
|
159
|
+
`(known: ${[...fields].sort().join(", ")}). Fix the path or add the field.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
kind: input.kind,
|
|
166
|
+
title: input.title ?? input.kind,
|
|
167
|
+
message: input.message,
|
|
168
|
+
...(input.category !== undefined ? { category: input.category } : {}),
|
|
169
|
+
...(input.icon !== undefined ? { icon: input.icon } : {}),
|
|
170
|
+
severity,
|
|
171
|
+
refs: refs.sort((a, b) => a.name.localeCompare(b.name)),
|
|
172
|
+
params: params.sort((a, b) => a.name.localeCompare(b.name)),
|
|
173
|
+
titleKey: activityKey(input.kind, "title"),
|
|
174
|
+
messageKey: activityKey(input.kind, "message"),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Re-exported so directive.ts can accept the zod payload type in the `.activity` overload without a cycle. */
|
|
179
|
+
export type { z };
|
package/src/aggregate.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* string: a typo'd handle is a compile error.
|
|
15
15
|
*/
|
|
16
16
|
import type { Field } from "./fields.js";
|
|
17
|
+
import { satisfiesContract, type ContractHandle } from "./contract.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* A pure, replay-stable predicate over one aggregate's PROJECTED snapshot —
|
|
@@ -155,6 +156,10 @@ export interface AggregateHandle<
|
|
|
155
156
|
readonly hasMany: HasManyFields<F>;
|
|
156
157
|
/** Brand so a plain string is never accepted where a handle is required. */
|
|
157
158
|
readonly __isAggregateHandle: true;
|
|
159
|
+
/** DOMAIN CONTRACTS this aggregate `.implements(...)` — the stable public shapes it satisfies (compile-
|
|
160
|
+
* checked). Codegen marks the aggregate as implementing them so cross-domain refs can target the CONTRACT
|
|
161
|
+
* instead of this concrete type. Additive metadata (not hashed law). OMITTED when it implements none. */
|
|
162
|
+
readonly implementsContracts?: readonly string[];
|
|
158
163
|
/**
|
|
159
164
|
* FRAMEWORK-SHARED brand (Dart-codegen hygiene). Set (by `framework/framework_aggregates.ts`)
|
|
160
165
|
* ONLY on the framework-owned aggregate singletons declared in `dsl/src/framework/` — most
|
|
@@ -235,6 +240,9 @@ export interface AudienceMethods<Id extends string, F extends Record<string, Fie
|
|
|
235
240
|
/** The owner (+ per-record grantees) read; owner = the verified author, stamped into `ownerField`
|
|
236
241
|
* (default `__owner`); operator-blind. NOTE: full owned read-scoping + E2E land in the next slice. */
|
|
237
242
|
owned(ownerField?: string): AggregateHandle<Id, F>;
|
|
243
|
+
/** DOMAIN CONTRACTS: assert this aggregate satisfies `contract`'s public shape (compile-checked — a
|
|
244
|
+
* mismatch throws naming the field). Chainable; records the contract so cross-domain refs can target it. */
|
|
245
|
+
implements(contract: ContractHandle): AggregateHandle<Id, F> & AudienceMethods<Id, F>;
|
|
238
246
|
}
|
|
239
247
|
|
|
240
248
|
export function aggregate<const Id extends string, F extends Record<string, Field>>(
|
|
@@ -354,16 +362,26 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
354
362
|
const build = (
|
|
355
363
|
audience: Audience | undefined,
|
|
356
364
|
visibility: ReadVisibility | undefined,
|
|
365
|
+
contracts: readonly string[] = [],
|
|
357
366
|
): AggregateHandle<Id, F> & AudienceMethods<Id, F> => ({
|
|
358
367
|
...base,
|
|
359
368
|
...(audience !== undefined ? { audience } : {}),
|
|
360
369
|
...(visibility !== undefined ? { visibility } : {}),
|
|
361
|
-
|
|
370
|
+
...(contracts.length > 0 ? { implementsContracts: contracts } : {}),
|
|
371
|
+
public: () => build({ kind: "public" }, undefined, contracts),
|
|
362
372
|
readableBy: (cap: string) => {
|
|
363
373
|
const v = requires(cap); // validates the capability name
|
|
364
|
-
return build({ kind: "readableBy", cap: v.requires }, v);
|
|
374
|
+
return build({ kind: "readableBy", cap: v.requires }, v, contracts);
|
|
375
|
+
},
|
|
376
|
+
owned: (ownerField: string = OWNER_FIELD) => build({ kind: "owned", ownerField }, undefined, contracts),
|
|
377
|
+
// DOMAIN CONTRACTS: assert this aggregate satisfies `contractHandle`'s public shape — compile-checked
|
|
378
|
+
// NOW (a mismatch throws a typed error naming the field). Records the contract so cross-domain refs may
|
|
379
|
+
// target it. Chainable (`.implements(A).implements(B)`); the aggregate keeps its concrete fields.
|
|
380
|
+
implements: (contractHandle: ContractHandle) => {
|
|
381
|
+
const verdict = satisfiesContract(id, stored as Record<string, Field>, contractHandle);
|
|
382
|
+
if (!verdict.ok) throw new Error(verdict.reason);
|
|
383
|
+
return build(audience, visibility, contracts.includes(contractHandle.id) ? contracts : [...contracts, contractHandle.id]);
|
|
365
384
|
},
|
|
366
|
-
owned: (ownerField: string = OWNER_FIELD) => build({ kind: "owned", ownerField }, undefined),
|
|
367
385
|
});
|
|
368
386
|
|
|
369
387
|
// Legacy `{ visibility: requires(cap) }` IS `readableBy(cap)` — carry both so existing law is unchanged.
|
package/src/build_package.ts
CHANGED
|
@@ -99,6 +99,17 @@ export function aggregatesOf(mod: Mod): AggregateHandle[] {
|
|
|
99
99
|
return out;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** Collect every exported `contract(...)` handle (`__isContract` duck-type). */
|
|
103
|
+
export function contractsOf(mod: Mod): import("./contract.js").ContractHandle[] {
|
|
104
|
+
const out: import("./contract.js").ContractHandle[] = [];
|
|
105
|
+
for (const v of Object.values(mod)) {
|
|
106
|
+
if (v && typeof v === "object" && (v as { __isContract?: boolean }).__isContract === true) {
|
|
107
|
+
out.push(v as import("./contract.js").ContractHandle);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
102
113
|
/** Collect every exported `Directive` ({id, plan, aggregateId} duck-type). */
|
|
103
114
|
export function directivesOf(mod: Mod): Directive<any>[] {
|
|
104
115
|
const out: Directive<any>[] = [];
|
|
@@ -262,6 +273,12 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
262
273
|
// the additive `nomosValueObjects` USD key. Omit-when-empty (era rule): a VO-free domain's
|
|
263
274
|
// module — and thus its USD/package bytes — is byte-identical to before VOs existed.
|
|
264
275
|
const valueObjects = collectValueObjectDescriptors(aggregates);
|
|
276
|
+
// DOMAIN CONTRACTS (domain_contracts.md): the public DTO shapes this domain declares, discovered by
|
|
277
|
+
// shape across the merged module list. Deduped by id (later-wins), sorted for byte-determinism. Omit-
|
|
278
|
+
// when-empty (era rule): a contract-free domain's module/USD/package bytes are byte-identical.
|
|
279
|
+
const contractById = new Map<string, import("./contract.js").ContractHandle>();
|
|
280
|
+
for (const c of contractsOf(merged)) contractById.set(c.id, c);
|
|
281
|
+
const contracts = [...contractById.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
265
282
|
// SPATIAL × VO PAIRING (fail at build, not at fold): a spatial index over a VO-typed field
|
|
266
283
|
// must index a GEO value object (an RFC 7946 geometry — the R*Tree's bbox source). A
|
|
267
284
|
// structural VO (e.g. a PostalAddress) has no geometry to index. Non-VO fields (a
|
|
@@ -394,6 +411,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
394
411
|
// Omit-when-empty — the `nomosValueObjects` era rule: a VO-free domain's USD/package
|
|
395
412
|
// bytes are byte-identical to before value objects existed.
|
|
396
413
|
...(valueObjects.length > 0 ? { valueObjects } : {}),
|
|
414
|
+
// Omit-when-empty — the `nomosContracts` era rule: a contract-free domain's USD/package bytes
|
|
415
|
+
// are byte-identical to before domain contracts existed (pure additive typing metadata).
|
|
416
|
+
...(contracts.length > 0 ? { contracts } : {}),
|
|
397
417
|
// THE PEER-SOVEREIGN ROOT-OF-TRUST: omit-when-false — a law that doesn't require parent-attested
|
|
398
418
|
// birth is byte-identical in the canonical manifest/USD (the nomosParentAttestedBirth era rule).
|
|
399
419
|
...(parentAttestedBirth ? { parentAttestedBirth } : {}),
|