@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.
@@ -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
- const target = field.foreignRef.targetAggregateId;
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
- return fr !== undefined ? `ForeignRef<${fr.targetAggregateId}Data>` : "string";
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`,