@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/src/codegen_dart.ts
CHANGED
|
@@ -744,7 +744,11 @@ function aggFieldSpec(
|
|
|
744
744
|
// workspace field give the app `resolve`/`watch`/`attest` on demand, without cloning the other holon.
|
|
745
745
|
// This takes precedence over the same-workspace typed-id path below.
|
|
746
746
|
if (field.foreignRef !== undefined) {
|
|
747
|
-
|
|
747
|
+
// A contract target names the shared DTO class `<ContractId>Contract` (emitted into the shared
|
|
748
|
+
// `contracts.dart`); the aggregate class is `<ContractId>` and must not collide, hence the suffix.
|
|
749
|
+
const target = field.foreignRef.targetIsContract === true
|
|
750
|
+
? `${field.foreignRef.targetAggregateId}Contract`
|
|
751
|
+
: field.foreignRef.targetAggregateId;
|
|
748
752
|
return {
|
|
749
753
|
name,
|
|
750
754
|
wireName,
|
|
@@ -1904,6 +1908,227 @@ function emitValueObjectClass(d: ValueObjectDescriptor): string {
|
|
|
1904
1908
|
].join("\n");
|
|
1905
1909
|
}
|
|
1906
1910
|
|
|
1911
|
+
/** The Dart type a CONTRACT field decodes to (domain_contracts.md) — a public DTO leaf. Contracts are
|
|
1912
|
+
* plain shapes (no merge state), so map by kind: scalars to their Dart primitive, a named VO to its class,
|
|
1913
|
+
* a ref/foreignRef to the raw id String. Everything is nullable (a projected DTO is a partial fold). */
|
|
1914
|
+
function dartTypeOfContractField(field: Field): string {
|
|
1915
|
+
switch (field.kind) {
|
|
1916
|
+
case "int":
|
|
1917
|
+
case "counter":
|
|
1918
|
+
return "int";
|
|
1919
|
+
case "bool":
|
|
1920
|
+
return "bool";
|
|
1921
|
+
case "json":
|
|
1922
|
+
return field.valueObject !== undefined ? field.valueObject.name : "Map<String, dynamic>";
|
|
1923
|
+
default:
|
|
1924
|
+
// string / enum / position / ref / foreignRef — all carried as a String on the wire.
|
|
1925
|
+
return "String";
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
/** The fromJson decode expression for a CONTRACT field, reading `data['<wire>']`. */
|
|
1930
|
+
function contractFromExpr(field: Field, wire: string): string {
|
|
1931
|
+
const acc = `data['${wire}']`;
|
|
1932
|
+
switch (field.kind) {
|
|
1933
|
+
case "int":
|
|
1934
|
+
case "counter":
|
|
1935
|
+
return `(${acc} as num?)?.toInt()`;
|
|
1936
|
+
case "bool":
|
|
1937
|
+
return `${acc} as bool?`;
|
|
1938
|
+
case "json":
|
|
1939
|
+
if (field.valueObject !== undefined) {
|
|
1940
|
+
return `${acc} == null ? null : ${field.valueObject.name}.fromProjected((${acc} as Map).cast<String, Object?>())`;
|
|
1941
|
+
}
|
|
1942
|
+
return `${acc} == null ? null : (${acc} as Map).cast<String, dynamic>()`;
|
|
1943
|
+
default:
|
|
1944
|
+
return `${acc} as String?`;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* Emit an immutable Dart class for ONE domain contract (a public DTO — architecture/domain_contracts.md).
|
|
1950
|
+
* The class is `<ContractId>Contract` (the suffix keeps it distinct from an identically-named aggregate's
|
|
1951
|
+
* `<ContractId>` read-model class). `final` fields + a `const` ctor + a `fromJson(Map, [id])` factory. Pure —
|
|
1952
|
+
* references only nomos_types primitives (+ any VO class, which is shared). Emitted into `contracts.dart`.
|
|
1953
|
+
*/
|
|
1954
|
+
function emitDartContractClass(c: import("./contract.js").ContractHandle): string {
|
|
1955
|
+
const cls = `${c.id}Contract`;
|
|
1956
|
+
const fields = Object.entries(c.fields).map(([wireName, f]) => ({
|
|
1957
|
+
wireName,
|
|
1958
|
+
member: dartMemberName(wireName),
|
|
1959
|
+
dartType: dartTypeOfContractField(f as Field),
|
|
1960
|
+
from: contractFromExpr(f as Field, wireName),
|
|
1961
|
+
}));
|
|
1962
|
+
return [
|
|
1963
|
+
`/// Generated domain CONTRACT '${c.id}' (architecture/domain_contracts.md) — a public DTO: a named,`,
|
|
1964
|
+
`/// stable shape other domains reference WITHOUT importing the concrete aggregate. Immutable; decoded`,
|
|
1965
|
+
`/// from a projection row via [${cls}.fromJson]. Fields are nullable (a projected DTO is a partial fold).`,
|
|
1966
|
+
`class ${cls} {`,
|
|
1967
|
+
` final String id;`,
|
|
1968
|
+
...fields.map((x) => ` final ${x.dartType}? ${x.member};`),
|
|
1969
|
+
``,
|
|
1970
|
+
` const ${cls}({`,
|
|
1971
|
+
` this.id = '',`,
|
|
1972
|
+
...fields.map((x) => ` this.${x.member},`),
|
|
1973
|
+
` });`,
|
|
1974
|
+
``,
|
|
1975
|
+
` factory ${cls}.fromJson(Map<String, dynamic> data, [String id = '']) => ${cls}(`,
|
|
1976
|
+
` id: id,`,
|
|
1977
|
+
...fields.map((x) => ` ${x.member}: ${x.from},`),
|
|
1978
|
+
` );`,
|
|
1979
|
+
`}`,
|
|
1980
|
+
].join("\n");
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* The full source of the shared `contracts.dart` file — every domain contract's DTO class, emitted ONCE
|
|
1985
|
+
* per package into the TYPES package (both the flat and the layered emit). Carries the standard support
|
|
1986
|
+
* import header so `compile_package_main`'s uniform rewrite (→ vendored relative / → nomos_types) applies;
|
|
1987
|
+
* the classes themselves reference only nomos_types primitives (+ shared VO classes), so the file is PURE.
|
|
1988
|
+
*/
|
|
1989
|
+
export function generateDartContracts(contracts: readonly import("./contract.js").ContractHandle[]): string {
|
|
1990
|
+
const sorted = [...contracts].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1991
|
+
return [
|
|
1992
|
+
`// GENERATED by nomos-compile — the package's shared domain CONTRACTS (public DTOs). DO NOT EDIT.`,
|
|
1993
|
+
`// Every domain depends on this shared layer; no domain imports another's concrete types (the acyclic graph).`,
|
|
1994
|
+
`//`,
|
|
1995
|
+
`// ignore_for_file: unused_import`,
|
|
1996
|
+
`library;`,
|
|
1997
|
+
``,
|
|
1998
|
+
`import 'package:nomos_client/nomos_client.dart';`,
|
|
1999
|
+
``,
|
|
2000
|
+
...sorted.map(emitDartContractClass),
|
|
2001
|
+
``,
|
|
2002
|
+
].join("\n");
|
|
2003
|
+
}
|
|
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
|
+
|
|
1907
2132
|
/** True when ANY aggregate field in the module is `t.evidence()` — gates emitting the
|
|
1908
2133
|
* shared [emitEvidenceRefClass] once. */
|
|
1909
2134
|
function moduleUsesEvidence(aggregates: readonly AggregateHandle[]): boolean {
|
|
@@ -3578,6 +3803,15 @@ export interface DomainModule {
|
|
|
3578
3803
|
* additive `nomosValueObjects` USD key (`usd.ts`).
|
|
3579
3804
|
*/
|
|
3580
3805
|
valueObjects?: readonly ValueObjectDescriptor[];
|
|
3806
|
+
/**
|
|
3807
|
+
* DOMAIN CONTRACTS (`architecture/domain_contracts.md`) — the public DTO shapes this domain declares
|
|
3808
|
+
* (`contract(id, fields)`), discovered by shape and composed across the domain's modules. ADDITIVE +
|
|
3809
|
+
* OPTIONAL: a contract-free domain omits this entirely and is byte-identical to before contracts existed.
|
|
3810
|
+
* Contracts are PURE typing/codegen metadata (NOT hashed law); they drive the additive `nomosContracts`
|
|
3811
|
+
* IR key + the shared `contracts.dart` DTO classes + the TS `<Id>Contract` interfaces. `implementedBy`
|
|
3812
|
+
* lists the concrete aggregate ids that `.implements(...)` this contract (from `implementsContracts`).
|
|
3813
|
+
*/
|
|
3814
|
+
contracts?: readonly import("./contract.js").ContractHandle[];
|
|
3581
3815
|
/**
|
|
3582
3816
|
* The domain's FIRST-CLASS WORKSPACE TYPES (`workspace_type.ts`; sharding §10
|
|
3583
3817
|
* RATIFIED, slice 1) — the law-declared workspace taxonomy
|
|
@@ -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";
|
|
@@ -499,8 +500,14 @@ function tsBaseTypeOfField(field: Field): string {
|
|
|
499
500
|
case "ref": {
|
|
500
501
|
// A `t.foreignRef` field surfaces as a TYPED cross-workspace locator (hydrated by the accessors from
|
|
501
502
|
// the raw id + the declared workspace field); a plain same-workspace `t.ref` stays the id string.
|
|
502
|
-
const fr = (field as { foreignRef?: { targetAggregateId: string } }).foreignRef;
|
|
503
|
-
|
|
503
|
+
const fr = (field as { foreignRef?: { targetAggregateId: string; targetIsContract?: boolean } }).foreignRef;
|
|
504
|
+
// A contract target types against the shared `<ContractId>Contract` interface (the public DTO),
|
|
505
|
+
// not the concrete `<Agg>Data` — that is how the reference stays acyclic.
|
|
506
|
+
return fr !== undefined
|
|
507
|
+
? fr.targetIsContract === true
|
|
508
|
+
? `ForeignRef<${fr.targetAggregateId}Contract>`
|
|
509
|
+
: `ForeignRef<${fr.targetAggregateId}Data>`
|
|
510
|
+
: "string";
|
|
504
511
|
}
|
|
505
512
|
case "json":
|
|
506
513
|
// A NAMED VALUE OBJECT field reads as its generated interface (`value_object.ts`);
|
|
@@ -659,6 +666,86 @@ export interface TsClientOptions {
|
|
|
659
666
|
readonly compat?: NomosCompat;
|
|
660
667
|
}
|
|
661
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
|
+
|
|
662
749
|
/** TRUE when any module declares a PACKED workspace taxonomy (the routed-holon shape).
|
|
663
750
|
* Gates every slice-4 paged-surface emission: taxonomy-free clients stay BYTE-IDENTICAL. */
|
|
664
751
|
function anyPackedTaxonomy(modules: readonly DomainModule[]): boolean {
|
|
@@ -684,6 +771,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
684
771
|
);
|
|
685
772
|
const encPresent = encMap !== null;
|
|
686
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;
|
|
687
778
|
const out: string[] = [];
|
|
688
779
|
// The PACKAGE-LEVEL recursive-type registry: every directive payload's `z.lazy` knots mint
|
|
689
780
|
// named recursive type aliases into it (deduped by lazy identity across directives); the decls
|
|
@@ -847,6 +938,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
847
938
|
` deadLetters(): Promise<{ id?: string; oid: string; source: string; error: string; domain?: string | null; directiveId?: string | null; at: string }[]>;`,
|
|
848
939
|
` retryDeadLetter(id: string): Promise<{ ok: boolean; head?: string; alreadyOnMain?: boolean; error?: string }>;`,
|
|
849
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
|
+
: []),
|
|
850
951
|
` /** THE COMPAT SUPERPOWER (#72b) — OPTIONAL: a holon from @githolon/client's connect() attaches it.`,
|
|
851
952
|
` * The factory hands over this client's baked compat manifest + structural interface (the requirement),`,
|
|
852
953
|
` * and the holon computes the LIVE per-member verdict on \`holon.compat\`. A KNOWN-incompatible directive`,
|
|
@@ -1218,6 +1319,24 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1218
1319
|
}
|
|
1219
1320
|
}
|
|
1220
1321
|
|
|
1322
|
+
// ── domain contracts: one `<Id>Contract` interface per public DTO shape, deduped across domains ──
|
|
1323
|
+
const contractByIdTs = new Map<string, import("./contract.js").ContractHandle>();
|
|
1324
|
+
for (const mod of modules) {
|
|
1325
|
+
for (const c of ((mod as { contracts?: readonly import("./contract.js").ContractHandle[] }).contracts ?? [])) {
|
|
1326
|
+
contractByIdTs.set(c.id, c);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (contractByIdTs.size > 0) {
|
|
1330
|
+
out.push(`// ── domain contracts (public DTO shapes — architecture/domain_contracts.md) ──`, ``);
|
|
1331
|
+
for (const [, c] of [...contractByIdTs].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1332
|
+
out.push(`export interface ${c.id}Contract {`);
|
|
1333
|
+
for (const [name, field] of Object.entries(c.fields)) {
|
|
1334
|
+
out.push(` ${name}?: ${tsTypeOfField(field as Field)};`);
|
|
1335
|
+
}
|
|
1336
|
+
out.push(`}`, ``);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1221
1340
|
out.push(`// ── read models (projection \`data\` shapes; partial-fold optional, EXCEPT .fromBirth()`, `// fields — their presence is gate law, so the type is non-optional) ──`, ``);
|
|
1222
1341
|
const foreignRefFields: { agg: string; field: string; workspaceField: string; target: string }[] = [];
|
|
1223
1342
|
// wrap a read accessor return expression through the foreignRef row-hydrator (identity + byte-identical
|
|
@@ -1465,6 +1584,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1465
1584
|
}
|
|
1466
1585
|
}
|
|
1467
1586
|
|
|
1587
|
+
// ── activity feed (declared `.activity(...)`) — types + `renderActivity` registry (empty ⇒ nothing) ──
|
|
1588
|
+
out.push(...emitTsActivityBlock(activityEntries));
|
|
1589
|
+
|
|
1468
1590
|
// ── payload types + per-domain client factories ──
|
|
1469
1591
|
const usedPayloadNames = new Set<string>();
|
|
1470
1592
|
for (const mod of modules) {
|
|
@@ -1690,6 +1812,38 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1690
1812
|
);
|
|
1691
1813
|
}
|
|
1692
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
|
+
|
|
1693
1847
|
out.push(
|
|
1694
1848
|
`/** Typed client for the \`${domain}\` domain, bound to a connected holon.`,
|
|
1695
1849
|
` * Pass \`{ actor }\` (the verified principal uid) to author AS that user — every write seals it as`,
|