@githolon/dsl 0.66.0 → 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/codegen_dart.ts +127 -0
- package/src/codegen_dart_client.ts +65 -0
- package/src/codegen_ts.ts +130 -0
- package/src/compile_package_main.ts +27 -3
- package/src/directive.ts +23 -0
- package/src/index.ts +1 -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/codegen_dart.ts
CHANGED
|
@@ -2002,6 +2002,133 @@ export function generateDartContracts(contracts: readonly import("./contract.js"
|
|
|
2002
2002
|
].join("\n");
|
|
2003
2003
|
}
|
|
2004
2004
|
|
|
2005
|
+
// ── ACTIVITY DESCRIPTORS (architecture/activity_descriptors.md) — shared registry codegen ──
|
|
2006
|
+
|
|
2007
|
+
/** One directive's declared activity, tagged with its provenance (domain + directive id). */
|
|
2008
|
+
export interface PackageActivityEntry {
|
|
2009
|
+
readonly domain: string;
|
|
2010
|
+
readonly directiveId: string;
|
|
2011
|
+
readonly desc: import("./activity.js").ActivityDescriptor;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/** Collect every directive's `.activity(...)` descriptor across the package's modules, tagged with its
|
|
2015
|
+
* domain + directive id, sorted by directive id (byte-deterministic switch/registry order). Empty when
|
|
2016
|
+
* no directive opts in — an activity-free package then emits NO registry (byte-identical). Shared by the
|
|
2017
|
+
* flat + layered Dart emit AND the TS client (one source of truth). */
|
|
2018
|
+
export function collectActivityDescriptors(modules: readonly DomainModule[]): PackageActivityEntry[] {
|
|
2019
|
+
const out: PackageActivityEntry[] = [];
|
|
2020
|
+
for (const mod of modules) {
|
|
2021
|
+
const domain = mod.domain ?? mod.name;
|
|
2022
|
+
for (const d of mod.directives as AnyDirective[]) {
|
|
2023
|
+
const desc = (d as { activityDescriptor?: import("./activity.js").ActivityDescriptor }).activityDescriptor;
|
|
2024
|
+
if (desc !== undefined) out.push({ domain, directiveId: d.id, desc });
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
return out.sort((a, b) => (a.directiveId < b.directiveId ? -1 : a.directiveId > b.directiveId ? 1 : 0));
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/** Dart string literal (single-quoted, backslash/quote/`$` escaped). */
|
|
2031
|
+
const dartLit = (s: string): string => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\$/g, "\\$")}'`;
|
|
2032
|
+
|
|
2033
|
+
/** Emit ONE `case '<directiveId>':` arm folding a raw row into a typed [ActivityEvent]. */
|
|
2034
|
+
function emitDartActivityArm(e: PackageActivityEntry): string[] {
|
|
2035
|
+
const { directiveId, desc } = e;
|
|
2036
|
+
const paramEntries = desc.params.map((p) => ` ${dartLit(p.name)}: _activityPayloadPath(payload, ${dartLit(p.from)}),`);
|
|
2037
|
+
const refEntries = desc.refs.map(
|
|
2038
|
+
(r) => ` ${dartLit(r.name)}: ActivityRef(type: ${dartLit(r.type)}, id: _activityPayloadPath(payload, ${dartLit(r.from)})),`,
|
|
2039
|
+
);
|
|
2040
|
+
return [
|
|
2041
|
+
` case ${dartLit(directiveId)}:`,
|
|
2042
|
+
` {`,
|
|
2043
|
+
` final params = <String, String>{`,
|
|
2044
|
+
...paramEntries,
|
|
2045
|
+
` };`,
|
|
2046
|
+
` final vars = <String, String>{'actor': actorSubject, ...params};`,
|
|
2047
|
+
` return ActivityEvent(`,
|
|
2048
|
+
` eventId: (rawRow['intentId'] ?? '') as String,`,
|
|
2049
|
+
` kind: ${dartLit(desc.kind)},`,
|
|
2050
|
+
` actorSubject: actorSubject,`,
|
|
2051
|
+
` occurredAt: (rawRow['occurredAt'] ?? '') as String,`,
|
|
2052
|
+
` title: renderActivityTemplate(${dartLit(desc.title)}, vars),`,
|
|
2053
|
+
` message: renderActivityTemplate(${dartLit(desc.message)}, vars),`,
|
|
2054
|
+
` titleKey: ${dartLit(desc.titleKey)},`,
|
|
2055
|
+
` messageKey: ${dartLit(desc.messageKey)},`,
|
|
2056
|
+
` params: params,`,
|
|
2057
|
+
` refs: <String, ActivityRef>{`,
|
|
2058
|
+
...refEntries,
|
|
2059
|
+
` },`,
|
|
2060
|
+
...(desc.category !== undefined ? [` category: ${dartLit(desc.category)},`] : []),
|
|
2061
|
+
...(desc.icon !== undefined ? [` icon: ${dartLit(desc.icon)},`] : []),
|
|
2062
|
+
` severity: ActivitySeverity.${desc.severity},`,
|
|
2063
|
+
` domain: (rawRow['domain'] ?? '') as String,`,
|
|
2064
|
+
` directiveId: ${dartLit(directiveId)},`,
|
|
2065
|
+
` );`,
|
|
2066
|
+
` }`,
|
|
2067
|
+
];
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
/**
|
|
2071
|
+
* The full source of the shared `activity_registry.dart` file — the PURE per-directive activity registry
|
|
2072
|
+
* + `renderActivity(rawRow) => ActivityEvent?`, emitted ONCE per package into the TYPES package (both the
|
|
2073
|
+
* flat + layered emit). References only nomos_types (`ActivityEvent`/`ActivityRef`/`ActivitySeverity`/
|
|
2074
|
+
* `renderActivityTemplate`), so the file is PURE (frontend-safe, no bridge). A raw row is
|
|
2075
|
+
* `{ intentId, domain, directiveId, actorSubject, occurredAt, payload }` (the runtime's per-committed-intent
|
|
2076
|
+
* shape); `renderActivity` returns null for a directive with no declared `.activity(...)`.
|
|
2077
|
+
*/
|
|
2078
|
+
export function generateDartActivityRegistry(entries: readonly PackageActivityEntry[]): string {
|
|
2079
|
+
const sorted = [...entries].sort((a, b) => (a.directiveId < b.directiveId ? -1 : a.directiveId > b.directiveId ? 1 : 0));
|
|
2080
|
+
return [
|
|
2081
|
+
`// GENERATED by nomos-compile — the package's shared ACTIVITY REGISTRY (architecture/activity_descriptors.md). DO NOT EDIT.`,
|
|
2082
|
+
`// A directive OPT-IN declares its user-facing activity metadata; \`renderActivity\` folds each committed-intent`,
|
|
2083
|
+
`// raw row into a typed ActivityEvent. Pure (nomos_types only) — the kernel/ledger stays the audit truth.`,
|
|
2084
|
+
`//`,
|
|
2085
|
+
`// ignore_for_file: unused_import`,
|
|
2086
|
+
`library;`,
|
|
2087
|
+
``,
|
|
2088
|
+
`import 'package:nomos_client/nomos_client.dart';`,
|
|
2089
|
+
``,
|
|
2090
|
+
`/// A NARRATIVE over the activity log (for an in-app LLM): the filtered [events] + a plain-English`,
|
|
2091
|
+
`/// [transcript] (ONE LINE PER EVENT, oldest-first, \`"<occurredAt> — <title>: <message>"\`) grounded in`,
|
|
2092
|
+
`/// the VERIFIED log, never raw payloads. Produced by the client's \`describe\` / \`describeHistory\`.`,
|
|
2093
|
+
`class ActivityNarrative {`,
|
|
2094
|
+
` final List<ActivityEvent> events;`,
|
|
2095
|
+
` final String transcript;`,
|
|
2096
|
+
` const ActivityNarrative({required this.events, required this.transcript});`,
|
|
2097
|
+
`}`,
|
|
2098
|
+
``,
|
|
2099
|
+
`/// Wrap events into an [ActivityNarrative], building the LLM-ready transcript: one line per event,`,
|
|
2100
|
+
`/// OLDEST-FIRST (the log yields newest-first).`,
|
|
2101
|
+
`ActivityNarrative activityNarrative(List<ActivityEvent> events) => ActivityNarrative(`,
|
|
2102
|
+
` events: events,`,
|
|
2103
|
+
` transcript: events.reversed.map((e) => '\${e.occurredAt} — \${e.title}: \${e.message}').join('\\n'),`,
|
|
2104
|
+
` );`,
|
|
2105
|
+
``,
|
|
2106
|
+
`/// Read a dotted path off a decoded intent payload, coerced to String ('' when absent/null).`,
|
|
2107
|
+
`String _activityPayloadPath(Map<String, dynamic> payload, String path) {`,
|
|
2108
|
+
` dynamic cur = payload;`,
|
|
2109
|
+
` for (final seg in path.split('.')) {`,
|
|
2110
|
+
` if (cur is! Map) return '';`,
|
|
2111
|
+
` cur = cur[seg];`,
|
|
2112
|
+
` }`,
|
|
2113
|
+
` return cur == null ? '' : '\$cur';`,
|
|
2114
|
+
`}`,
|
|
2115
|
+
``,
|
|
2116
|
+
`/// 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 }.`,
|
|
2118
|
+
`ActivityEvent? renderActivity(Map<String, dynamic> rawRow) {`,
|
|
2119
|
+
` final directiveId = (rawRow['directiveId'] ?? '') as String;`,
|
|
2120
|
+
` final actorSubject = (rawRow['actorSubject'] ?? '') as String;`,
|
|
2121
|
+
` final payload = ((rawRow['payload'] as Map?)?.cast<String, dynamic>()) ?? const <String, dynamic>{};`,
|
|
2122
|
+
` switch (directiveId) {`,
|
|
2123
|
+
...sorted.flatMap(emitDartActivityArm),
|
|
2124
|
+
` default:`,
|
|
2125
|
+
` return null;`,
|
|
2126
|
+
` }`,
|
|
2127
|
+
`}`,
|
|
2128
|
+
``,
|
|
2129
|
+
].join("\n");
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2005
2132
|
/** True when ANY aggregate field in the module is `t.evidence()` — gates emitting the
|
|
2006
2133
|
* shared [emitEvidenceRefClass] once. */
|
|
2007
2134
|
function moduleUsesEvidence(aggregates: readonly AggregateHandle[]): boolean {
|
|
@@ -121,6 +121,19 @@ export interface DartBridgeClientOptions {
|
|
|
121
121
|
* client re-exports that barrel so one import of the client still gives the whole typed surface.
|
|
122
122
|
*/
|
|
123
123
|
readonly typesImportUri?: string;
|
|
124
|
+
/**
|
|
125
|
+
* ACTIVITY FEED (architecture/activity_descriptors.md): true iff the PACKAGE declares ≥1 directive
|
|
126
|
+
* `.activity(...)`. When set the client emits `watchActivity()` / `activityLog()` (folding the bridge's
|
|
127
|
+
* raw committed-intent rows through the shared pure `renderActivity`). Off ⇒ byte-identical to before.
|
|
128
|
+
*/
|
|
129
|
+
readonly emitActivity?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* FLAT (vendored) mode only: the relative import of the shared `activity_registry.dart` (where the pure
|
|
132
|
+
* `renderActivity` lives), needed because a flat client imports only its sibling `<name>.dart`. In layered
|
|
133
|
+
* mode the registry resolves through {@link typesImportUri} (the types barrel re-exports it), so this is
|
|
134
|
+
* omitted. Only meaningful when {@link emitActivity} is set.
|
|
135
|
+
*/
|
|
136
|
+
readonly activityImportUri?: string;
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
/** Generate the `<name>.client.dart` async bridge client for one domain module. */
|
|
@@ -345,6 +358,55 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
345
358
|
);
|
|
346
359
|
}
|
|
347
360
|
|
|
361
|
+
// ── activity feed → typed reactive + one-shot (declared `.activity(...)`) ──
|
|
362
|
+
if (opts.emitActivity === true) {
|
|
363
|
+
methods.push(
|
|
364
|
+
` /// THE ACTIVITY FEED (architecture/activity_descriptors.md): fold the committed intent log into typed`,
|
|
365
|
+
` /// [ActivityEvent]s (declared \`.activity(...)\`). Re-emits on every new committed intent; a directive`,
|
|
366
|
+
` /// with no declared activity is dropped. Cancel the subscription to stop.`,
|
|
367
|
+
` Stream<List<ActivityEvent>> watchActivity() =>`,
|
|
368
|
+
` bridge.watchActivity().map((rows) => rows`,
|
|
369
|
+
` .map((r) => renderActivity((r as Map).cast<String, dynamic>()))`,
|
|
370
|
+
` .whereType<ActivityEvent>()`,
|
|
371
|
+
` .toList());`,
|
|
372
|
+
``,
|
|
373
|
+
` /// One-shot: the current committed activity log, folded to typed [ActivityEvent]s (declared \`.activity(...)\`).`,
|
|
374
|
+
` Future<List<ActivityEvent>> activityLog({String? base, int? limit}) async =>`,
|
|
375
|
+
` (await bridge.activityLog(base: base, limit: limit))`,
|
|
376
|
+
` .map((r) => renderActivity(r.cast<String, dynamic>()))`,
|
|
377
|
+
` .whereType<ActivityEvent>()`,
|
|
378
|
+
` .toList();`,
|
|
379
|
+
``,
|
|
380
|
+
` /// NARRATIVE (for an in-app LLM): the workspace story — the activity log folded to typed events + a`,
|
|
381
|
+
` /// plain-English [ActivityNarrative.transcript] grounded in the VERIFIED log. Filter by [actor] /`,
|
|
382
|
+
` /// [category] / [since] (ISO occurredAt lower bound); [limit] caps the rows pulled. Newest-first.`,
|
|
383
|
+
` Future<ActivityNarrative> describe({String? since, String? actor, String? category, int? limit}) async {`,
|
|
384
|
+
` final raw = await bridge.activityLog(limit: limit);`,
|
|
385
|
+
` var events = raw`,
|
|
386
|
+
` .map((r) => renderActivity(r.cast<String, dynamic>()))`,
|
|
387
|
+
` .whereType<ActivityEvent>()`,
|
|
388
|
+
` .toList();`,
|
|
389
|
+
` if (actor != null) events = events.where((e) => e.actorSubject == actor).toList();`,
|
|
390
|
+
` if (category != null) events = events.where((e) => e.category == category).toList();`,
|
|
391
|
+
` if (since != null) events = events.where((e) => e.occurredAt.compareTo(since) >= 0).toList();`,
|
|
392
|
+
` return activityNarrative(events);`,
|
|
393
|
+
` }`,
|
|
394
|
+
``,
|
|
395
|
+
` /// NARRATIVE for ONE thing: the story of the aggregate [aggregateId] — events whose declared refs`,
|
|
396
|
+
` /// point at it (only directives declaring a ref to the aggregate appear in its story) + a transcript.`,
|
|
397
|
+
` Future<ActivityNarrative> describeHistory(String aggregateId, {int? limit}) async {`,
|
|
398
|
+
` final raw = await bridge.activityLog(limit: limit);`,
|
|
399
|
+
` final events = raw`,
|
|
400
|
+
` .map((r) => renderActivity(r.cast<String, dynamic>()))`,
|
|
401
|
+
` .whereType<ActivityEvent>()`,
|
|
402
|
+
` .where((e) => e.refs.values.any((ref) => ref.id == aggregateId))`,
|
|
403
|
+
` .toList();`,
|
|
404
|
+
` return activityNarrative(events);`,
|
|
405
|
+
` }`,
|
|
406
|
+
``,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
348
410
|
return [
|
|
349
411
|
`// AUTO-GENERATED by nomos-compile — typed ASYNC Dart client for domain "${domain}".`,
|
|
350
412
|
`// Drives the REAL githolon over a NomosBridge (the WebView runner). Regenerated every compile; do`,
|
|
@@ -368,6 +430,9 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
368
430
|
// Layered-packages mode: the sibling types live in their OWN package — one barrel import
|
|
369
431
|
// replaces both the relative types import and the relative framework import.
|
|
370
432
|
...(opts.typesImportUri !== undefined ? [`import '${opts.typesImportUri}';`] : [`import '${mod.name}.dart';`]),
|
|
433
|
+
// ACTIVITY FEED (flat mode): the shared pure `renderActivity` lives in the sibling `activity_registry.dart`
|
|
434
|
+
// (in layered mode it resolves through the types-package barrel above). Only when the package opts in.
|
|
435
|
+
...(opts.emitActivity === true && opts.activityImportUri !== undefined ? [`import '${opts.activityImportUri}';`] : []),
|
|
371
436
|
// Framework-shared aggregate types (RelationTupleReadModel, …) live in the ONE shared file — import
|
|
372
437
|
// them (the by-id accessors reference them) and re-export them (once per package, Dart de-dups).
|
|
373
438
|
...(opts.typesImportUri === undefined && opts.frameworkImportUri !== undefined ? [`import '${opts.frameworkImportUri}';`] : []),
|
package/src/codegen_ts.ts
CHANGED
|
@@ -40,6 +40,7 @@ import type { AggregateHandle } from "./aggregate.js";
|
|
|
40
40
|
import type { Field, ValueObjectDescriptor, ValueObjectFieldDescriptor } from "./fields.js";
|
|
41
41
|
import type { Directive } from "./directive.js";
|
|
42
42
|
import type { DomainModule } from "./codegen_dart.js";
|
|
43
|
+
import { collectActivityDescriptors, type PackageActivityEntry } from "./codegen_dart.js";
|
|
43
44
|
import type { NomosCompat } from "./compat_manifest.js";
|
|
44
45
|
import type { QueryDecl } from "./query.js";
|
|
45
46
|
import { voMetaOf, collectPayloadValueObjectDescriptors } from "./value_object.js";
|
|
@@ -665,6 +666,86 @@ export interface TsClientOptions {
|
|
|
665
666
|
readonly compat?: NomosCompat;
|
|
666
667
|
}
|
|
667
668
|
|
|
669
|
+
/** TS string literal (JSON.stringify covers escaping). */
|
|
670
|
+
const _actLit = (s: string): string => JSON.stringify(s);
|
|
671
|
+
|
|
672
|
+
/** Emit the ACTIVITY-FEED block (architecture/activity_descriptors.md): the `ActivityEvent`/`ActivityRef`/
|
|
673
|
+
* `ActivityRawRow` runtime types, a dotted payload-path getter, the per-directive `renderActivity` registry,
|
|
674
|
+
* and the shared template renderer — inline in the generated `.client.ts` (mirrors the ForeignRef placement).
|
|
675
|
+
* Empty when no directive declares `.activity(...)` — an activity-free package stays BYTE-IDENTICAL. */
|
|
676
|
+
function emitTsActivityBlock(entries: readonly PackageActivityEntry[]): string[] {
|
|
677
|
+
if (entries.length === 0) return [];
|
|
678
|
+
const arm = (e: PackageActivityEntry): string[] => {
|
|
679
|
+
const { directiveId, desc } = e;
|
|
680
|
+
const params = desc.params.map((p) => `${_actLit(p.name)}: _activityPayloadPath(row.payload, ${_actLit(p.from)})`).join(", ");
|
|
681
|
+
const refs = desc.refs
|
|
682
|
+
.map((r) => `${_actLit(r.name)}: { type: ${_actLit(r.type)}, id: _activityPayloadPath(row.payload, ${_actLit(r.from)}) }`)
|
|
683
|
+
.join(", ");
|
|
684
|
+
return [
|
|
685
|
+
` ${_actLit(directiveId)}: (row: ActivityRawRow): ActivityEvent => {`,
|
|
686
|
+
` const params: Record<string, string> = { ${params} };`,
|
|
687
|
+
` const vars = { actor: row.actorSubject, ...params };`,
|
|
688
|
+
` return {`,
|
|
689
|
+
` eventId: row.intentId, kind: ${_actLit(desc.kind)}, actorSubject: row.actorSubject, occurredAt: row.occurredAt,`,
|
|
690
|
+
` title: _renderActivityTemplate(${_actLit(desc.title)}, vars), message: _renderActivityTemplate(${_actLit(desc.message)}, vars),`,
|
|
691
|
+
` titleKey: ${_actLit(desc.titleKey)}, messageKey: ${_actLit(desc.messageKey)},`,
|
|
692
|
+
` params, refs: { ${refs} },`,
|
|
693
|
+
...(desc.category !== undefined ? [` category: ${_actLit(desc.category)},`] : []),
|
|
694
|
+
...(desc.icon !== undefined ? [` icon: ${_actLit(desc.icon)},`] : []),
|
|
695
|
+
` severity: ${_actLit(desc.severity)}, domain: row.domain, directiveId: ${_actLit(directiveId)},`,
|
|
696
|
+
` };`,
|
|
697
|
+
` },`,
|
|
698
|
+
];
|
|
699
|
+
};
|
|
700
|
+
return [
|
|
701
|
+
`// ── activity feed (architecture/activity_descriptors.md) — typed rows folded from the intent log ──`,
|
|
702
|
+
`/** A declared navigation target on an activity row (the referenced type + the resolved id). */`,
|
|
703
|
+
`export interface ActivityRef { type: string; id: string }`,
|
|
704
|
+
`/** A rendered activity row — produced by \`renderActivity\` from a directive's declared \`.activity(...)\`. */`,
|
|
705
|
+
`export interface ActivityEvent {`,
|
|
706
|
+
` eventId: string; kind: string; actorSubject: string; occurredAt: string;`,
|
|
707
|
+
` title: string; message: string; titleKey: string; messageKey: string;`,
|
|
708
|
+
` params: Record<string, string>; refs: Record<string, ActivityRef>;`,
|
|
709
|
+
` category?: string; icon?: string;`,
|
|
710
|
+
` severity: "info" | "success" | "warning" | "critical";`,
|
|
711
|
+
` domain: string; directiveId: string;`,
|
|
712
|
+
`}`,
|
|
713
|
+
`/** 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> }`,
|
|
715
|
+
`/** A NARRATIVE over the activity log (for an in-app LLM): the filtered events + a plain-English \`transcript\``,
|
|
716
|
+
` * (ONE LINE PER EVENT, oldest-first, \`"<occurredAt> — <title>: <message>"\`) — grounding text over the`,
|
|
717
|
+
` * VERIFIED log, never raw payloads. Produced by the client's \`describe\` / \`describeHistory\`. */`,
|
|
718
|
+
`export interface ActivityNarrative { events: ActivityEvent[]; transcript: string }`,
|
|
719
|
+
`/** Build the LLM-ready transcript: one line per event, OLDEST-FIRST (the log yields newest-first). */`,
|
|
720
|
+
`function _activityTranscript(events: readonly ActivityEvent[]): string {`,
|
|
721
|
+
" return [...events].reverse().map((e) => `${e.occurredAt} — ${e.title}: ${e.message}`).join(\"\\n\");",
|
|
722
|
+
`}`,
|
|
723
|
+
`/** Read a dotted path off a decoded intent payload, coerced to string ("" when absent/null). */`,
|
|
724
|
+
`function _activityPayloadPath(obj: unknown, path: string): string {`,
|
|
725
|
+
` let cur: unknown = obj;`,
|
|
726
|
+
` for (const seg of path.split(".")) {`,
|
|
727
|
+
` if (cur === null || typeof cur !== "object") return "";`,
|
|
728
|
+
` cur = (cur as Record<string, unknown>)[seg];`,
|
|
729
|
+
` }`,
|
|
730
|
+
` return cur === null || cur === undefined ? "" : String(cur);`,
|
|
731
|
+
`}`,
|
|
732
|
+
`/** Interpolate \`{name}\` placeholders in a template from vars (a missing var renders empty). */`,
|
|
733
|
+
`function _renderActivityTemplate(template: string, vars: Record<string, string>): string {`,
|
|
734
|
+
` return template.replace(/\\{([a-zA-Z0-9_]+)\\}/g, (_m, name: string) => vars[name] ?? "");`,
|
|
735
|
+
`}`,
|
|
736
|
+
`const _ACTIVITY_REGISTRY: Record<string, (row: ActivityRawRow) => ActivityEvent> = {`,
|
|
737
|
+
...entries.flatMap(arm),
|
|
738
|
+
`};`,
|
|
739
|
+
`/** Fold ONE committed-intent raw row into a typed [ActivityEvent], or null when the directive declares`,
|
|
740
|
+
` * no \`.activity(...)\`. Pure — the kernel/ledger stays the audit truth; this is a client-side projection. */`,
|
|
741
|
+
`export function renderActivity(row: ActivityRawRow): ActivityEvent | null {`,
|
|
742
|
+
` const f = _ACTIVITY_REGISTRY[row.directiveId];`,
|
|
743
|
+
` return f === undefined ? null : f(row);`,
|
|
744
|
+
`}`,
|
|
745
|
+
``,
|
|
746
|
+
];
|
|
747
|
+
}
|
|
748
|
+
|
|
668
749
|
/** TRUE when any module declares a PACKED workspace taxonomy (the routed-holon shape).
|
|
669
750
|
* Gates every slice-4 paged-surface emission: taxonomy-free clients stay BYTE-IDENTICAL. */
|
|
670
751
|
function anyPackedTaxonomy(modules: readonly DomainModule[]): boolean {
|
|
@@ -690,6 +771,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
690
771
|
);
|
|
691
772
|
const encPresent = encMap !== null;
|
|
692
773
|
const encConst = encryptionConstName(opts.packageName);
|
|
774
|
+
// ACTIVITY FEED (architecture/activity_descriptors.md): the package's declared `.activity(...)` descriptors.
|
|
775
|
+
// Empty ⇒ NOTHING activity-related emits (byte-identical to a pre-activity compile).
|
|
776
|
+
const activityEntries = collectActivityDescriptors(modules);
|
|
777
|
+
const activityPresent = activityEntries.length > 0;
|
|
693
778
|
const out: string[] = [];
|
|
694
779
|
// The PACKAGE-LEVEL recursive-type registry: every directive payload's `z.lazy` knots mint
|
|
695
780
|
// named recursive type aliases into it (deduped by lazy identity across directives); the decls
|
|
@@ -853,6 +938,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
853
938
|
` deadLetters(): Promise<{ id?: string; oid: string; source: string; error: string; domain?: string | null; directiveId?: string | null; at: string }[]>;`,
|
|
854
939
|
` retryDeadLetter(id: string): Promise<{ ok: boolean; head?: string; alreadyOnMain?: boolean; error?: string }>;`,
|
|
855
940
|
` discardDeadLetter(id: string): Promise<{ ok: boolean; removed: number }>;`,
|
|
941
|
+
...(activityPresent
|
|
942
|
+
? [
|
|
943
|
+
` /** THE ACTIVITY FEED (declared \`.activity(...)\`): subscribe to per-committed-intent RAW ROWS`,
|
|
944
|
+
` * (the runtime folds the intent log). \`cb\` fires with the current rows + on every new intent;`,
|
|
945
|
+
` * returns an unsubscribe fn. The generated client maps these through \`renderActivity\`. */`,
|
|
946
|
+
` watchActivity(cb: (rows: ActivityRawRow[]) => void, opts?: { base?: string }): () => void;`,
|
|
947
|
+
` /** One-shot: the current committed activity log as RAW ROWS (folded to typed rows by the client). */`,
|
|
948
|
+
` activityLog(opts?: { base?: string; limit?: number }): Promise<ActivityRawRow[]>;`,
|
|
949
|
+
]
|
|
950
|
+
: []),
|
|
856
951
|
` /** THE COMPAT SUPERPOWER (#72b) — OPTIONAL: a holon from @githolon/client's connect() attaches it.`,
|
|
857
952
|
` * The factory hands over this client's baked compat manifest + structural interface (the requirement),`,
|
|
858
953
|
` * and the holon computes the LIVE per-member verdict on \`holon.compat\`. A KNOWN-incompatible directive`,
|
|
@@ -1489,6 +1584,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1489
1584
|
}
|
|
1490
1585
|
}
|
|
1491
1586
|
|
|
1587
|
+
// ── activity feed (declared `.activity(...)`) — types + `renderActivity` registry (empty ⇒ nothing) ──
|
|
1588
|
+
out.push(...emitTsActivityBlock(activityEntries));
|
|
1589
|
+
|
|
1492
1590
|
// ── payload types + per-domain client factories ──
|
|
1493
1591
|
const usedPayloadNames = new Set<string>();
|
|
1494
1592
|
for (const mod of modules) {
|
|
@@ -1714,6 +1812,38 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1714
1812
|
);
|
|
1715
1813
|
}
|
|
1716
1814
|
|
|
1815
|
+
if (activityPresent) {
|
|
1816
|
+
methods.push(
|
|
1817
|
+
` /** THE ACTIVITY FEED (declared \`.activity(...)\`): typed activity rows folded from the intent log.`,
|
|
1818
|
+
` * \`cb\` fires with the current rows + on every new committed intent; returns stop(). Directives with`,
|
|
1819
|
+
` * no declared activity are dropped. */`,
|
|
1820
|
+
` watchActivity: (cb: (events: ActivityEvent[]) => void, opts?: { base?: string }) =>`,
|
|
1821
|
+
` holon.watchActivity((rows) => cb(rows.map(renderActivity).filter((e): e is ActivityEvent => e !== null)), opts),`,
|
|
1822
|
+
` /** One-shot: the current committed activity log, folded to typed [ActivityEvent]s. */`,
|
|
1823
|
+
` activityLog: async (opts?: { base?: string; limit?: number }): Promise<ActivityEvent[]> =>`,
|
|
1824
|
+
` (await holon.activityLog(opts)).map(renderActivity).filter((e): e is ActivityEvent => e !== null),`,
|
|
1825
|
+
` /** NARRATIVE (for an in-app LLM): the workspace story — the activity log folded to typed events +`,
|
|
1826
|
+
` * a plain-English \`transcript\` grounded in the VERIFIED log. Filter by \`actor\` / \`category\` /`,
|
|
1827
|
+
` * \`since\` (ISO occurredAt lower bound); \`limit\` caps the rows pulled. Events are newest-first. */`,
|
|
1828
|
+
` describe: async (opts: { since?: string; actor?: string; category?: string; limit?: number } = {}): Promise<ActivityNarrative> => {`,
|
|
1829
|
+
` const raw = await holon.activityLog(opts.limit !== undefined ? { limit: opts.limit } : {});`,
|
|
1830
|
+
` let events = raw.map(renderActivity).filter((e): e is ActivityEvent => e !== null);`,
|
|
1831
|
+
` if (opts.actor !== undefined) events = events.filter((e) => e.actorSubject === opts.actor);`,
|
|
1832
|
+
` if (opts.category !== undefined) events = events.filter((e) => e.category === opts.category);`,
|
|
1833
|
+
` if (opts.since !== undefined) events = events.filter((e) => e.occurredAt >= opts.since!);`,
|
|
1834
|
+
` return { events, transcript: _activityTranscript(events) };`,
|
|
1835
|
+
` },`,
|
|
1836
|
+
` /** NARRATIVE for ONE thing: the story of the aggregate \`aggregateId\` — events whose declared refs`,
|
|
1837
|
+
` * point at it (only directives declaring a ref to the aggregate appear in its story) + a transcript. */`,
|
|
1838
|
+
` describeHistory: async (aggregateId: string, opts: { limit?: number } = {}): Promise<ActivityNarrative> => {`,
|
|
1839
|
+
` const raw = await holon.activityLog(opts.limit !== undefined ? { limit: opts.limit } : {});`,
|
|
1840
|
+
` const events = raw.map(renderActivity).filter((e): e is ActivityEvent => e !== null)`,
|
|
1841
|
+
` .filter((e) => Object.values(e.refs).some((r) => r.id === aggregateId));`,
|
|
1842
|
+
` return { events, transcript: _activityTranscript(events) };`,
|
|
1843
|
+
` },`,
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1717
1847
|
out.push(
|
|
1718
1848
|
`/** Typed client for the \`${domain}\` domain, bound to a connected holon.`,
|
|
1719
1849
|
` * Pass \`{ actor }\` (the verified principal uid) to author AS that user — every write seals it as`,
|
|
@@ -56,6 +56,8 @@ import type { AggregateHandle } from "./aggregate.js";
|
|
|
56
56
|
import {
|
|
57
57
|
generateDartDomain,
|
|
58
58
|
generateDartContracts,
|
|
59
|
+
generateDartActivityRegistry,
|
|
60
|
+
collectActivityDescriptors,
|
|
59
61
|
type DartImport,
|
|
60
62
|
type DartRefImport,
|
|
61
63
|
type DomainModule,
|
|
@@ -428,6 +430,7 @@ const DART_SUPPORT_CORE_SRC = [
|
|
|
428
430
|
// The foreign-ref locator runtime (ForeignRef<T>/NomosForeignReader/…, t.foreignRef) — a generated read
|
|
429
431
|
// model with a `t.foreignRef` field references `ForeignRef<Target>` through the barrel, so it must vendor.
|
|
430
432
|
"foreign_ref.dart",
|
|
433
|
+
"activity.dart",
|
|
431
434
|
] as const;
|
|
432
435
|
// The RUNTIME support files, vendored from `nomos_client` (dsl/dart/lib/src): the async WebView
|
|
433
436
|
// bridge the generated `<name>.client.dart` drives, and the provider surface. Their
|
|
@@ -450,6 +453,10 @@ const FRAMEWORK_FILE = "nomos_framework";
|
|
|
450
453
|
/** The shared basename of the domain-contracts DTO file emitted once per package (domain_contracts.md). */
|
|
451
454
|
const CONTRACTS_FILE = "contracts";
|
|
452
455
|
|
|
456
|
+
/** The shared basename of the ACTIVITY REGISTRY file emitted once per package (activity_descriptors.md).
|
|
457
|
+
* Absent when no directive declares `.activity(...)` — an activity-free package emits no `activity_registry.dart`. */
|
|
458
|
+
const ACTIVITY_FILE = "activity_registry";
|
|
459
|
+
|
|
453
460
|
/** Collect every domain contract across the package's modules, deduped by id (later-wins). Sorted by id.
|
|
454
461
|
* Empty when no domain declares a contract (contract-free packages emit no `contracts.dart`). */
|
|
455
462
|
function collectPackageContracts(modules: readonly DomainModule[]): ContractHandle[] {
|
|
@@ -731,6 +738,14 @@ function emitDart(
|
|
|
731
738
|
if (packageContracts.length > 0) {
|
|
732
739
|
writeGenerated(CONTRACTS_FILE, generateDartContracts(packageContracts));
|
|
733
740
|
}
|
|
741
|
+
// 2a-ter. the shared ACTIVITY REGISTRY (`activity_registry.dart`) — the pure `renderActivity`, ONCE. Emitted
|
|
742
|
+
// iff a directive opts in via `.activity(...)` (activity-free packages stay byte-identical). Pure
|
|
743
|
+
// (nomos_types), so the client folds typed activity rows from the intent log with no runtime import.
|
|
744
|
+
const packageActivity = collectActivityDescriptors(modules);
|
|
745
|
+
const activityImportUri = packageActivity.length > 0 ? `${ACTIVITY_FILE}.dart` : undefined;
|
|
746
|
+
if (packageActivity.length > 0) {
|
|
747
|
+
writeGenerated(ACTIVITY_FILE, generateDartActivityRegistry(packageActivity));
|
|
748
|
+
}
|
|
734
749
|
// CROSS-DOMAIN `t.foreignRef` (auto import). A foreignRef field typed `ForeignRef<CatalogueListing>` +
|
|
735
750
|
// `CatalogueListing.fromJson` needs the TARGET aggregate's Dart class — which lives in the target DOMAIN's
|
|
736
751
|
// generated file. Within ONE package the compiler knows which domain owns each aggregate, so we auto-emit
|
|
@@ -794,6 +809,7 @@ function emitDart(
|
|
|
794
809
|
...(compat !== undefined ? { compat } : {}),
|
|
795
810
|
supportImport,
|
|
796
811
|
...(domainFrameworkImport !== undefined ? { frameworkImportUri: domainFrameworkImport } : {}),
|
|
812
|
+
...(packageActivity.length > 0 ? { emitActivity: true, ...(activityImportUri !== undefined ? { activityImportUri } : {}) } : {}),
|
|
797
813
|
});
|
|
798
814
|
const cp = path.join(genDir, `${mod.name}.client.dart`);
|
|
799
815
|
writeFileSync(cp, clientSrc, "utf8");
|
|
@@ -820,6 +836,7 @@ function emitDart(
|
|
|
820
836
|
: `export 'package:nomos_client/nomos_client.dart';`,
|
|
821
837
|
...(hasFrameworkSymbols ? [`export '${genRel}/${FRAMEWORK_FILE}.dart';`] : []),
|
|
822
838
|
...(contractsImportUri !== undefined ? [`export '${genRel}/${CONTRACTS_FILE}.dart';`] : []),
|
|
839
|
+
...(activityImportUri !== undefined ? [`export '${genRel}/${ACTIVITY_FILE}.dart';`] : []),
|
|
823
840
|
...modules.flatMap((m) => [
|
|
824
841
|
`export '${genRel}/${m.name}.dart';`,
|
|
825
842
|
`export '${genRel}/${m.name}.client.dart';`,
|
|
@@ -844,8 +861,10 @@ function emitDart(
|
|
|
844
861
|
/** The pure-core support import generated TYPES files carry in layered-packages mode. */
|
|
845
862
|
const DART_TYPES_IMPORT = `import 'package:nomos_types/nomos_types.dart';`;
|
|
846
863
|
/** Version constraints the scaffolded pubspecs pin — move together with the Dart packages' releases. */
|
|
847
|
-
|
|
848
|
-
const
|
|
864
|
+
// FLOOR = the release carrying every symbol generated code may reference (0.1.2: ActivityEvent).
|
|
865
|
+
const NOMOS_TYPES_DEP = "^0.1.2";
|
|
866
|
+
// FLOOR = the release carrying the bridge methods generated clients call (0.44.0: activityLog/watchActivity).
|
|
867
|
+
const NOMOS_CLIENT_DEP = "^0.44.0";
|
|
849
868
|
|
|
850
869
|
const isPubPackageName = (s: string): boolean => /^[a-z][a-z0-9_]*$/.test(s);
|
|
851
870
|
|
|
@@ -960,7 +979,7 @@ function emitDartPackages(
|
|
|
960
979
|
// field as the target aggregate's class, which lives in the target domain's file). A sibling types file is
|
|
961
980
|
// still PURE (no runtime/bridge surface — the purity gate exists to keep nomos_client out, not to isolate
|
|
962
981
|
// domain files from each other; Dart allows type-import cycles for mutual refs).
|
|
963
|
-
const siblingTypeFiles = new Set<string>([...modules.map((m) => `${m.name}.dart`), `${FRAMEWORK_FILE}.dart`, `${CONTRACTS_FILE}.dart`]);
|
|
982
|
+
const siblingTypeFiles = new Set<string>([...modules.map((m) => `${m.name}.dart`), `${FRAMEWORK_FILE}.dart`, `${CONTRACTS_FILE}.dart`, `${ACTIVITY_FILE}.dart`]);
|
|
964
983
|
const writeTypes = (fileBase: string, src: string): void => {
|
|
965
984
|
if (!src.includes(DART_PACKAGE_IMPORT)) {
|
|
966
985
|
fail(`generated Dart for '${fileBase}' does not carry the expected support import — codegen_dart header drifted`);
|
|
@@ -994,6 +1013,10 @@ function emitDartPackages(
|
|
|
994
1013
|
const packageContracts = collectPackageContracts(modules);
|
|
995
1014
|
const contractsImportUri = packageContracts.length > 0 ? `${CONTRACTS_FILE}.dart` : undefined;
|
|
996
1015
|
if (packageContracts.length > 0) writeTypes(CONTRACTS_FILE, generateDartContracts(packageContracts));
|
|
1016
|
+
// ACTIVITY REGISTRY (layered): the pure `renderActivity` in the TYPES package (nomos_types only). The
|
|
1017
|
+
// client resolves it through the types-package barrel (which re-exports every types file). Opt-in only.
|
|
1018
|
+
const packageActivity = collectActivityDescriptors(modules);
|
|
1019
|
+
if (packageActivity.length > 0) writeTypes(ACTIVITY_FILE, generateDartActivityRegistry(packageActivity));
|
|
997
1020
|
// CROSS-DOMAIN foreignRef (layered): map aggregate id → its domain's type file, so a foreignRef field's
|
|
998
1021
|
// target class resolves via a sibling import (the purity gate now permits sibling type files).
|
|
999
1022
|
const aggIdToTypeFile = new Map<string, string>();
|
|
@@ -1085,6 +1108,7 @@ function emitDartPackages(
|
|
|
1085
1108
|
...(compat !== undefined ? { compat } : {}),
|
|
1086
1109
|
supportImport: "package",
|
|
1087
1110
|
typesImportUri,
|
|
1111
|
+
...(packageActivity.length > 0 ? { emitActivity: true } : {}),
|
|
1088
1112
|
});
|
|
1089
1113
|
const cp = path.join(clientGen, `${mod.name}.client.dart`);
|
|
1090
1114
|
writeFileSync(cp, clientSrc, "utf8");
|
package/src/directive.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { Ports } from "./ctx.js";
|
|
|
23
23
|
import type { CertifiedReadDecl } from "./certified_read.js";
|
|
24
24
|
import type { RelationDecl } from "./relation.js";
|
|
25
25
|
import type { QueryDecl } from "./query.js";
|
|
26
|
+
import { normalizeActivity, type ActivityInput, type ActivityDescriptor } from "./activity.js";
|
|
26
27
|
import { BIRTH_METER_QUERIES } from "./birth_meter.js";
|
|
27
28
|
import {
|
|
28
29
|
isFromPayloadRef,
|
|
@@ -115,6 +116,15 @@ export interface Directive<P = unknown> {
|
|
|
115
116
|
* byte-identical in the canonical manifest to before `description` existed).
|
|
116
117
|
*/
|
|
117
118
|
readonly description?: string;
|
|
119
|
+
/**
|
|
120
|
+
* OPT-IN user-facing ACTIVITY metadata (`architecture/activity_descriptors.md`) — the kind/title/message
|
|
121
|
+
* template, refs, params, category/icon/severity a client uses to render a typed activity row from this
|
|
122
|
+
* directive's intents (no string-enricher). Set by `.activity({...})`; NON-LAW, additive (same class as
|
|
123
|
+
* `description` — a directive that never declares activity is byte-identical). Normalized + validated at
|
|
124
|
+
* author time. `undefined` ⇒ this directive contributes no activity row. (The setter is `.activity({...})`;
|
|
125
|
+
* this stored field is `activityDescriptor` to avoid a method/field name clash on `RequirableDirective`.)
|
|
126
|
+
*/
|
|
127
|
+
readonly activityDescriptor?: ActivityDescriptor;
|
|
118
128
|
/**
|
|
119
129
|
* NATIVE ZANZIBAR write-authz (P4): the RELATION the captured author must HOLD to author this
|
|
120
130
|
* directive, and the OBJECT it is checked on. The ONE gate runs `engine.check(schema, object,
|
|
@@ -414,6 +424,14 @@ export interface RequirableDirective<P> extends Directive<P> {
|
|
|
414
424
|
* calls this declares no prose (codegen falls back to a default derived from the id).
|
|
415
425
|
*/
|
|
416
426
|
doc(description: string): RequirableDirective<P>;
|
|
427
|
+
/**
|
|
428
|
+
* Declare OPT-IN user-facing ACTIVITY metadata (`architecture/activity_descriptors.md`) — a stable `kind`,
|
|
429
|
+
* a `{actor}`+params message template, navigation `refs`, display `params` (payload field paths), and
|
|
430
|
+
* category/icon/severity. Codegen emits a typed activity descriptor + `renderActivity` so a client folds
|
|
431
|
+
* typed activity rows from the intent log (no string-enricher). Validated at author time (unknown template
|
|
432
|
+
* var / missing payload field refuse). Non-law, additive; a directive that never calls it is byte-identical.
|
|
433
|
+
*/
|
|
434
|
+
activity(input: ActivityInput): RequirableDirective<P>;
|
|
417
435
|
/**
|
|
418
436
|
* Declare the directive's CertifiedReads (survival Constraint 5; `certified_read.md`
|
|
419
437
|
* §"Tenant authoring") — the profiled, named write-path reads its `decide()` consults, the
|
|
@@ -810,6 +828,11 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
|
810
828
|
doc(description: string): RequirableDirective<P> {
|
|
811
829
|
return makeRequirable({ ...base, description });
|
|
812
830
|
},
|
|
831
|
+
activity(input: ActivityInput): RequirableDirective<P> {
|
|
832
|
+
// Normalize + validate against this directive's payload (unknown template var / missing payload field
|
|
833
|
+
// refuse here). Non-law, additive — stored like `description`.
|
|
834
|
+
return makeRequirable({ ...base, activityDescriptor: normalizeActivity(base.id, input, base.payloadSchema) });
|
|
835
|
+
},
|
|
813
836
|
readsCertified(reads: Record<string, CertifiedReadDecl>): RequirableDirective<P> {
|
|
814
837
|
// Accumulate onto any prior declared CertifiedReads (a repeated local name's later decl
|
|
815
838
|
// wins) — additive, non-destructive. The local binding NAME (the key) is what `decide`
|
package/src/index.ts
CHANGED
|
@@ -136,6 +136,7 @@ export {
|
|
|
136
136
|
// surface). The dev never sees a "prim" — that is the GitHolon's internal OpenUSD IR.
|
|
137
137
|
export { create, existing, ensure, type AggregateRef } from "./authoring.js";
|
|
138
138
|
export { contract, isContractHandle, satisfiesContract, type ContractHandle, type ContractField, type ContractSatisfaction } from "./contract.js";
|
|
139
|
+
export { normalizeActivity, renderActivityTemplate, activityKey, type ActivityInput, type ActivityDescriptor, type ActivityRefInput, type ActivitySeverity } from "./activity.js";
|
|
139
140
|
// THE CAPTURED READ — a plan reads O(1) DSL queries via `read(query, args)`; the result is committed as
|
|
140
141
|
// the intent's read footprint (deterministic replay + stale-premise detection). The dev calls typed DSL
|
|
141
142
|
// queries; the library routes them here. The host capability is `nomos.read` (twin of `nomos.rng`).
|