@githolon/dsl 0.100.5 → 0.101.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.
Files changed (97) hide show
  1. package/dart/CHANGELOG.md +12 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +220 -71
  4. package/dart/lib/src/webview_bridge.dart +640 -126
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/pubspec.yaml +2 -2
  7. package/dart/test/acquire_session_credentials_test.dart +1 -1
  8. package/dart/test/adv_realm_interference_test.dart +1 -1
  9. package/dart/test/bundle_version_pairing_test.dart +11 -22
  10. package/dart/test/cluster_attested_test.dart +1 -1
  11. package/dart/test/cluster_births_test.dart +1 -1
  12. package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
  13. package/dart/test/cluster_local_autosync_test.dart +1 -1
  14. package/dart/test/cluster_scale_test.dart +1 -1
  15. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  16. package/dart/test/dlq_session_surface_test.dart +1 -1
  17. package/dart/test/errors_test.dart +63 -12
  18. package/dart/test/first_app_gauntlet_test.dart +132 -87
  19. package/dart/test/local_sync_inert_test.dart +10 -5
  20. package/dart/test/parent_signer_recovery_test.dart +1 -1
  21. package/dart/test/sealing_client_test.dart +1 -1
  22. package/dart/test/session_bound_test.dart +1 -1
  23. package/dart/test/test_harness_test.dart +2 -2
  24. package/dart/test/vo_read_typing_test.dart +1 -1
  25. package/dart/test/watch_after_offer_stall_test.dart +1 -1
  26. package/dart/test/webview_bridge_test.dart +274 -0
  27. package/dart_types/CHANGELOG.md +6 -0
  28. package/dart_types/lib/src/errors.dart +136 -30
  29. package/dart_types/lib/src/surfaced.dart +48 -2
  30. package/dart_types/lib/src/wire.dart +3 -1
  31. package/dart_types/pubspec.yaml +1 -1
  32. package/dart_types/test/surfaced_test.dart +19 -0
  33. package/package.json +1 -1
  34. package/src/activity.ts +43 -4
  35. package/src/aggregate.ts +132 -22
  36. package/src/attested_core.ts +1 -1
  37. package/src/authoring.ts +119 -6
  38. package/src/birth_meter.ts +4 -4
  39. package/src/birth_meter_ensure.ts +165 -29
  40. package/src/births.ts +53 -22
  41. package/src/build_package.ts +96 -20
  42. package/src/business_birth.ts +79 -16
  43. package/src/codegen_dart.ts +267 -37
  44. package/src/codegen_dart_client.ts +930 -87
  45. package/src/codegen_openapi.ts +28 -5
  46. package/src/codegen_proof.ts +107 -11
  47. package/src/codegen_ts.ts +961 -135
  48. package/src/compile_package_main.ts +172 -53
  49. package/src/conflicts.ts +252 -0
  50. package/src/describe_model.ts +448 -0
  51. package/src/directive.ts +5 -38
  52. package/src/drivers.ts +12 -4
  53. package/src/engine_entry.ts +4 -0
  54. package/src/evolve_preview.ts +72 -11
  55. package/src/fields.ts +201 -55
  56. package/src/framework/application_home.ts +103 -0
  57. package/src/framework/application_home_domain.ts +2 -0
  58. package/src/framework/attested_receipts.ts +2 -2
  59. package/src/framework/auth_provider_law.ts +51 -24
  60. package/src/framework/business_corrections.ts +170 -0
  61. package/src/framework/disclosure.ts +1 -1
  62. package/src/framework/domain_lifecycle.ts +250 -16
  63. package/src/framework/encryption_identity.ts +1 -1
  64. package/src/framework/encryption_scope.ts +21 -13
  65. package/src/framework/era_certification.ts +1 -1
  66. package/src/framework/framework_aggregates.ts +2 -0
  67. package/src/framework/genesis.ts +1 -1
  68. package/src/framework/home.ts +22 -9
  69. package/src/framework/identity.ts +5 -5
  70. package/src/framework/impure_capability.ts +2 -2
  71. package/src/framework/owned_custody.ts +561 -0
  72. package/src/framework/platform_admin.ts +1 -1
  73. package/src/framework/relations.ts +9 -1
  74. package/src/framework/repair.ts +1 -1
  75. package/src/framework/signing_identity.ts +2 -2
  76. package/src/framework/source_roster.ts +1 -1
  77. package/src/framework/workspaces.ts +17 -10
  78. package/src/generated_client_names.ts +39 -0
  79. package/src/index.ts +20 -15
  80. package/src/kernel_capabilities.ts +25 -1
  81. package/src/law_check.ts +4 -1
  82. package/src/manifest.ts +356 -63
  83. package/src/read_projection_manifest.ts +8 -0
  84. package/src/read_projection_namespace.ts +4 -0
  85. package/src/relation.ts +1 -1
  86. package/src/shape_compat.ts +61 -5
  87. package/src/stable_ids.ts +38 -6
  88. package/src/stable_ids_types.ts +30 -0
  89. package/src/surfaced_lint.ts +0 -0
  90. package/src/usd.ts +62 -11
  91. package/src/usd_layers.ts +54 -3
  92. package/src/value_object.ts +23 -19
  93. package/src/wire.ts +1 -0
  94. package/src/wire_encode.ts +48 -4
  95. package/src/workspace_routing.ts +57 -0
  96. package/src/workspace_sharding.ts +9 -9
  97. package/src/certified_read.ts +0 -313
package/src/codegen_ts.ts CHANGED
@@ -36,10 +36,18 @@
36
36
  */
37
37
  import type { z } from "zod";
38
38
 
39
- import type { AggregateHandle } from "./aggregate.js";
39
+ import {
40
+ BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
41
+ OWNED_CUSTODY_PAYLOAD_FIELD,
42
+ OWNED_KEY_PAYLOAD_FIELD,
43
+ OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
44
+ type AggregateHandle,
45
+ } from "./aggregate.js";
40
46
  import type { Field, ValueObjectDescriptor, ValueObjectFieldDescriptor } from "./fields.js";
41
47
  import type { Directive } from "./directive.js";
42
48
  import type { DomainModule } from "./codegen_dart.js";
49
+ import { OWNED_DEVICE_APPROVAL_DIRECTIVE, ownedRecordActionDirectiveId } from "./framework/owned_custody.js";
50
+ import { BUSINESS_CORRECTION_DIRECTIVE } from "./framework/business_corrections.js";
43
51
  import { collectActivityDescriptors, type PackageActivityEntry } from "./codegen_dart.js";
44
52
  import type { NomosCompat } from "./compat_manifest.js";
45
53
  import type { QueryDecl } from "./query.js";
@@ -50,13 +58,21 @@ import { finishSum } from "./sum.js";
50
58
  import type { DerivedDecl } from "./derived.js";
51
59
  import type { CombinedDecl } from "./combined.js";
52
60
  import { resolveWorkspaceTypes, type ResolvedWorkspaceType, type WorkspaceTypeDecl } from "./workspace_type.js";
53
- import { deriveClientMintPlan, type ClientMintPlanEntry } from "./workspace_routing.js";
61
+ import {
62
+ deriveClientMintPlan,
63
+ deriveDirectiveRecordTarget,
64
+ type ClientMintPlanEntry,
65
+ } from "./workspace_routing.js";
54
66
  import { admitsNull, nullDocLine } from "./null_meta.js";
67
+ import { isNamespacedAggregate, qualifyReadProjectionType } from "./read_projection_namespace.js";
68
+ import { publicClientInterface, type DomainInterface } from "./shape_compat.js";
69
+ import { buildNomosDescribeModel, type NomosDescribeModel } from "./describe_model.js";
70
+ import { assertModelClientMemberAvailable } from "./generated_client_names.js";
55
71
 
56
72
  const cap = (s: string) => (s.length ? s[0]!.toUpperCase() + s.slice(1) : s);
57
73
  const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
58
74
  const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
59
- const upperSnake = (s: string) => s.replace(/[-\s]+/g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
75
+ const upperSnake = (s: string) => s.replace(/[^A-Za-z0-9]+/g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
60
76
 
61
77
  /** A count/sum group-by field → a valid TS param identifier. A DOTTED group-by (`location.source`,
62
78
  * the #30 follow-up) camel-joins into `locationSource` (a bare `location.source` is not a legal
@@ -113,13 +129,14 @@ export interface EncFieldMap {
113
129
  readonly scope: string;
114
130
  readonly aggSid: string;
115
131
  readonly fieldSid: string;
132
+ readonly owned?: true;
116
133
  }
117
134
  /** Per-domain: which directive payload fields seal, and which read-model fields decrypt. */
118
135
  export interface EncDomainMap {
119
136
  /** directiveId → (payload field → seal coordinates). */
120
137
  readonly directives: Record<string, Record<string, EncFieldMap>>;
121
138
  /** aggregate type id → (field → { scope }) — the fields a read decrypts under `scope`. */
122
- readonly reads: Record<string, Record<string, { scope: string }>>;
139
+ readonly reads: Record<string, Record<string, { scope: string; owned?: true }>>;
123
140
  }
124
141
 
125
142
  /** The sid-keyed read shape of ONE domain's interface (mirrors `shape_compat.ts` DomainInterface). */
@@ -157,13 +174,14 @@ export function domainEncryptionMap(
157
174
  const domain = mod.domain ?? mod.name;
158
175
  const aggs = mod.aggregates as AggregateHandle[];
159
176
  const directives: Record<string, Record<string, EncFieldMap>> = {};
160
- const reads: Record<string, Record<string, { scope: string }>> = {};
177
+ const reads: Record<string, Record<string, { scope: string; owned?: true }>> = {};
161
178
  for (const agg of aggs) {
162
179
  const scope = agg.visibility?.requires;
163
180
  const encFields = agg.encryptedFields;
164
181
  if (encFields === undefined || encFields.length === 0 || scope === undefined) continue;
165
- const r: Record<string, { scope: string }> = {};
166
- for (const f of encFields) r[f] = { scope };
182
+ const r: Record<string, { scope: string; owned?: true }> = {};
183
+ const owned = agg.audience?.kind === "owned";
184
+ for (const f of encFields) r[f] = { scope, ...(owned ? { owned: true as const } : {}) };
167
185
  reads[agg.id] = r;
168
186
  }
169
187
  for (const d of mod.directives as Directive<unknown>[]) {
@@ -185,7 +203,12 @@ export function domainEncryptionMap(
185
203
  `the package structural interface (with stable ids) is required to bake the E2E-sealing map`,
186
204
  );
187
205
  }
188
- fmap[f] = { scope, aggSid: sids.aggSid, fieldSid: sids.fieldSid };
206
+ fmap[f] = {
207
+ scope,
208
+ aggSid: sids.aggSid,
209
+ fieldSid: sids.fieldSid,
210
+ ...(agg.audience?.kind === "owned" ? { owned: true as const } : {}),
211
+ };
189
212
  }
190
213
  if (Object.keys(fmap).length > 0) directives[d.id] = fmap;
191
214
  }
@@ -487,7 +510,7 @@ function tsTypeOfField(field: Field): string {
487
510
  // SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): the FIRST driver that
488
511
  // changes a field's generated type. A `.merge(Surfaced(...))` field reads as the plain scalar
489
512
  // once resolved, or the `{__surfaced:{pending:[...]}}` envelope while genuinely concurrent
490
- // writes await a human pick — typed as the discriminated union `SurfacedValue<T>` (emitted
513
+ // writes await a business merge — typed as the discriminated union `SurfacedValue<T>` (emitted
491
514
  // once per file that needs it). The client does NO transformation; this types the exact
492
515
  // read-collapse JSON.
493
516
  if ((field.driver as { kind?: string } | undefined)?.kind === "Surfaced") {
@@ -691,47 +714,183 @@ export interface TsClientOptions {
691
714
  /** TS string literal (JSON.stringify covers escaping). */
692
715
  const _actLit = (s: string): string => JSON.stringify(s);
693
716
 
694
- /** Emit the ACTIVITY-FEED block (architecture/activity_descriptors.md): the `ActivityEvent`/`ActivityRef`/
717
+ /** Emit the ACTIVITY + SELF-DESCRIPTION block (architecture/activity_descriptors.md): the `ActivityEvent`/`ActivityRef`/
695
718
  * `ActivityRawRow` runtime types, a dotted payload-path getter, the per-directive `renderActivity` registry,
696
719
  * and the shared template renderer — inline in the generated `.client.ts` (mirrors the ForeignRef placement).
697
- * Empty when no directive declares `.activity(...)` — an activity-free package stays BYTE-IDENTICAL. */
698
- function emitTsActivityBlock(entries: readonly PackageActivityEntry[]): string[] {
699
- if (entries.length === 0) return [];
720
+ * An action-free package gets an empty registry but keeps the universal describe() surface. Every public
721
+ * directive receives either its declared prose or a framework-owned accountable baseline. */
722
+ function emitTsActivityBlock(entries: readonly PackageActivityEntry[], describeModel: NomosDescribeModel): string[] {
700
723
  const arm = (e: PackageActivityEntry): string[] => {
701
- const { directiveId, desc } = e;
724
+ const { domain, directiveId, desc } = e;
702
725
  const params = desc.params.map((p) => `${_actLit(p.name)}: _activityPayloadPath(row.payload, ${_actLit(p.from)})`).join(", ");
703
- const refs = desc.refs
726
+ const refs = e.correction === true
727
+ ? `_activityCorrectionRefs(row)`
728
+ : `{ ${desc.refs
704
729
  .map((r) => {
705
730
  // A `from: "@created"` ref reads the id the LAW MINTED (the row's `created` list, first type match);
706
731
  // any other `from` is a payload path. Every ref carries the session workspace (the row's `workspace`).
707
732
  const id = r.from === "@created"
708
- ? `_activityCreatedId(row.created, ${_actLit(r.type)})`
709
- : `_activityPayloadPath(row.payload, ${_actLit(r.from)})`;
733
+ ? `_activityAggregateId(row.created, ${_actLit(r.type)})`
734
+ : r.from === "@affected"
735
+ ? `_activityAggregateId(row.affected, ${_actLit(r.type)})`
736
+ : `_activityPayloadPath(row.payload, ${_actLit(r.from)})`;
710
737
  return `${_actLit(r.name)}: { workspace: row.workspace ?? "", type: ${_actLit(r.type)}, id: ${id} }`;
711
738
  })
712
- .join(", ");
739
+ .join(", ")} }`;
713
740
  return [
714
- ` ${_actLit(directiveId)}: (row: ActivityRawRow): ActivityEvent => {`,
741
+ ` ${_actLit(`${domain.length}:${domain}${directiveId}`)}: (row: _CompleteActivityRawRow): ActivityEvent => {`,
715
742
  ` const params: Record<string, string> = { ${params} };`,
716
743
  ` const vars = { actor: row.actorSubject, ...params };`,
717
744
  ` return {`,
718
- ` eventId: row.intentId, kind: ${_actLit(desc.kind)}, actorSubject: row.actorSubject, occurredAt: row.occurredAt,`,
745
+ ` eventId: ${e.correction === true ? "_activityOpaqueCorrectionId(row.intentId)" : "row.intentId"}, kind: ${_actLit(desc.kind)}, actorSubject: row.actorSubject, occurredAt: row.occurredAt,`,
719
746
  ` title: _renderActivityTemplate(${_actLit(desc.title)}, vars), message: _renderActivityTemplate(${_actLit(desc.message)}, vars),`,
720
747
  ` titleKey: ${_actLit(desc.titleKey)}, messageKey: ${_actLit(desc.messageKey)},`,
721
- ` params, refs: { ${refs} },`,
748
+ ` params, refs: ${refs},`,
722
749
  ...(desc.category !== undefined ? [` category: ${_actLit(desc.category)},`] : []),
723
750
  ...(desc.icon !== undefined ? [` icon: ${_actLit(desc.icon)},`] : []),
724
- ` severity: ${_actLit(desc.severity)}, domain: row.domain, directiveId: ${_actLit(directiveId)},`,
751
+ ` severity: ${_actLit(desc.severity)}, domain: row.domain, directiveId: ${_actLit(e.correction === true ? "correction" : directiveId)},`,
725
752
  ` };`,
726
753
  ` },`,
727
754
  ];
728
755
  };
729
756
  return [
757
+ `// ── model evolution — generated business surface over exact-history proof ──`,
758
+ `export interface NomosModelDifference {`,
759
+ ` kind: "rules" | "outcome" | "evidence"; area?: string; action?: string; summary: string;`,
760
+ `}`,
761
+ `export interface NomosModelReview {`,
762
+ ` checkedActions: number; candidateActions: number; sameOutcome: boolean | null;`,
763
+ ` differences: NomosModelDifference[];`,
764
+ `}`,
765
+ `export type NomosModelAdoption =`,
766
+ ` | { status: "adopted"; summary: string; review: NomosModelReview }`,
767
+ ` | { status: "needs-decision"; summary: string; review: NomosModelReview; accept(because: string): Promise<NomosModelAdoption> }`,
768
+ ` | { status: "blocked"; summary: string; review: NomosModelReview };`,
769
+ `export type NomosModelRestoration =`,
770
+ ` | { status: "restored"; summary: string; review: NomosModelReview }`,
771
+ ` | { status: "needs-decision"; summary: string; review: NomosModelReview; accept(because: string): Promise<NomosModelRestoration> }`,
772
+ ` | { status: "blocked"; summary: string; review: NomosModelReview };`,
773
+ `interface __NomosModelReportWire {`,
774
+ ` sourceVerified: boolean; complete: boolean; promotable: boolean;`,
775
+ ` replayedIntents: number; candidateIntents: number; evolve: { ok: boolean };`,
776
+ ` projection?: { equal: boolean };`,
777
+ ` divergences: { kind: string; domain?: string; directiveId?: string; admitted?: boolean }[];`,
778
+ `}`,
779
+ `interface __NomosModelAdoptionWire {`,
780
+ ` ok: boolean; stage: "promoted" | "identity" | "preflight" | "acceptance" | "admission";`,
781
+ ` report?: __NomosModelReportWire; error?: string;`,
782
+ `}`,
783
+ `interface __NomosModelRestorationWire {`,
784
+ ` ok: boolean; stage: "restored" | "identity" | "preflight" | "acceptance" | "admission";`,
785
+ ` report?: __NomosModelReportWire; error?: string;`,
786
+ `}`,
787
+ `function _nomosModelReview(report?: __NomosModelReportWire): NomosModelReview {`,
788
+ ` const differences: NomosModelDifference[] = [];`,
789
+ ` for (const difference of report?.divergences ?? []) {`,
790
+ ` const area = typeof difference.domain === "string" && !difference.domain.includes("nomos_internal")`,
791
+ ` ? difference.domain : undefined;`,
792
+ ` const action = typeof difference.directiveId === "string" && !difference.directiveId.includes("nomos_internal")`,
793
+ ` ? difference.directiveId : undefined;`,
794
+ ` const kind: NomosModelDifference["kind"] = difference.kind === "plan-divergence" ? "outcome"`,
795
+ ` : difference.kind === "unsupported-premise" || difference.kind === "read-conflict" ? "evidence" : "rules";`,
796
+ ` const subject = action ?? (kind === "outcome" ? "A business action" : "A historical action");`,
797
+ ` const summary = kind === "outcome"`,
798
+ ` ? subject + " would produce a different result under this model."`,
799
+ ` : kind === "evidence"`,
800
+ ` ? "Nomos could not reproduce all evidence needed by " + subject + "."`,
801
+ ` : subject + " is not accepted by this model's rules.";`,
802
+ ` differences.push({ kind, ...(area === undefined ? {} : { area }), ...(action === undefined ? {} : { action }), summary });`,
803
+ ` }`,
804
+ ` if (report !== undefined && !report.evolve.ok && !differences.some((d) => d.kind === "rules")) {`,
805
+ ` differences.push({ kind: "rules", summary: "This model does not yet explain how to interpret every existing value." });`,
806
+ ` }`,
807
+ ` if (report !== undefined && (!report.sourceVerified || !report.complete) && !differences.some((d) => d.kind === "evidence")) {`,
808
+ ` differences.push({ kind: "evidence", summary: "Nomos could not re-prove every premise in the current business history." });`,
809
+ ` }`,
810
+ ` return {`,
811
+ ` checkedActions: report?.replayedIntents ?? 0, candidateActions: report?.candidateIntents ?? 0,`,
812
+ ` sameOutcome: report?.projection?.equal ?? null, differences,`,
813
+ ` };`,
814
+ `}`,
815
+ `async function _nomosAdoptModel(`,
816
+ ` holon: NomosHolon, domain: string, candidateHash: string, because?: string,`,
817
+ `): Promise<NomosModelAdoption> {`,
818
+ ` if (!holon.__nomosAdoptModel) {`,
819
+ ` return { status: "blocked", summary: "This device needs the current Nomos client before it can adopt a model.", review: _nomosModelReview() };`,
820
+ ` }`,
821
+ ` const raw = await holon.__nomosAdoptModel(domain, candidateHash, because === undefined ? {} : { because });`,
822
+ ` const review = _nomosModelReview(raw.report);`,
823
+ ` if (raw.stage === "promoted" && raw.ok) {`,
824
+ ` return { status: "adopted", summary: "Nomos proved this model against current history and made it active.", review };`,
825
+ ` }`,
826
+ ` if (raw.stage === "acceptance") {`,
827
+ ` return {`,
828
+ ` status: "needs-decision",`,
829
+ ` summary: "This model would change prior business outcomes. Review the differences and record why that is intended.",`,
830
+ ` review,`,
831
+ ` accept: async (reason: string) => {`,
832
+ ` const stated = reason.trim();`,
833
+ ` if (!stated) throw new Error("accept(reason) needs the business reason for changing prior outcomes");`,
834
+ ` return _nomosAdoptModel(holon, domain, candidateHash, stated);`,
835
+ ` },`,
836
+ ` };`,
837
+ ` }`,
838
+ ` const summary = raw.stage === "identity"`,
839
+ ` ? "Sign in before adopting this model so the decision is accountable."`,
840
+ ` : raw.report !== undefined && !raw.report.sourceVerified`,
841
+ ` ? "The current history could not be verified. Nothing changed and the current model remains active."`,
842
+ ` : raw.report !== undefined && !raw.report.complete`,
843
+ ` ? "Nomos could not re-prove every historical dependency. Nothing changed and the current model remains active."`,
844
+ ` : raw.report !== undefined && !raw.report.evolve.ok`,
845
+ ` ? "This model cannot yet interpret every existing value. Nothing changed and the current model remains active."`,
846
+ ` : "The workspace changed or refused this adoption. Nothing changed; ask Nomos to try again.";`,
847
+ ` return { status: "blocked", summary, review };`,
848
+ `}`,
849
+ `async function _nomosRestorePreviousModel(`,
850
+ ` holon: NomosHolon, domain: string, currentHash: string, because?: string,`,
851
+ `): Promise<NomosModelRestoration> {`,
852
+ ` if (!holon.__nomosRestorePreviousModel) {`,
853
+ ` return { status: "blocked", summary: "This device needs the current Nomos client before it can restore a model.", review: _nomosModelReview() };`,
854
+ ` }`,
855
+ ` const raw = await holon.__nomosRestorePreviousModel(domain, currentHash, because === undefined ? {} : { because });`,
856
+ ` const review = _nomosModelReview(raw.report);`,
857
+ ` if (raw.stage === "restored" && raw.ok) {`,
858
+ ` return { status: "restored", summary: "Nomos re-proved current history and restored the previous model.", review };`,
859
+ ` }`,
860
+ ` if (raw.stage === "acceptance") {`,
861
+ ` return {`,
862
+ ` status: "needs-decision",`,
863
+ ` summary: "Restoring the previous model changes which rules govern future work. Review the effects and record why restoration is intended.",`,
864
+ ` review,`,
865
+ ` accept: async (reason: string) => {`,
866
+ ` const stated = reason.trim();`,
867
+ ` if (!stated) throw new Error("accept(reason) needs the business reason for restoring the previous model");`,
868
+ ` return _nomosRestorePreviousModel(holon, domain, currentHash, stated);`,
869
+ ` },`,
870
+ ` };`,
871
+ ` }`,
872
+ ` const summary = raw.stage === "identity"`,
873
+ ` ? "Sign in before restoring a model so the decision is accountable."`,
874
+ ` : raw.error?.includes("restore-no-previous")`,
875
+ ` ? "This is the first model for this business; there is no previous model to restore."`,
876
+ ` : raw.error?.includes("restore-current-changed")`,
877
+ ` ? "The current model changed before restoration could finish. Nothing changed; ask Nomos to review again."`,
878
+ ` : raw.report !== undefined && !raw.report.sourceVerified`,
879
+ ` ? "The current history could not be verified. Nothing changed and the current model remains active."`,
880
+ ` : raw.report !== undefined && !raw.report.complete`,
881
+ ` ? "Nomos could not re-prove every historical dependency. Nothing changed and the current model remains active."`,
882
+ ` : raw.report !== undefined && !raw.report.evolve.ok`,
883
+ ` ? "The previous model cannot safely interpret every existing value. Nothing changed and the current model remains active."`,
884
+ ` : "The workspace refused restoration. Nothing changed and the current model remains active.";`,
885
+ ` return { status: "blocked", summary, review };`,
886
+ `}`,
887
+ ``,
730
888
  `// ── activity feed (architecture/activity_descriptors.md) — typed rows folded from the intent log ──`,
731
889
  `/** A declared navigation target on an activity row: the canonical [NomosRef] triple (workspace + type +`,
732
890
  ` * resolved id). A \`from: "@created"\` ref resolves the id the LAW minted; others a payload path. */`,
733
891
  `export interface ActivityRef { workspace: string; type: string; id: string }`,
734
- `/** A rendered activity row — produced by \`renderActivity\` from a directive's declared \`.activity(...)\`. */`,
892
+ `/** A rendered activity row — every public business directive has a framework-owned baseline;`,
893
+ ` * an optional \`.activity(...)\` declaration enriches its language and references. */`,
735
894
  `export interface ActivityEvent {`,
736
895
  ` eventId: string; kind: string; actorSubject: string; occurredAt: string;`,
737
896
  ` title: string; message: string; titleKey: string; messageKey: string;`,
@@ -741,11 +900,56 @@ function emitTsActivityBlock(entries: readonly PackageActivityEntry[]): string[]
741
900
  ` domain: string; directiveId: string;`,
742
901
  `}`,
743
902
  `/** The runtime's per-committed-intent RAW ROW (the shape \`holon.watchActivity\`/\`activityLog\` yield). */`,
744
- `export interface ActivityRawRow { intentId: string; domain: string; directiveId: string; actorSubject: string; occurredAt: string; payload: Record<string, unknown>; created?: { type: string; id: string }[]; workspace?: string }`,
745
- `/** A NARRATIVE over the activity log (for an in-app LLM): the filtered events + a plain-English \`transcript\``,
746
- ` * (ONE LINE PER EVENT, oldest-first, \`"<occurredAt> — <title>: <message>"\`) — grounding text over the`,
747
- ` * VERIFIED log, never raw payloads. Produced by the client's \`describe\` / \`describeHistory\`. */`,
903
+ `export interface ActivityRawRow { intentId: string | null; domain: string | null; directiveId: string | null; actorSubject: string | null; occurredAt: string | null; payload: Record<string, unknown>; created?: { type: string; id: string }[]; affected?: { type: string; id: string }[]; workspace?: string }`,
904
+ `interface _CompleteActivityRawRow extends ActivityRawRow { intentId: string; domain: string; directiveId: string; actorSubject: string; occurredAt: string }`,
905
+ `/** A verified narrative over the activity log. \`describeHistory\` returns this narrow form;`,
906
+ ` * \`describe\` returns the richer linked [NomosDescription] while preserving these two fields. */`,
748
907
  `export interface ActivityNarrative { events: ActivityEvent[]; transcript: string }`,
908
+ `/** The root page plus the six product questions every generated client can answer. */`,
909
+ `export type NomosDescribeSection = "root" | "model" | "current" | "rules" | "history" | "decisions" | "evidence";`,
910
+ `export interface NomosDescribeLink {`,
911
+ ` section: Exclude<NomosDescribeSection, "root">; label: string; question: string; summary: string;`,
912
+ `}`,
913
+ `export interface NomosDescribeItem {`,
914
+ ` kind: string; id: string; label: string; summary: string;`,
915
+ ` details?: Record<string, unknown>; refs?: NomosRef[];`,
916
+ `}`,
917
+ `/** A parent-ledger link to another part of the organisation. Open that workspace's generated client`,
918
+ ` * and call describe() again; each child explains itself in its own installed business language. */`,
919
+ `export interface NomosDescribeChild {`,
920
+ ` workspace: string; status: "active" | "retired" | "reclaimed" | "unknown"; bornAt: string | null;`,
921
+ ` section: "root"; label: string; question: string; summary: string;`,
922
+ `}`,
923
+ `export interface NomosOrganisationDescription { complete: boolean; children: NomosDescribeChild[] }`,
924
+ `/** One grounded page in the hierarchical self-description. Root links to exactly six detail pages. */`,
925
+ `export interface NomosDescription extends ActivityNarrative {`,
926
+ ` section: NomosDescribeSection; title: string; summary: string;`,
927
+ ` items: NomosDescribeItem[]; links: NomosDescribeLink[];`,
928
+ ` organisation: NomosOrganisationDescription;`,
929
+ `}`,
930
+ `/** Compiler-known business language and policy. Runtime facts are joined behind \`describe()\`. */`,
931
+ `const _NOMOS_DESCRIBE_MODEL = ${JSON.stringify(describeModel)} as const;`,
932
+ `type _NomosRefusalReason = "permission" | "business-rule" | "model-change" | "evidence" | "review";`,
933
+ `/** Turn a kernel/runtime refusal into stable business vocabulary. The raw verdict remains in`,
934
+ ` * durable custody; describe() never asks an agent to parse it or leaks it as explanation. */`,
935
+ `function _nomosRefusalReason(error: string): _NomosRefusalReason {`,
936
+ ` const text = error.toLowerCase();`,
937
+ ` const has = (...parts: string[]): boolean => parts.some((part) => text.includes(part));`,
938
+ ` if (has("evolve", "schema", "projection", "retype", "removed field", "law drift", "law-drift", "cross-version", "older model", "authored under")) return "model-change";`,
939
+ ` if (has("attest", "evidence", "source session", "source unreachable", "read unreachable")) return "evidence";`,
940
+ ` if (has("permission", "unauthor", "forbidden", "requires relation", "relation gate", "signer", "principal", "access denied", "not enrolled")) return "permission";`,
941
+ ` if (has("precondition", "invariant", "constraint", "validation", "invalid payload", "business rule", "required field", "must be")) return "business-rule";`,
942
+ ` return "review";`,
943
+ `}`,
944
+ `function _nomosRefusalSummary(reason: _NomosRefusalReason, action: string): string {`,
945
+ ` switch (reason) {`,
946
+ ` case "permission": return action + " was retained because its author did not have the required business permission.";`,
947
+ ` case "business-rule": return action + " was retained because it did not satisfy the business rules declared for that action.";`,
948
+ ` case "model-change": return action + " was created under a model the workspace cannot currently interpret. It remains available while other work continues.";`,
949
+ ` case "evidence": return action + " was retained because required verified business evidence was unavailable.";`,
950
+ ` case "review": return action + " was retained for review and has not changed the business state.";`,
951
+ ` }`,
952
+ `}`,
749
953
  `/** Build the LLM-ready transcript: one line per event, OLDEST-FIRST (the log yields newest-first). */`,
750
954
  `function _activityTranscript(events: readonly ActivityEvent[]): string {`,
751
955
  " return [...events].reverse().map((e) => `${e.occurredAt} — ${e.title}: ${e.message}`).join(\"\\n\");",
@@ -762,23 +966,39 @@ function emitTsActivityBlock(entries: readonly PackageActivityEntry[]): string[]
762
966
  `/** The id the LAW minted for a \`from: "@created"\` ref — the first entry of the row's \`created\` list whose`,
763
967
  ` * type matches the declared aggregate (the runtime type is uuid-stripped + may be namespaced, so match on`,
764
968
  ` * equality OR \`<domain>.<Type>\` suffix). "" when the holon returned no created list (era-safe). */`,
765
- `function _activityCreatedId(created: { type: string; id: string }[] | undefined, declared: string): string {`,
766
- ` if (created === undefined) return "";`,
767
- ` for (const c of created) if (c.type === declared || c.type.endsWith("." + declared)) return c.id;`,
969
+ `function _activityAggregateId(aggregates: { type: string; id: string }[] | undefined, declared: string): string {`,
970
+ ` if (aggregates === undefined) return "";`,
971
+ ` for (const c of aggregates) if (c.type === declared || c.type.endsWith("." + declared)) return c.id;`,
768
972
  ` return "";`,
769
973
  `}`,
974
+ `/** Correction refs are derived by the runtime from the corrected action's sealed events. */`,
975
+ `function _activityCorrectionRefs(row: ActivityRawRow): Record<string, ActivityRef> {`,
976
+ ` return Object.fromEntries((row.affected ?? []).map((ref, index) => [`,
977
+ ` \`subject\${index + 1}\`, { workspace: row.workspace ?? "", type: ref.type, id: ref.id },`,
978
+ ` ]));`,
979
+ `}`,
980
+ `/** Keep a framework-private correction identity stable and traceable without exposing its internal name. */`,
981
+ `function _activityOpaqueCorrectionId(id: string): string {`,
982
+ ` const hash = (seed: number): string => {`,
983
+ ` let h = seed >>> 0;`,
984
+ ` for (let i = 0; i < id.length; i++) h = Math.imul((h ^ id.charCodeAt(i)) >>> 0, 16777619) >>> 0;`,
985
+ ` return h.toString(16).padStart(8, "0");`,
986
+ ` };`,
987
+ ` return \`correction-\${hash(2166136261)}\${hash(2246822507)}\`;`,
988
+ `}`,
770
989
  `/** Interpolate \`{name}\` placeholders in a template from vars (a missing var renders empty). */`,
771
990
  `function _renderActivityTemplate(template: string, vars: Record<string, string>): string {`,
772
991
  ` return template.replace(/\\{([a-zA-Z0-9_]+)\\}/g, (_m, name: string) => vars[name] ?? "");`,
773
992
  `}`,
774
- `const _ACTIVITY_REGISTRY: Record<string, (row: ActivityRawRow) => ActivityEvent> = {`,
993
+ `const _ACTIVITY_REGISTRY: Record<string, (row: _CompleteActivityRawRow) => ActivityEvent> = {`,
775
994
  ...entries.flatMap(arm),
776
995
  `};`,
777
- `/** Fold ONE committed-intent raw row into a typed [ActivityEvent], or null when the directive declares`,
778
- ` * no \`.activity(...)\`. Pure — the kernel/ledger stays the audit truth; this is a client-side projection. */`,
996
+ `/** Fold ONE committed-intent raw row into a typed [ActivityEvent], or null for framework/control work`,
997
+ ` * outside this generated business package. Pure — the kernel/ledger stays the audit truth. */`,
779
998
  `export function renderActivity(row: ActivityRawRow): ActivityEvent | null {`,
780
- ` const f = _ACTIVITY_REGISTRY[row.directiveId];`,
781
- ` return f === undefined ? null : f(row);`,
999
+ ` if (typeof row.intentId !== "string" || typeof row.domain !== "string" || typeof row.directiveId !== "string" || typeof row.actorSubject !== "string" || typeof row.occurredAt !== "string") return null;`,
1000
+ ` const f = _ACTIVITY_REGISTRY[\`${"${row.domain.length}:${row.domain}${row.directiveId}"}\`];`,
1001
+ ` return f === undefined ? null : f(row as _CompleteActivityRawRow);`,
782
1002
  `}`,
783
1003
  ``,
784
1004
  ];
@@ -800,6 +1020,9 @@ function anyPackedTaxonomy(modules: readonly DomainModule[]): boolean {
800
1020
  }
801
1021
 
802
1022
  export function generateTsClient(modules: readonly DomainModule[], opts: TsClientOptions): string {
1023
+ // Collect the one framework-authored correction testimony before presentation filtering removes its
1024
+ // private directive. Ordinary internal machinery stays absent from the registry.
1025
+ const packageActivityEntries = collectActivityDescriptors(modules);
803
1026
  // Business-birth framework declarations remain in the certified module, but are not application
804
1027
  // vocabulary. Strip only the presentation marker here; manifests, hashes and the engine see the full law.
805
1028
  modules = modules.map((mod) => {
@@ -821,15 +1044,38 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
821
1044
  : { readModelAggregates: mod.readModelAggregates.filter((id) => publicAggregateIds.has(id)) }),
822
1045
  };
823
1046
  });
1047
+ for (const mod of modules) assertModelClientMemberAvailable(mod);
1048
+ const clientInterface = opts.interface === undefined
1049
+ ? undefined
1050
+ : publicClientInterface(modules, opts.interface as unknown as Record<string, DomainInterface>);
824
1051
  const hashConst = tsHashConstName(opts.packageName);
825
1052
  // THE E2E-SEALING MAP (the sealing client): non-null iff any field is `.encrypted()`. When null,
826
1053
  // NOTHING below emits — the generated client is byte-identical to a pre-sealing compile.
827
1054
  const encMap = buildEncryptionMap(
828
1055
  modules,
829
- opts.interface as Record<string, { aggregates?: Record<string, InterfaceAggShape> }> | undefined,
1056
+ clientInterface as Record<string, { aggregates?: Record<string, InterfaceAggShape> }> | undefined,
830
1057
  );
831
1058
  const encPresent = encMap !== null;
1059
+ const ownedPresent = encMap !== null && Object.values(encMap).some((domain) =>
1060
+ [...Object.values(domain.directives), ...Object.values(domain.reads)].some((fields) =>
1061
+ Object.values(fields).some((field) => field.owned === true),
1062
+ ),
1063
+ );
1064
+ const manualEncryptionPresent = encMap !== null && Object.values(encMap).some((domain) =>
1065
+ [...Object.values(domain.directives), ...Object.values(domain.reads)].some((fields) =>
1066
+ Object.values(fields).some((field) => field.owned !== true),
1067
+ ),
1068
+ );
832
1069
  const encConst = encryptionConstName(opts.packageName);
1070
+ const internalFieldsByType = Object.fromEntries(
1071
+ [...modules.flatMap((mod) => mod.aggregates)]
1072
+ .map((aggregate) => [
1073
+ aggregate.id,
1074
+ [...((aggregate as { internalFields?: readonly string[] }).internalFields ?? [])].sort(),
1075
+ ] as const)
1076
+ .filter(([, fields]) => fields.length > 0),
1077
+ );
1078
+ const hasInternalFields = Object.keys(internalFieldsByType).length > 0;
833
1079
  // A package declaring ≥1 `.births()` directive emits the branded identity wrappers (below) its typed
834
1080
  // birth methods take for `actor`/`authorSecret`. Births-free packages emit nothing (byte-identical).
835
1081
  const hasBirths = modules.some((m) =>
@@ -847,10 +1093,22 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
847
1093
  ),
848
1094
  ),
849
1095
  );
850
- // ACTIVITY FEED (architecture/activity_descriptors.md): the package's declared `.activity(...)` descriptors.
851
- // Empty ⇒ NOTHING activity-related emits (byte-identical to a pre-activity compile).
852
- const activityEntries = collectActivityDescriptors(modules);
853
- const activityPresent = activityEntries.length > 0;
1096
+ const hasConflictResolvers = modules.some(
1097
+ (m) => ((m as { surfacedResolvers?: readonly unknown[] }).surfacedResolvers?.length ?? 0) > 0,
1098
+ );
1099
+ // ACTIVITY + SELF-DESCRIPTION: public actions populate the registry; an action-free package keeps an empty
1100
+ // registry because the other five questions still have answers.
1101
+ const activityEntries = packageActivityEntries;
1102
+ // Self-description is universal. A read-only/empty-history package still has a
1103
+ // model, current state, rules and evidence; its activity registry is simply empty.
1104
+ const activityPresent = true;
1105
+ const hasCorrections = modules.some((m) =>
1106
+ (m.directives as Directive<unknown>[]).some((d) =>
1107
+ (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true
1108
+ && (d as { birthsChild?: unknown }).birthsChild !== true
1109
+ && (d as { businessBirth?: unknown }).businessBirth === undefined,
1110
+ ),
1111
+ );
854
1112
  const out: string[] = [];
855
1113
  // The PACKAGE-LEVEL recursive-type registry: every directive payload's `z.lazy` knots mint
856
1114
  // named recursive type aliases into it (deduped by lazy identity across directives); the decls
@@ -869,13 +1127,13 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
869
1127
  `/** sha256 of the deployed \`.package.usda\` — the installed law this client offers under. */`,
870
1128
  `export const ${hashConst} = ${JSON.stringify(opts.domainHash)};`,
871
1129
  ``,
872
- ...(opts.interface !== undefined
1130
+ ...(clientInterface !== undefined
873
1131
  ? [
874
1132
  `/** This client's STRUCTURAL INTERFACE REQUIREMENT (#72): the directives (payload shapes)`,
875
1133
  ` * and aggregates (read shapes, sid-keyed) it depends on, keyed by domain. \`connect()\``,
876
1134
  ` * negotiates this against the workspace's installed offer by SHAPE (stable-id subtyping),`,
877
1135
  ` * not by law hash — so the domain can evolve without republishing this client. */`,
878
- `export const ${interfaceConstName(opts.packageName)} = ${JSON.stringify(opts.interface)} as const;`,
1136
+ `export const ${interfaceConstName(opts.packageName)} = ${JSON.stringify(clientInterface)} as const;`,
879
1137
  ``,
880
1138
  ]
881
1139
  : []),
@@ -912,10 +1170,28 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
912
1170
  ` * (composed at decode from the session workspace + the row's type/id — additive, the \`data\` is untouched). */`,
913
1171
  `export interface Row<T> { type: string; id: string; data: T; ref: NomosRef }`,
914
1172
  `/** Stamp each row's canonical [NomosRef] from the session \`ws\` + the row's type/id (additive — already-`,
915
- ` * stamped rows pass through). */`,
916
- `function _stampRefs<T>(ws: string, rows: Row<T>[]): Row<T>[] {`,
917
- ` return rows.map((r) => (r.ref !== undefined ? r : { ...r, ref: { workspace: ws, type: r.type, id: r.id } }));`,
918
- `}`,
1173
+ ` * stamped rows pass through).${hasInternalFields ? " Framework-owned fields are removed at this decode boundary." : ""} */`,
1174
+ ...(hasInternalFields
1175
+ ? [
1176
+ `const _INTERNAL_FIELDS: Record<string, readonly string[]> = ${JSON.stringify(internalFieldsByType)};`,
1177
+ `function _stampRefs<T>(ws: string, rows: Row<T>[]): Row<T>[] {`,
1178
+ ` return rows.map((r) => {`,
1179
+ ` const fields = _INTERNAL_FIELDS[r.type] ?? _INTERNAL_FIELDS[r.type.slice(r.type.lastIndexOf(".") + 1)];`,
1180
+ ` let row = r;`,
1181
+ ` if (fields !== undefined && fields.length > 0 && r.data !== null && typeof r.data === "object") {`,
1182
+ ` const data = { ...(r.data as Record<string, unknown>) };`,
1183
+ ` for (const field of fields) delete data[field];`,
1184
+ ` row = { ...r, data: data as T };`,
1185
+ ` }`,
1186
+ ` return row.ref !== undefined ? row : { ...row, ref: { workspace: ws, type: row.type, id: row.id } };`,
1187
+ ` });`,
1188
+ `}`,
1189
+ ]
1190
+ : [
1191
+ `function _stampRefs<T>(ws: string, rows: Row<T>[]): Row<T>[] {`,
1192
+ ` return rows.map((r) => (r.ref !== undefined ? r : { ...r, ref: { workspace: ws, type: r.type, id: r.id } }));`,
1193
+ `}`,
1194
+ ]),
919
1195
  `function _stampRefsP<T>(ws: string, p: Promise<Row<T>[]>): Promise<Row<T>[]> { return p.then((rows) => _stampRefs(ws, rows)); }`,
920
1196
  `/** THE TYPED WRITE OUTCOME (architecture/nomos_ref.md) — every generated directive method returns this.`,
921
1197
  ` * \`created\` is EVERY aggregate the plan minted (typed [NomosRef]s, composed with the session workspace;`,
@@ -1010,7 +1286,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1010
1286
  ``,
1011
1287
  // SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): the read-collapse union for a
1012
1288
  // `.merge(Surfaced(...))` field — the plain scalar once resolved, or the pending envelope while
1013
- // genuinely concurrent writes await a human pick. Emitted ONLY when the package declares one.
1289
+ // genuinely concurrent writes await a business merge. Emitted ONLY when the package declares one.
1014
1290
  ...(hasSurfaced
1015
1291
  ? [
1016
1292
  `/** One competing observation on a PENDING Surfaced field. \`author\` is the writer principal —`,
@@ -1018,7 +1294,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1018
1294
  ` * JSON key is pinned in ONE place: this property name (kernel b215430d — exactly "author"). */`,
1019
1295
  `export interface SurfacedObservation<T> {`,
1020
1296
  ` value: T;`,
1021
- ` occurredAt: { physical: number; logical: number; replica: number };`,
1297
+ ` occurredAt: { physical: number; logical: number; replica: string };`,
1022
1298
  ` author?: string | null;`,
1023
1299
  `}`,
1024
1300
  `/** The PENDING read-collapse of a \`.merge(Surfaced(...))\` field: the kernel retained BOTH sides`,
@@ -1026,15 +1302,21 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1026
1302
  ` * to the plain scalar; bounded, cap 8) instead of silently picking a winner. Resolve`,
1027
1303
  ` * it with an ORDINARY next write (a \`.mutates()\` directive that set()s the field) — the field`,
1028
1304
  ` * then reads as the plain scalar again, indistinguishable from any other field. */`,
1029
- `export interface SurfacedPending<T> { __surfaced: { pending: SurfacedObservation<T>[] } }`,
1030
- `/** A Surfaced field's read value: the resolved scalar, or the pending envelope under conflict. */`,
1031
- `export type SurfacedValue<T> = T | SurfacedPending<T>;`,
1305
+ `export interface SurfacedPending<T> { __surfaced: { status: "pending"; pending: SurfacedObservation<T>[] } }`,
1306
+ `/** A retained value authored under a different field contract. Nomos withholds the value`,
1307
+ ` * until a declared deterministic evolution can produce the current generated type. */`,
1308
+ `export interface SurfacedEvolutionRequired { __surfaced: { status: "evolutionRequired"; targetSchema: string; evolution: { sourceSchemas: string[]; authoredUnder: string[] } } }`,
1309
+ `/** A Surfaced field's read value: resolved, pending, or awaiting schema evolution. */`,
1310
+ `export type SurfacedValue<T> = T | SurfacedPending<T> | SurfacedEvolutionRequired;`,
1032
1311
  `/** True iff \`v\` is the pending-conflict envelope (narrows to [SurfacedPending]). */`,
1033
1312
  `export function isSurfacedPending<T>(v: SurfacedValue<T> | null | undefined): v is SurfacedPending<T> {`,
1034
- ` return typeof v === "object" && v !== null && "__surfaced" in (v as Record<string, unknown>);`,
1313
+ ` return typeof v === "object" && v !== null && (v as { __surfaced?: { status?: unknown } }).__surfaced?.status === "pending";`,
1314
+ `}`,
1315
+ `export function isSurfacedEvolutionRequired<T>(v: SurfacedValue<T> | null | undefined): v is SurfacedEvolutionRequired {`,
1316
+ ` return typeof v === "object" && v !== null && (v as { __surfaced?: { status?: unknown } }).__surfaced?.status === "evolutionRequired";`,
1035
1317
  `}`,
1036
- `/** ONE row of the workspace-wide \`pendingConflicts()\` read (reserved id \`nomosPendingConflicts\`):`,
1037
- ` * a (aggregate, field) whose folded state awaits a human pick. MAINTAINED like counts/sums`,
1318
+ `/** ONE row of Nomos's maintained conflict index (reserved id \`nomosPendingConflicts\`):`,
1319
+ ` * a (aggregate, field) whose folded state awaits a business merge. MAINTAINED like counts/sums`,
1038
1320
  ` * (fold-time, O(pending) to read) and DERIVED, NOT DRAINED — a resolution write folding in removes`,
1039
1321
  ` * the row by derivation. The workspace-wide surface is HETEROGENEOUS, so each observation's`,
1040
1322
  ` * \`value\` is \`unknown\` here — the per-field typed seam stays on the read models (\`SurfacedValue<T>\`). */`,
@@ -1042,17 +1324,61 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1042
1324
  ` aggregateType: string;`,
1043
1325
  ` aggregateId: string;`,
1044
1326
  ` field: string;`,
1045
- ` observations: SurfacedObservation<unknown>[];`,
1327
+ ` status: "ready" | "evolutionRequired";`,
1328
+ ` targetSchema: string;`,
1329
+ ` observations?: SurfacedObservation<unknown>[];`,
1330
+ ` evolution?: { sourceSchemas: string[]; authoredUnder: string[]; reason: string };`,
1331
+ ` /** Opaque Nomos identity of exactly this observation set. */`,
1332
+ ` snapshot: string;`,
1333
+ `}`,
1334
+ `/** A counts-only view over the maintained pending-conflict index. Aggregate keys are generated`,
1335
+ ` * from this law; absent keys have zero pending merges. */`,
1336
+ `export interface ConflictSummary {`,
1337
+ ` total: number;`,
1338
+ ` ready: number;`,
1339
+ ` evolutionRequired: number;`,
1340
+ ` byAggregate: Readonly<Record<string, number>>;`,
1046
1341
  `}`,
1047
1342
  ``,
1048
1343
  ]
1049
1344
  : []),
1345
+ `/** One opaque, verified point in this workspace's history. Applications pass it back to`,
1346
+ ` * \`client.past.at(moment)\`; its framework identity is never parsed as business data. */`,
1347
+ `export interface NomosMoment {`,
1348
+ ` readonly id: string;`,
1349
+ ` readonly occurredAt: string | null;`,
1350
+ ` readonly actorSubject: string | null;`,
1351
+ ` readonly domain: string | null;`,
1352
+ ` readonly action: string | null;`,
1353
+ `}`,
1354
+ ...(hasCorrections
1355
+ ? [
1356
+ `/** Result of a reasoned business correction. Framework target and testimony ids remain private. */`,
1357
+ `export interface NomosCorrectionOutcome {`,
1358
+ ` readonly ok: boolean; readonly kind: "undo" | "redo";`,
1359
+ ` readonly reason?: string; readonly message?: string; readonly head?: string;`,
1360
+ `}`,
1361
+ ]
1362
+ : []),
1363
+ `/** Read-only primitive used behind the generated, business-shaped historic view. */`,
1364
+ `export interface NomosHistoricHolon {`,
1365
+ ` query(queryId: string, params?: Record<string, unknown>, keys?: unknown): Promise<unknown>;`,
1366
+ ` queryById(aggregateId: string, keys?: unknown): Promise<unknown>;`,
1367
+ ` count(countId: string, groupKey?: string): Promise<number>;`,
1368
+ ` sum(sumId: string, groupKey?: string): Promise<number>;`,
1369
+ `}`,
1050
1370
  `/** The holon surface this client binds to (structurally satisfied by @githolon/client). */`,
1051
1371
  `export interface NomosHolon {`,
1052
1372
  ` /** THE ONE ENCODER offer (the single v2-envelope codec lives in @githolon/client \`encodeIntentOfferBytes\`):`,
1053
1373
  ` * hand the holon a PLANNED directive + the verified \`actor\`; the holon encodes + offers. The generated`,
1054
1374
  ` * client calls THIS, never building offer bytes itself — so there is exactly ONE copy of the wire format. */`,
1055
1375
  ` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean; attestations?: readonly { queryId: string; result: unknown; attestation: unknown }[] }): Promise<string | { head: string; created?: { type: string; id: string }[]; workspace?: string; born?: unknown; birthOutcomes?: unknown }>;`,
1376
+ ...(hasCorrections
1377
+ ? [
1378
+ ` /** Framework-private seam used by generated \`corrections\`; apps never supply an intent id. */`,
1379
+ ` correctMostRecent?(opts: { kind: "undo" | "redo"; domain: string; actions: string[]; reason: string; domainHash?: string; actor?: string }): Promise<NomosCorrectionOutcome>;`,
1380
+ ]
1381
+ : []),
1056
1382
  ` /** Opaque-bytes offer — retained for carriers that already hold an encoded envelope (back-compat). */`,
1057
1383
  ` offer(intentBytes: string | Uint8Array): Promise<string>;`,
1058
1384
  ...(hasBirths
@@ -1067,10 +1393,28 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1067
1393
  ` workspace: string;`,
1068
1394
  ` /** The identity already proved by this bound session (explicit principal or token subject). */`,
1069
1395
  ` principal?: string;`,
1396
+ ...(ownedPresent
1397
+ ? [
1398
+ ` /** FRAMEWORK-INTERNAL: reserve the typed record identity before record-bound owned sealing. */`,
1399
+ ` mint?(typeTag: string): Promise<string>;`,
1400
+ ]
1401
+ : []),
1402
+ ` /** FRAMEWORK-INTERNAL verified child graph. Generated describe() exposes only business-safe links. */`,
1403
+ ` __nomosChildWorkspaces?(): Promise<readonly { workspace: string; status: "active" | "retired" | "reclaimed" | "unknown"; bornAt: string | null }[]>;`,
1404
+ ` /** FRAMEWORK-INTERNAL exact-history adoption seam. Generated clients expose only`,
1405
+ ` * \`model.adopt()\` and a chained \`decision.accept(reason)\`. */`,
1406
+ ` __nomosAdoptModel?(domain: string, candidateHash: string, opts?: { because?: string }): Promise<__NomosModelAdoptionWire>;`,
1407
+ ` /** FRAMEWORK-INTERNAL exact-history restoration seam. Generated clients expose only`,
1408
+ ` * \`model.restorePrevious()\` and a chained \`decision.accept(reason)\`. */`,
1409
+ ` __nomosRestorePreviousModel?(domain: string, currentHash: string, opts?: { because?: string }): Promise<__NomosModelRestorationWire>;`,
1070
1410
  ` /** Persist local custody and reconcile its configured remote. Business birth awaits this when available. */`,
1071
1411
  ` sync?(): Promise<unknown>;`,
1072
1412
  ` /** Complete durable custody and cold-chain verification for a newly born aggregate. */`,
1073
1413
  ` settleBirth?(workspace: string, opts?: { timeoutMs?: number; pollMs?: number }): Promise<unknown>;`,
1414
+ ` /** Verified points in local history. Optional only so a pre-history runtime fails at the named call. */`,
1415
+ ` commits?(opts?: { limit?: number }): Promise<{ oid: string; at: number | null; actorSubject: string | null; domain: string | null; directiveId: string | null }[]>;`,
1416
+ ` /** Read the verified projection at one opaque moment without moving the live world. */`,
1417
+ ` at?(momentId: string): NomosHistoricHolon;`,
1074
1418
  ...(encPresent
1075
1419
  ? [
1076
1420
  ` /** \`keys\` (E2E sealing): the reader's held scope keys, injected so encrypted fields decrypt;`,
@@ -1081,6 +1425,26 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1081
1425
  ` * (bound to the scope + stable sids). OPTIONAL: structural binding tolerates a pre-sealing holon`,
1082
1426
  ` * (the write path then throws with an upgrade hint before any plaintext dispatch). */`,
1083
1427
  ` cryptoSealField?(args: { scopeKey: string; scope: string; epoch: number; aggSid: string; fieldSid: string; plaintext: string }): Promise<string>;`,
1428
+ ` /** FRAMEWORK-INTERNAL: derive an owned aggregate's current record key from this session's private custody. */`,
1429
+ ` __nomosOwnedScopeKey?(args: { scope: string; record?: { type: string; id: string } }): Promise<NomosScopeKey>;`,
1430
+ ` /** FRAMEWORK-INTERNAL: every held key epoch for one record, including shared and rotated history. */`,
1431
+ ` __nomosOwnedScopeKeys?(args: { scope: string; record: { type: string; id: string } }): Promise<NomosScopeKey[]>;`,
1432
+ ` /** FRAMEWORK-INTERNAL: make this principal ready to receive generated record shares. */`,
1433
+ ` __nomosEnsureOwnedIdentity?(args: { scope: string; domain: string; domainHash?: string }): Promise<void>;`,
1434
+ ` /** FRAMEWORK-INTERNAL: atomically record the opt-in recovery policy and escrow wrap. */`,
1435
+ ` __nomosProtectOwnedIdentity?(args: { scope: string; domain: string; domainHash?: string; recoveryPrincipal: string }): Promise<{ status: "protected"; recoveryPrincipal: string; unchanged?: boolean }>;`,
1436
+ ` /** FRAMEWORK-INTERNAL: prepare the atomic law payload behind share/unshare/transfer. */`,
1437
+ ` __nomosPrepareOwnedRecordAction?(args: { action: "share" | "unshare" | "transfer"; scope: string; record: NomosRef; peer: string }): Promise<Record<string, unknown>>;`,
1438
+ ` /** FRAMEWORK-INTERNAL: derive the deterministic unborn child's owned key before atomic genesis. */`,
1439
+ ` __nomosOwnedBirthScopeKey?(args: { scope: string; namespace: string; key: unknown }): Promise<NomosScopeKey & { workspace: string }>;`,
1440
+ ...(ownedPresent
1441
+ ? [
1442
+ ` /** FRAMEWORK-INTERNAL: make the opaque request for this session's not-yet-approved device. */`,
1443
+ ` __nomosOwnedDeviceRequest?(args: { scope: string }): Promise<string>;`,
1444
+ ` /** FRAMEWORK-INTERNAL: validate a request and prepare the one atomic approval payload. */`,
1445
+ ` __nomosPrepareOwnedDeviceApproval?(args: { request: string; scope: string }): Promise<Record<string, unknown>>;`,
1446
+ ]
1447
+ : []),
1084
1448
  ` /** Unwrap an on-chain WrappedKey with this device's secret → the base64 scope key (default KeyStore). */`,
1085
1449
  ` cryptoUnwrapKey?(args: { secret: string; hpkeEpk: string; ct: string }): Promise<string>;`,
1086
1450
  ` /** OWNER CEREMONY (slice 2): HPKE-wrap a scope epoch secret TO a member's published X25519 pubkey. */`,
@@ -1092,12 +1456,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1092
1456
  ` query(queryId: string, params?: Record<string, unknown>): Promise<unknown>;`,
1093
1457
  ` queryById(aggregateId: string): Promise<unknown>;`,
1094
1458
  ]),
1459
+ ` /** Watch a declared query in the local verified projection; returns stop(). Sync is runtime-owned. */`,
1460
+ ` watch(queryId: string, params: Record<string, unknown> | undefined, cb: (rows: unknown) => void, opts?: { intervalMs?: number }): () => void;`,
1095
1461
  ` watchById(aggregateId: string, cb: (rows: unknown) => void, opts?: { intervalMs?: number }): () => void;`,
1096
1462
  ...(hasSurfaced
1097
1463
  ? [
1098
- ` /** The workspace-wide pending Surfaced-conflict read (reserved id \`nomosPendingConflicts\`).`,
1464
+ ` /** A framework-filtered pending Surfaced-conflict read (reserved id \`nomosPendingConflicts\`).`,
1099
1465
  ` * OPTIONAL member: structural binding tolerates a pre-Surfaced holon (the accessor throws at call). */`,
1100
- ` pendingConflicts?(): Promise<PendingConflictRow[]>;`,
1466
+ ` pendingConflicts?(aggregateType?: string): Promise<PendingConflictRow[]>;`,
1467
+ ` /** Counts-only workspace view; competing business values do not cross this call. */`,
1468
+ ` pendingConflictSummary?(): Promise<ConflictSummary>;`,
1101
1469
  ]
1102
1470
  : []),
1103
1471
  ` /** Declared-count / declared-sum O(1) reads (maintained by the read engine). */`,
@@ -1122,7 +1490,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1122
1490
  ` /** THE ACTIVITY FEED (declared \`.activity(...)\`): subscribe to per-committed-intent RAW ROWS`,
1123
1491
  ` * (the runtime folds the intent log). \`cb\` fires with the current rows + on every new intent;`,
1124
1492
  ` * returns an unsubscribe fn. The generated client maps these through \`renderActivity\`. */`,
1125
- ` watchActivity(cb: (rows: ActivityRawRow[]) => void, opts?: { base?: string }): () => void;`,
1493
+ ` watchActivity(cb: (rows: ActivityRawRow[]) => void, opts?: { base?: string; intervalMs?: number }): () => void;`,
1126
1494
  ` /** One-shot: the current committed activity log as RAW ROWS (folded to typed rows by the client). */`,
1127
1495
  ` activityLog(opts?: { base?: string; limit?: number }): Promise<ActivityRawRow[]>;`,
1128
1496
  ]
@@ -1143,6 +1511,14 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1143
1511
  ` on?(event: "lawChanged", cb: (compat: NomosCompatVerdict) => void): () => void;`,
1144
1512
  `}`,
1145
1513
  ``,
1514
+ ...(hasConflictResolvers
1515
+ ? [
1516
+ `/** Framework-private merge lane used only by generated conflict capabilities. The snapshot`,
1517
+ ` * proves the exact observation set being merged; application code never supplies it. */`,
1518
+ `type __NomosConflictOffer = (domain: string, directiveId: string, payload: unknown, opts: { domainHash?: string; actor?: string; returnCreated: true; __nomosConflictSnapshots: readonly { aggregateType: string; aggregateId: string; field: string; targetSchema: string; token: string }[] }) => ReturnType<NomosHolon["offerDirective"]>;`,
1519
+ ``,
1520
+ ]
1521
+ : []),
1146
1522
  `/** THE LIVE COMPAT VERDICT (#72b) stashed on \`holon.compat\`. \`verdict\` is the three-state roll-up;`,
1147
1523
  ` * \`members\` names each directive/aggregate's own \`{ ok, reason? }\`; \`install\` is the deployed law's`,
1148
1524
  ` * lifecycle phase. \`has(id)\` = the feature flag; \`refresh()\` re-evaluates against the projection. */`,
@@ -1202,12 +1578,21 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1202
1578
  `export const ${encConst} = ${JSON.stringify(encMap)} as const;`,
1203
1579
  ``,
1204
1580
  `/** A held scope key: the base64 symmetric key + the epoch it belongs to (forward-only rotation). */`,
1205
- `export interface NomosScopeKey { scope: string; epoch: number; key: string }`,
1581
+ `${manualEncryptionPresent ? "export " : ""}interface NomosScopeKey { scope: string; epoch: number; key: string; ownedKeySubject?: string; ownedCustody?: string }`,
1582
+ ...(ownedPresent
1583
+ ? [
1584
+ `/** An opaque request copied from a new device to an already-working one. */`,
1585
+ `export type NomosDeviceRequest = string & { readonly __nomosDeviceRequest: unique symbol };`,
1586
+ ]
1587
+ : []),
1588
+ `const _OWNED_KEY_PAYLOAD_FIELD = ${JSON.stringify(OWNED_KEY_PAYLOAD_FIELD)};`,
1589
+ `const _OWNED_CUSTODY_PAYLOAD_FIELD = ${JSON.stringify(OWNED_CUSTODY_PAYLOAD_FIELD)};`,
1590
+ `const _OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD = ${JSON.stringify(OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD)};`,
1206
1591
  ``,
1207
1592
  `/** The app-supplied KEY CUSTODY surface. Supply one to \`<domain>Client(holon, { keyStore })\` and the`,
1208
1593
  ` * generated methods seal writes + decrypt reads automatically. \`wrappedKeyStore\` is the default over`,
1209
1594
  ` * the on-chain WrappedKeys; an app may implement this to source keys from a secure enclave. */`,
1210
- `export interface NomosKeyStore {`,
1595
+ `${manualEncryptionPresent ? "export " : ""}interface NomosKeyStore {`,
1211
1596
  ` /** The CURRENT (highest-epoch) scope key, for SEALING a write. undefined ⇒ EncryptionKeyUnavailable. */`,
1212
1597
  ` scopeKey(scope: string): Promise<NomosScopeKey | undefined>;`,
1213
1598
  ` /** EVERY held key for these scopes (all epochs), injected on reads so old ciphertext also decrypts. */`,
@@ -1216,7 +1601,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1216
1601
  ``,
1217
1602
  `/** Thrown when a write must seal an encrypted field but the KeyStore holds no key for its scope — the`,
1218
1603
  ` * write NEVER falls through to a plaintext dispatch (the sid-envelope gate would refuse it anyway). */`,
1219
- `export class EncryptionKeyUnavailable extends Error {`,
1604
+ `${manualEncryptionPresent ? "export " : ""}class EncryptionKeyUnavailable extends Error {`,
1220
1605
  ` readonly code = "nomos.crypto.key_unavailable";`,
1221
1606
  ` readonly recommendedAction = "obtainScopeKey";`,
1222
1607
  ` constructor(readonly scope: string, readonly field: string) {`,
@@ -1230,20 +1615,38 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1230
1615
  `const _sealFields = async (`,
1231
1616
  ` holon: NomosHolon,`,
1232
1617
  ` keyStore: NomosKeyStore | undefined,`,
1233
- ` fieldMap: Record<string, { scope: string; aggSid: string; fieldSid: string }>,`,
1618
+ ` fieldMap: Record<string, { scope: string; aggSid: string; fieldSid: string; owned?: true }>,`,
1234
1619
  ` payload: Record<string, unknown>,`,
1235
1620
  ` seal: boolean,`,
1621
+ ` ownedKeyOverride?: NomosScopeKey,`,
1622
+ ` ownedRecord?: { type: string; id: string; preMinted?: true },`,
1236
1623
  `): Promise<Record<string, unknown>> => {`,
1237
- ` if (seal === false || keyStore == null) return payload;`,
1624
+ ` if (seal === false) {`,
1625
+ ` if (Object.values(fieldMap).some((m) => m.owned === true)) throw new Error(".owned() encryption cannot be disabled");`,
1626
+ ` return payload;`,
1627
+ ` }`,
1238
1628
  ` let out: Record<string, unknown> | undefined;`,
1629
+ ` let ownedKey: NomosScopeKey | undefined = ownedKeyOverride;`,
1630
+ ` const owned = Object.values(fieldMap).find((m) => m.owned === true);`,
1631
+ ` if (owned) {`,
1632
+ ` if (!ownedKey) {`,
1633
+ ` if (!holon.__nomosOwnedScopeKey) throw new Error("this holon predates automatic .owned() key custody — upgrade @githolon/client");`,
1634
+ ` if (!ownedRecord || !ownedRecord.type || !ownedRecord.id) throw new Error("Nomos could not derive the owned record identity before sealing; recompile the client and law together");`,
1635
+ ` ownedKey = await holon.__nomosOwnedScopeKey({ scope: owned.scope, record: ownedRecord });`,
1636
+ ` }`,
1637
+ ` if (!ownedKey.ownedKeySubject) throw new Error("Nomos did not return the framework-owned device identity for .owned() custody");`,
1638
+ ` (out ??= { ...payload })[_OWNED_KEY_PAYLOAD_FIELD] = ownedKey.ownedKeySubject;`,
1639
+ ` if (ownedKey.ownedCustody) (out ??= { ...payload })[_OWNED_CUSTODY_PAYLOAD_FIELD] = ownedKey.ownedCustody;`,
1640
+ ` if (ownedRecord?.preMinted === true) (out ??= { ...payload })[_OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD] = [{ type: ownedRecord.type, id: ownedRecord.id }];`,
1641
+ ` }`,
1239
1642
  ` for (const field of Object.keys(fieldMap)) {`,
1240
1643
  ` const v = payload[field];`,
1241
1644
  ` if (v === undefined || v === null) continue;`,
1242
1645
  ` const m = fieldMap[field]!;`,
1243
- ` const sk = await keyStore.scopeKey(m.scope);`,
1646
+ ` const sk = m.owned === true ? ownedKey : (keyStore != null ? await keyStore.scopeKey(m.scope) : undefined);`,
1244
1647
  ` if (!sk) throw new EncryptionKeyUnavailable(m.scope, field);`,
1245
1648
  ` if (!holon.cryptoSealField) throw new Error("this holon predates E2E sealing — upgrade @githolon/client");`,
1246
- ` const envelope = await holon.cryptoSealField({ scopeKey: sk.key, scope: m.scope, epoch: sk.epoch, aggSid: m.aggSid, fieldSid: m.fieldSid, plaintext: String(v) });`,
1649
+ ` const envelope = await holon.cryptoSealField({ scopeKey: sk.key, scope: m.owned === true ? sk.scope : m.scope, epoch: sk.epoch, aggSid: m.aggSid, fieldSid: m.fieldSid, plaintext: String(v) });`,
1247
1650
  ` (out ??= { ...payload })[field] = envelope;`,
1248
1651
  ` }`,
1249
1652
  ` return out ?? payload;`,
@@ -1251,12 +1654,64 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1251
1654
  ``,
1252
1655
  `/** The read keys to inject for the given scopes, or undefined when there is nothing to inject`,
1253
1656
  ` * (no keyStore, or no held keys) — the read then behaves exactly as today (opaque envelopes). */`,
1254
- `const _readKeys = async (keyStore: NomosKeyStore | undefined, scopes: readonly string[]): Promise<NomosScopeKey[] | undefined> => {`,
1255
- ` if (keyStore == null || scopes.length === 0) return undefined;`,
1256
- ` const keys = await keyStore.readKeys(scopes);`,
1657
+ `const _readKeys = async (holon: NomosHolon, keyStore: NomosKeyStore | undefined, scopes: readonly { scope: string; owned?: true }[], records: readonly { type: string; id: string }[] = []): Promise<NomosScopeKey[] | undefined> => {`,
1658
+ ` if (scopes.length === 0) return undefined;`,
1659
+ ` const ordinary = [...new Set(scopes.filter((s) => s.owned !== true).map((s) => s.scope))];`,
1660
+ ` const keys = keyStore == null || ordinary.length === 0 ? [] : await keyStore.readKeys(ordinary);`,
1661
+ ` for (const s of scopes) {`,
1662
+ ` if (s.owned !== true) continue;`,
1663
+ ` if (!holon.__nomosOwnedScopeKeys) throw new Error("owned record reads need the current @githolon/client");`,
1664
+ ` for (const record of records) {`,
1665
+ ` for (const held of await holon.__nomosOwnedScopeKeys({ scope: s.scope, record })) {`,
1666
+ ` if (!keys.some((key) => key.scope === held.scope && key.epoch === held.epoch)) keys.push(held);`,
1667
+ ` }`,
1668
+ ` }`,
1669
+ ` }`,
1257
1670
  ` return keys.length ? keys : undefined;`,
1258
1671
  `};`,
1259
1672
  ``,
1673
+ `/** Owned multi-row reads stay business-shaped: first fetch the authorized ciphertext rows locally,`,
1674
+ ` * derive only those record keys, then repeat the same local indexed read with decryption injected. */`,
1675
+ `const _readWithOwnedKeys = async <T extends { type: string; id: string }>(`,
1676
+ ` holon: NomosHolon,`,
1677
+ ` keyStore: NomosKeyStore | undefined,`,
1678
+ ` scopes: readonly { scope: string; owned?: true }[],`,
1679
+ ` read: (keys: readonly NomosScopeKey[] | undefined) => Promise<T[]>,`,
1680
+ `): Promise<T[]> => {`,
1681
+ ` const firstKeys = await _readKeys(holon, keyStore, scopes);`,
1682
+ ` const first = await read(firstKeys);`,
1683
+ ` if (!scopes.some((scope) => scope.owned === true) || first.length === 0) return first;`,
1684
+ ` const keys = await _readKeys(holon, keyStore, scopes, first.map(({ type, id }) => ({ type, id })));`,
1685
+ ` return read(keys);`,
1686
+ `};`,
1687
+ ``,
1688
+ `/** Reactive owned reads use the exact same two-pass, record-key-aware read as one-shot reads.`,
1689
+ ` * The raw watch is only a local change signal; ciphertext rows never reach application callbacks. */`,
1690
+ `const _watchWithOwnedKeys = <T extends { type: string; id: string }>(`,
1691
+ ` subscribe: (changed: () => void) => () => void,`,
1692
+ ` read: () => Promise<T[]>,`,
1693
+ ` cb: (rows: T[]) => void,`,
1694
+ `): (() => void) => {`,
1695
+ ` let active = true;`,
1696
+ ` let pending = Promise.resolve();`,
1697
+ ` const changed = () => {`,
1698
+ ` pending = pending.catch(() => undefined).then(async () => {`,
1699
+ ` try {`,
1700
+ ` const rows = await read();`,
1701
+ ` if (active) cb(rows);`,
1702
+ ` } catch (error) {`,
1703
+ ` if (!active) return;`,
1704
+ ` const message = String((error as { message?: unknown })?.message ?? error).toLowerCase();`,
1705
+ ` if (/forbid|denied|not been shared|not currently sharing|no longer shared/.test(message)) { cb([]); return; }`,
1706
+ ` throw error;`,
1707
+ ` }`,
1708
+ ` });`,
1709
+ ` };`,
1710
+ ` const stop = subscribe(changed);`,
1711
+ ` return () => { active = false; stop(); };`,
1712
+ `};`,
1713
+ ``,
1714
+ ...(manualEncryptionPresent ? [
1260
1715
  `/** THE DEFAULT KeyStore — unwraps this principal's on-chain WrappedKeys with its device secret, caching`,
1261
1716
  ` * per scope. Mirror of the documented reader handshake (e2e_encryption.md): the secret stays client-side,`,
1262
1717
  ` * the operator stays blind. Key custody (where \`deviceSecret\` lives) remains the app's concern. */`,
@@ -1447,6 +1902,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1447
1902
  ` };`,
1448
1903
  `}`,
1449
1904
  ``,
1905
+ ] : []),
1450
1906
  );
1451
1907
  }
1452
1908
 
@@ -1566,8 +2022,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1566
2022
  }
1567
2023
  for (const [typeId, agg] of [...aggByType].sort(([a], [b]) => a.localeCompare(b))) {
1568
2024
  const attributed = (agg as { attributedFields?: readonly string[] }).attributedFields ?? [];
2025
+ const internal = new Set((agg as { internalFields?: readonly string[] }).internalFields ?? []);
1569
2026
  out.push(`export interface ${typeId}Data {`);
1570
2027
  for (const [name, field] of Object.entries(agg.fields)) {
2028
+ if (internal.has(name)) continue;
1571
2029
  const required = (field as Field).isFromBirth === true;
1572
2030
  const doc = nullDocLine(field);
1573
2031
  if (doc !== undefined) out.push(` /** ${doc} */`);
@@ -1833,8 +2291,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1833
2291
  }
1834
2292
  }
1835
2293
 
1836
- // ── activity feed (declared `.activity(...)`) — types + `renderActivity` registry (empty ⇒ nothing) ──
1837
- out.push(...emitTsActivityBlock(activityEntries));
2294
+ // ── accountable activity feed (automatic baseline; `.activity(...)` enriches) ──
2295
+ if (activityPresent) {
2296
+ out.push(...emitTsActivityBlock(activityEntries, buildNomosDescribeModel(modules, opts.packageName)));
2297
+ }
1838
2298
 
1839
2299
  // ── payload types + per-domain client factories ──
1840
2300
  const usedPayloadNames = new Set<string>();
@@ -1844,6 +2304,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1844
2304
  (d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object"
1845
2305
  && (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
1846
2306
  );
2307
+ const correctionActions = dirs.filter((d) =>
2308
+ (d as { birthsChild?: unknown }).birthsChild !== true
2309
+ && (d as { businessBirth?: unknown }).businessBirth === undefined,
2310
+ );
1847
2311
  out.push(`// ── domain "${domain}" ──`, ``);
1848
2312
 
1849
2313
  const mintPlan = mintPlans.get(mod);
@@ -1859,12 +2323,19 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1859
2323
  } catch { /* the compile gate already failed a bad taxonomy loudly */ }
1860
2324
  }
1861
2325
  const methods: string[] = [];
2326
+ const pastMethods: string[] = [];
2327
+ const runtimeTypeOf = (aggregate: AggregateHandle): string => isNamespacedAggregate(aggregate)
2328
+ ? qualifyReadProjectionType(domain, aggregate.id)
2329
+ : aggregate.id;
1862
2330
  // E2E READ INJECTION: the distinct encryption scopes a read of `typeId` must inject keys for (empty
1863
2331
  // ⇒ nothing encrypted on that type ⇒ the accessor is emitted plain, unchanged). The `_readKeys` helper
1864
2332
  // returns undefined when no keyStore is bound, so an injected accessor still behaves as today offline.
1865
- const readScopesFor = (typeId: string): string[] => {
2333
+ const readScopesFor = (typeId: string): { scope: string; owned?: true }[] => {
1866
2334
  const r = encMap?.[domain]?.reads?.[typeId];
1867
- return r === undefined ? [] : [...new Set(Object.values(r).map((x) => x.scope))].sort();
2335
+ if (r === undefined) return [];
2336
+ const byKey = new Map<string, { scope: string; owned?: true }>();
2337
+ for (const x of Object.values(r)) byKey.set(`${x.scope}:${x.owned === true}`, x);
2338
+ return [...byKey.values()].sort((a, b) => a.scope.localeCompare(b.scope));
1868
2339
  };
1869
2340
  // The DISTINCT encryption scopes this domain defines (across encrypted reads + sealing directives) —
1870
2341
  // the set the owner ceremony rotates. Empty ⇒ no `encryption` surface is emitted for this domain.
@@ -1872,11 +2343,59 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1872
2343
  const dm = encMap?.[domain];
1873
2344
  if (dm === undefined) return [];
1874
2345
  const s = new Set<string>();
1875
- for (const r of Object.values(dm.reads)) for (const f of Object.values(r)) s.add(f.scope);
1876
- for (const dir of Object.values(dm.directives)) for (const f of Object.values(dir)) s.add(f.scope);
2346
+ for (const r of Object.values(dm.reads)) for (const f of Object.values(r)) if (f.owned !== true) s.add(f.scope);
2347
+ for (const dir of Object.values(dm.directives)) for (const f of Object.values(dir)) if (f.owned !== true) s.add(f.scope);
1877
2348
  return [...s].sort();
1878
2349
  })();
1879
- // directiveId → its emitted payload TYPE name (the surfaced `resolve…` helpers reference it).
2350
+ const ownedScopes: string[] = (() => {
2351
+ const dm = encMap?.[domain];
2352
+ if (dm === undefined) return [];
2353
+ const scopes = new Set<string>();
2354
+ for (const r of Object.values(dm.reads)) for (const f of Object.values(r)) if (f.owned === true) scopes.add(f.scope);
2355
+ for (const dir of Object.values(dm.directives)) for (const f of Object.values(dir)) if (f.owned === true) scopes.add(f.scope);
2356
+ return [...scopes].sort();
2357
+ })();
2358
+ if (ownedScopes.length > 1) {
2359
+ throw new Error(`codegen: owned business state in domain '${domain}' unexpectedly uses more than one framework scope`);
2360
+ }
2361
+ const ownedScope = ownedScopes[0];
2362
+ const ownedActions = Object.fromEntries(
2363
+ (mod.aggregates as AggregateHandle[])
2364
+ .filter((aggregate) => aggregate.audience?.kind === "owned")
2365
+ .map((aggregate) => [runtimeTypeOf(aggregate), {
2366
+ share: ownedRecordActionDirectiveId("share", aggregate.id),
2367
+ unshare: ownedRecordActionDirectiveId("unshare", aggregate.id),
2368
+ transfer: ownedRecordActionDirectiveId("transfer", aggregate.id),
2369
+ }]),
2370
+ );
2371
+ if (ownedScope !== undefined && Object.keys(ownedActions).length > 0) {
2372
+ methods.push(
2373
+ ` /** Share one record. The peer can read its history and future changes; Nomos owns the keys and policy. */`,
2374
+ ` share: (record: NomosRef, peer: string): Promise<NomosOutcome> => _ownedRecordAction("share", record, peer),`,
2375
+ ` /** Stop sharing one record. Earlier knowledge cannot be erased; future changes use a fresh private key. */`,
2376
+ ` unshare: (record: NomosRef, peer: string): Promise<NomosOutcome> => _ownedRecordAction("unshare", record, peer),`,
2377
+ ` /** Hand ownership, readable history and future authority for one record to another person. */`,
2378
+ ` transfer: (record: NomosRef, peer: string): Promise<NomosOutcome> => _ownedRecordAction("transfer", record, peer),`,
2379
+ );
2380
+ }
2381
+ if (correctionActions.length > 0) {
2382
+ methods.push(
2383
+ ` /** Correct this principal's latest business change. Nomos owns target selection, testimony and replay. */`,
2384
+ ` corrections: {`,
2385
+ ` undo: async (reason: string): Promise<NomosCorrectionOutcome> => {`,
2386
+ ` if (!reason.trim()) throw new Error("corrections.undo() needs a reason");`,
2387
+ ` if (!holon.correctMostRecent) throw new Error("corrections need the current @githolon/client");`,
2388
+ ` return holon.correctMostRecent({ kind: "undo", domain: ${JSON.stringify(domain)}, actions: ${JSON.stringify(correctionActions.map((d) => d.id))}, reason, domainHash, actor: actor ?? holon.principal });`,
2389
+ ` },`,
2390
+ ` redo: async (reason: string): Promise<NomosCorrectionOutcome> => {`,
2391
+ ` if (!reason.trim()) throw new Error("corrections.redo() needs a reason");`,
2392
+ ` if (!holon.correctMostRecent) throw new Error("corrections need the current @githolon/client");`,
2393
+ ` return holon.correctMostRecent({ kind: "redo", domain: ${JSON.stringify(domain)}, actions: ${JSON.stringify(correctionActions.map((d) => d.id))}, reason, domainHash, actor: actor ?? holon.principal });`,
2394
+ ` },`,
2395
+ ` },`,
2396
+ );
2397
+ }
2398
+ // directiveId → its emitted payload TYPE name (the generated conflict references use it).
1880
2399
  const dirPayloadNames = new Map<string, string>();
1881
2400
  for (const d of dirs) {
1882
2401
  let payloadName = `${cap(d.id)}Payload`;
@@ -1884,7 +2403,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1884
2403
  usedPayloadNames.add(payloadName);
1885
2404
  dirPayloadNames.set(d.id, payloadName);
1886
2405
  const businessBirth = (d as unknown as {
1887
- businessBirth?: { aggregateId: string; publicPayloadSchema: z.ZodTypeAny; rootId: string; namespace: string };
2406
+ businessBirth?: { aggregateId: string; publicPayloadSchema: z.ZodTypeAny; rootId: string; namespace: string; keyField: string };
1888
2407
  }).businessBirth;
1889
2408
  const schema = businessBirth?.publicPayloadSchema
1890
2409
  ?? (d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema;
@@ -1902,7 +2421,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1902
2421
  // (via the KeyStore) BEFORE dispatch. `wantSeal` forces an async block body + a `callOpts.seal`
1903
2422
  // escape hatch, and `pv` (the sealed copy) replaces `payload` downstream. Non-encrypted directives
1904
2423
  // are byte-identical (wantSeal === false ⇒ no prelude, no extra param, plain `payload`).
1905
- const wantSeal = encMap?.[domain]?.directives?.[d.id] !== undefined;
2424
+ const encFields = encMap?.[domain]?.directives?.[d.id];
2425
+ const wantSeal = encFields !== undefined;
1906
2426
  // FALLBACK ATTESTATION INJECTION (co2): a directive whose law declares a foreign read
1907
2427
  // (`.readsFrom(...)`) accepts pre-fetched signed envelopes as `callOpts.attestations` — the manual
1908
2428
  // escape hatch when the colocation resolver can't serve the read (the source isn't on the plane). The
@@ -1911,8 +2431,37 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1911
2431
  // directives (an ordinary directive's signature is byte-identical).
1912
2432
  const wantAttest = (((d as { declaredForeignReads?: unknown[] }).declaredForeignReads?.length ?? 0) > 0);
1913
2433
  const attTy = `readonly { queryId: string; result: unknown; attestation: unknown }[]`;
2434
+ const ownedSeal = encFields !== undefined && Object.values(encFields).every((field) => field.owned === true);
2435
+ const ownedTargetAggregate = ownedSeal
2436
+ ? (mod.aggregates as AggregateHandle[]).find((aggregate) => aggregate.id === (d as { aggregateId?: string }).aggregateId)
2437
+ : undefined;
2438
+ const ownedRecordTarget = ownedTargetAggregate === undefined
2439
+ ? undefined
2440
+ : deriveDirectiveRecordTarget(d, ownedTargetAggregate);
2441
+ if (ownedSeal && businessBirth === undefined && ownedRecordTarget === undefined) {
2442
+ throw new Error(
2443
+ `codegen: cannot derive the record targeted by encrypted owned directive '${domain}/${d.id}'. ` +
2444
+ `Address the record from one string payload field, one deterministic instance, or create it with create(Aggregate).`,
2445
+ );
2446
+ }
2447
+ const ownedRuntimeType = ownedTargetAggregate === undefined ? undefined : runtimeTypeOf(ownedTargetAggregate);
2448
+ const ownedRecordPrelude = businessBirth !== undefined || ownedRecordTarget === undefined || ownedRuntimeType === undefined
2449
+ ? ""
2450
+ : ownedRecordTarget.kind === "premint"
2451
+ ? ` if (!holon.mint) throw new Error("this holon predates record-bound .owned() identity — upgrade @githolon/client");\n` +
2452
+ ` const _ownedRecord = { type: ${JSON.stringify(ownedRuntimeType)}, id: await holon.mint(${JSON.stringify(ownedRuntimeType)}), preMinted: true as const };`
2453
+ : ownedRecordTarget.kind === "payload"
2454
+ ? ` const _ownedRecord = { type: ${JSON.stringify(ownedRuntimeType)}, id: String((payload as Record<string, unknown>)[${JSON.stringify(ownedRecordTarget.field)}] ?? "") };`
2455
+ : ` const _ownedRecord = { type: ${JSON.stringify(ownedRuntimeType)}, id: ${JSON.stringify(ownedRecordTarget.id)} };`;
2456
+ const ownedBirthDescriptor = businessBirth === undefined || encFields === undefined
2457
+ ? undefined
2458
+ : Object.values(encFields).find((field) => field.owned === true);
2459
+ const ownedBirthPrelude = ownedBirthDescriptor === undefined
2460
+ ? ""
2461
+ : ` if (!holon.__nomosOwnedBirthScopeKey) throw new Error("this holon predates automatic .owned() birth custody — upgrade @githolon/client");\n` +
2462
+ ` const _ownedBirthKey = await holon.__nomosOwnedBirthScopeKey({ scope: ${JSON.stringify(ownedBirthDescriptor.scope)}, namespace: ${JSON.stringify(businessBirth!.namespace)}, key: (payload as Record<string, unknown>)[${JSON.stringify(businessBirth!.keyField)}] });`;
1914
2463
  const callOptsFields = [
1915
- ...(wantSeal ? ["seal?: boolean"] : []),
2464
+ ...(wantSeal && !ownedSeal ? ["seal?: boolean"] : []),
1916
2465
  ...(wantAttest ? [`attestations?: ${attTy}`] : []),
1917
2466
  ];
1918
2467
  const paramSig = callOptsFields.length
@@ -1920,7 +2469,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1920
2469
  : `payload: ${payloadName}`;
1921
2470
  const offerExtra = wantAttest ? ", attestations: callOpts?.attestations" : "";
1922
2471
  const sealPrelude = wantSeal
1923
- ? ` const _sealed = await _sealFields(holon, keyStore, ${encConst}[${JSON.stringify(domain)}].directives[${JSON.stringify(d.id)}] as Record<string, { scope: string; aggSid: string; fieldSid: string }>, payload as Record<string, unknown>, callOpts?.seal !== false) as ${payloadName};`
2472
+ ? ` const _sealed = await _sealFields(holon, keyStore, ${encConst}[${JSON.stringify(domain)}].directives[${JSON.stringify(d.id)}] as Record<string, { scope: string; aggSid: string; fieldSid: string; owned?: true }>, payload as Record<string, unknown>, ${businessBirth !== undefined || ownedSeal ? "true" : "callOpts?.seal !== false"}${ownedBirthDescriptor !== undefined ? ", _ownedBirthKey" : ", undefined"}${ownedRecordPrelude ? ", _ownedRecord" : ""}) as ${payloadName};`
1924
2473
  : "";
1925
2474
  const pv = wantSeal ? "_sealed" : "payload";
1926
2475
  // MULTI-OUTPUT: name every aggregate this directive writes (primary marker target + declared
@@ -1944,9 +2493,14 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1944
2493
  ` birth: async (payload: ${payloadName}): Promise<NomosBorn<${businessBirth.aggregateId}Data>> => {`,
1945
2494
  ` const principal = actor ?? holon.principal;`,
1946
2495
  ` if (!principal || !principal.trim()) throw new Error("birth() needs a signed-in Nomos session");`,
1947
- ` const result = await holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, { ...payload, principal, bornAt: new Date().toISOString() }, { domainHash, actor: principal, returnCreated: true });`,
2496
+ ...(ownedBirthPrelude ? [ownedBirthPrelude] : []),
2497
+ ...(wantSeal ? [sealPrelude] : []),
2498
+ ` const result = await holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, { ...payload, principal, bornAt: new Date().toISOString()${wantSeal ? `, ${JSON.stringify(BUSINESS_BIRTH_SEED_PAYLOAD_FIELD)}: JSON.stringify(_sealed)` : ""}${ownedBirthDescriptor !== undefined ? `, [_OWNED_KEY_PAYLOAD_FIELD]: _ownedBirthKey.ownedKeySubject` : ""} }, { domainHash, actor: principal, returnCreated: true });`,
1948
2499
  ` const born = typeof result === "object" && result !== null && Array.isArray(result.born) ? result.born[0] : undefined;`,
1949
2500
  ` if (typeof born !== "object" || born === null || typeof born.workspace !== "string" || !born.workspace) throw new Error("birth() committed without returning the born workspace");`,
2501
+ ...(ownedBirthDescriptor !== undefined
2502
+ ? [` if (born.workspace !== _ownedBirthKey.workspace) throw new Error("birth() returned a workspace different from the deterministic owned-custody address");`]
2503
+ : []),
1950
2504
  ` await holon.sync?.();`,
1951
2505
  ` if (!holon.settleBirth) throw new Error("birth() requires the current @githolon/client to complete durable recovery");`,
1952
2506
  ` await holon.settleBirth(born.workspace);`,
@@ -1989,13 +2543,20 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1989
2543
  const body = stamps.length
1990
2544
  ? `stampNow({ ...${pv}, ${mint.field} }, ${JSON.stringify(stamps)})`
1991
2545
  : `{ ...${pv}, ${mint.field} }`;
2546
+ const ownedMintTarget = ownedSeal
2547
+ && ownedRecordTarget?.kind === "payload"
2548
+ && ownedRecordTarget.field === mint.field
2549
+ && ownedRuntimeType !== undefined;
1992
2550
  methods.push(
1993
2551
  ` /** Author \`${domain}/${d.id}\`: the front-door MINTS \`${mint.field}\` (route-tagged) when omitted`,
1994
2552
  ` * — ids stay internal plumbing — and returns the outcome (\`target\` = the minted ${dd.aggregateId ?? "aggregate"}).${gate ? ` GATED: needs \`{ actor }\` (\`${gate.relation}\`).` : ""} */`,
1995
2553
  ` ${d.id}: async (${paramSig}): Promise<NomosOutcome> => {`,
1996
2554
  ...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
1997
- ...(sealPrelude ? [sealPrelude] : []),
1998
2555
  ` const ${mint.field} = payload.${mint.field} ?? (await (holon as NomosRoutedHolon).mint(${JSON.stringify(mint.mintType)}${mintOpts}));`,
2556
+ ...(ownedMintTarget
2557
+ ? [` const _ownedRecord = { type: ${JSON.stringify(ownedRuntimeType)}, id: String(${mint.field}) };`]
2558
+ : ownedRecordPrelude ? [ownedRecordPrelude] : []),
2559
+ ...(sealPrelude ? [sealPrelude] : []),
1999
2560
  ` return _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash, actor, returnCreated: true${offerExtra} }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>, { workspace: holon.workspace, type: ${JSON.stringify(dd.aggregateId ?? mint.mintType)}, id: ${mint.field} });`,
2000
2561
  ` },`,
2001
2562
  );
@@ -2006,12 +2567,15 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2006
2567
  d.marker === "creates"
2007
2568
  ? `Author \`${domain}/${d.id}\`: the LAW mints the ${d.aggregateId ?? "aggregate"} id inside the sealed intent; it rides back on \`outcome.created\` — no id in the payload, no query-after.`
2008
2569
  : `Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOutcome] (\`head\` + any \`created\` refs).`;
2009
- const sealDoc = wantSeal ? " Encrypted field(s) are SEALED via the bound { keyStore } before dispatch (pass { seal: false } to skip)." : "";
2570
+ const sealDoc = ownedSeal
2571
+ ? " Owned content is encrypted automatically by Nomos."
2572
+ : wantSeal ? " Encrypted field(s) are SEALED via the bound { keyStore } before dispatch (pass { seal: false } to skip)." : "";
2010
2573
  const gateDoc = gate ? ` GATED: needs \`{ actor }\` (\`${gate.relation}\`) — throws MissingOfferAuthority otherwise.` : "";
2011
2574
  methods.push(
2012
2575
  ` /** ${outcomeDoc}${sealDoc}${gateDoc} */`,
2013
2576
  ` ${d.id}: async (${paramSig}): Promise<NomosOutcome> => {`,
2014
2577
  ...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
2578
+ ...(ownedRecordPrelude ? [ownedRecordPrelude] : []),
2015
2579
  ...(sealPrelude ? [sealPrelude] : []),
2016
2580
  ` return _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor, returnCreated: true${offerExtra} }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>);`,
2017
2581
  ` },`,
@@ -2035,11 +2599,27 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2035
2599
  })
2036
2600
  .join("; ");
2037
2601
  const qScopes = readScopesFor(q.returns);
2602
+ const qOwned = qScopes.some((scope) => scope.owned === true);
2603
+ const qScopeJson = JSON.stringify(qScopes);
2038
2604
  methods.push(
2039
2605
  ` /** Declared query \`${q.id}\` (indexed probe — routed by the read manifest).${qScopes.length ? " Encrypted field(s) decrypt when the bound { keyStore } holds the scope key; else they stay opaque envelopes." : ""} */`,
2040
2606
  qScopes.length
2041
- ? ` ${lcFirst(camel(q.id))}: async (params: { ${params} }) => ${wrapP(`holon.query(${JSON.stringify(q.id)}, params, await _readKeys(keyStore, ${JSON.stringify(qScopes)})) as Promise<Row<${q.returns}Data>[]>`)},`
2607
+ ? qOwned
2608
+ ? ` ${lcFirst(camel(q.id))}: async (params: { ${params} }) => ${wrapP(`_readWithOwnedKeys(holon, keyStore, ${qScopeJson}, (keys) => holon.query(${JSON.stringify(q.id)}, params, keys) as Promise<Row<${q.returns}Data>[]>)`)},`
2609
+ : ` ${lcFirst(camel(q.id))}: async (params: { ${params} }) => ${wrapP(`holon.query(${JSON.stringify(q.id)}, params, await _readKeys(holon, keyStore, ${qScopeJson})) as Promise<Row<${q.returns}Data>[]>`)},`
2042
2610
  : ` ${lcFirst(camel(q.id))}: (params: { ${params} }) => ${wrapP(`holon.query(${JSON.stringify(q.id)}, params) as Promise<Row<${q.returns}Data>[]>`)},`,
2611
+ ` /** Reactive \`${q.id}\` — re-emits typed rows on every local change; returns stop(). */`,
2612
+ qOwned
2613
+ ? ` watch${cap(camel(q.id))}: (params: { ${params} }, cb: (rows: Row<${q.returns}Data>[]) => void, opts?: { intervalMs?: number }) => _watchWithOwnedKeys((changed) => holon.watch(${JSON.stringify(q.id)}, params, changed as (rows: unknown) => void, opts), () => ${wrapP(`_readWithOwnedKeys(holon, keyStore, ${qScopeJson}, (keys) => holon.query(${JSON.stringify(q.id)}, params, keys) as Promise<Row<${q.returns}Data>[]>)`)}, cb),`
2614
+ : ` watch${cap(camel(q.id))}: (params: { ${params} }, cb: (rows: Row<${q.returns}Data>[]) => void, opts?: { intervalMs?: number }) => holon.watch(${JSON.stringify(q.id)}, params, ((rows: Row<${q.returns}Data>[]) => cb(${wrapRows("rows")})) as (rows: unknown) => void, opts),`,
2615
+ );
2616
+ pastMethods.push(
2617
+ ` /** Declared query \`${q.id}\` in the verified world at this moment. */`,
2618
+ qScopes.length
2619
+ ? qOwned
2620
+ ? ` ${lcFirst(camel(q.id))}: async (params: { ${params} }) => ${wrapP(`_readWithOwnedKeys(holon, keyStore, ${qScopeJson}, (keys) => historic.query(${JSON.stringify(q.id)}, params, keys) as Promise<Row<${q.returns}Data>[]>)`)},`
2621
+ : ` ${lcFirst(camel(q.id))}: async (params: { ${params} }) => ${wrapP(`historic.query(${JSON.stringify(q.id)}, params, await _readKeys(holon, keyStore, ${qScopeJson})) as Promise<Row<${q.returns}Data>[]>`)},`
2622
+ : ` ${lcFirst(camel(q.id))}: (params: { ${params} }) => ${wrapP(`historic.query(${JSON.stringify(q.id)}, params) as Promise<Row<${q.returns}Data>[]>`)},`,
2043
2623
  );
2044
2624
  // THE LOGICAL PAGED ROW SURFACE (sharding §5.3, ruling 2 — slice 4): emitted
2045
2625
  // ONLY for packed-taxonomy domains (taxonomy-free clients stay byte-identical).
@@ -2064,6 +2644,12 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2064
2644
  ? ` ${lcFirst(camel(c.id))}: (${p}: string) => holon.count(${JSON.stringify(c.id)}, ${p}),`
2065
2645
  : ` ${lcFirst(camel(c.id))}: () => holon.count(${JSON.stringify(c.id)}),`,
2066
2646
  );
2647
+ pastMethods.push(
2648
+ ` /** Declared count \`${c.id}\` at this moment. */`,
2649
+ grouped
2650
+ ? ` ${lcFirst(camel(c.id))}: (${p}: string) => historic.count(${JSON.stringify(c.id)}, ${p}),`
2651
+ : ` ${lcFirst(camel(c.id))}: () => historic.count(${JSON.stringify(c.id)}),`,
2652
+ );
2067
2653
  }
2068
2654
  for (const s of (mod.sums ?? []).map((raw) => finishSum(raw))) {
2069
2655
  const grouped = s.by != null;
@@ -2074,89 +2660,277 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2074
2660
  ? ` ${lcFirst(camel(s.id))}: (${p}: string) => holon.sum(${JSON.stringify(s.id)}, ${p}),`
2075
2661
  : ` ${lcFirst(camel(s.id))}: () => holon.sum(${JSON.stringify(s.id)}),`,
2076
2662
  );
2663
+ pastMethods.push(
2664
+ ` /** Declared sum \`${s.id}\` at this moment. */`,
2665
+ grouped
2666
+ ? ` ${lcFirst(camel(s.id))}: (${p}: string) => historic.sum(${JSON.stringify(s.id)}, ${p}),`
2667
+ : ` ${lcFirst(camel(s.id))}: () => historic.sum(${JSON.stringify(s.id)}),`,
2668
+ );
2077
2669
  }
2078
2670
 
2079
- // ── SURFACED CONFLICT DX (surfaced_merge_conflict.md): the workspace-wide pendingConflicts
2080
- // accessor + one typed `resolve…` helper per designated `.resolves()` directive. Emitted iff
2081
- // this domain declares a Surfaced field (byte-identical otherwise — the era rule).
2671
+ // ── BUSINESS MERGE CONFLICT DX: Nomos keeps its heterogeneous workspace index private. The
2672
+ // generated surface exposes a summary, then aggregate-specific pending conflict references.
2673
+ // Each reference owns the one lawful `merge(...)` action for its field — Git semantics: inspect
2674
+ // the competing versions, produce a merged result (which may be either side or a third value),
2675
+ // and commit it as a causally-later write.
2082
2676
  const resolverBindings = (mod as { surfacedResolvers?: import("./surfaced_lint.js").SurfacedResolverBinding[] }).surfacedResolvers ?? [];
2083
2677
  if (resolverBindings.length > 0) {
2678
+ const conflictAggregateType = (aggregateId: string): string => {
2679
+ const aggregate = mod.aggregates.find((candidate) => candidate.id === aggregateId);
2680
+ if (aggregate === undefined) {
2681
+ throw new Error(`generated conflict policy refers to unknown aggregate '${aggregateId}' in domain '${domain}'`);
2682
+ }
2683
+ return isNamespacedAggregate(aggregate)
2684
+ ? qualifyReadProjectionType(domain, aggregateId)
2685
+ : aggregateId;
2686
+ };
2687
+ const byAggregate = new Map<string, typeof resolverBindings>();
2688
+ for (const binding of resolverBindings) {
2689
+ byAggregate.set(binding.aggregateId, [...(byAggregate.get(binding.aggregateId) ?? []), binding]);
2690
+ }
2084
2691
  methods.push(
2085
- ` /** PENDING SURFACED CONFLICTS across the workspace — the maintained "decisions needed" read`,
2086
- ` * (reserved id \`nomosPendingConflicts\`; derived, not drained: a resolution write removes its`,
2087
- ` * row by derivation). Pair each row with its typed \`resolve…\` helper below. */`,
2088
- ` pendingConflicts: () => {`,
2089
- ` if (!holon.pendingConflicts) throw new Error("this holon predates pendingConflicts reads — upgrade @githolon/client");`,
2090
- ` return holon.pendingConflicts();`,
2091
- ` },`,
2092
- ` /** Watch the pending Surfaced conflicts (local reactive read; returns stop()). */`,
2093
- ` watchPendingConflicts: (cb: (rows: PendingConflictRow[]) => void, opts?: { intervalMs?: number }) => {`,
2094
- ` if (!holon.pendingConflicts) throw new Error("this holon predates pendingConflicts reads — upgrade @githolon/client");`,
2095
- ` let stopped = false;`,
2096
- ` let last = "";`,
2097
- ` const tick = async () => { try { const rows = await holon.pendingConflicts!(); const j = JSON.stringify(rows); if (!stopped && j !== last) { last = j; cb(rows); } } catch { /* projection catching up */ } };`,
2098
- ` void tick();`,
2099
- ` const t = setInterval(tick, opts?.intervalMs ?? 1000);`,
2100
- ` return () => { stopped = true; clearInterval(t); };`,
2101
- ` },`,
2692
+ ` /** Pending business merges. Start at summary, then query one aggregate; the returned`,
2693
+ ` * conflict is the typed capability that can merge itself. Nomos's raw index stays private. */`,
2694
+ ` conflicts: {`,
2695
+ ` summary: async (): Promise<ConflictSummary> => {`,
2696
+ ` if (!holon.pendingConflictSummary) throw new Error("conflicts need the current @githolon/client");`,
2697
+ ` return holon.pendingConflictSummary();`,
2698
+ ` },`,
2102
2699
  );
2103
- for (const b of resolverBindings) {
2104
- if (b.valuePayloadField === undefined) continue; // probe couldn't bind — dispatch the directive directly.
2105
- const payloadName = dirPayloadNames.get(b.directiveId);
2106
- if (payloadName === undefined) continue;
2107
- const helper = `resolve${cap(camel(b.aggregateId))}${cap(camel(b.field))}`;
2108
- const idField = b.idPayloadField;
2109
- const bound = [b.valuePayloadField, ...(idField !== undefined ? [idField] : [])];
2110
- const restType = `Omit<${payloadName}, ${bound.map((f) => JSON.stringify(f)).join(" | ")}>`;
2111
- const conflictParam = idField !== undefined ? `conflict: string | PendingConflictRow, ` : ``;
2112
- const payloadExpr = `{ ...(rest ?? {}), ${idField !== undefined ? `${JSON.stringify(idField)}: typeof conflict === "string" ? conflict : conflict.aggregateId, ` : ``}${JSON.stringify(b.valuePayloadField)}: chosen } as ${payloadName}`;
2700
+ for (const [aggregateId, bindings] of byAggregate) {
2701
+ const runtimeAggregateType = conflictAggregateType(aggregateId);
2113
2702
  methods.push(
2114
- ` /** RESOLVE a pending conflict on \`${b.aggregateId}.${b.field}\` by dispatching the DESIGNATED`,
2115
- ` * resolver \`${b.directiveId}\` with the chosen value${idField !== undefined ? ` (the conflict row — or the bare aggregate id — names the instance)` : ""}.`,
2116
- ` * ESCAPE HATCH: any gated directive that sets the field still resolves the conflict in the`,
2117
- ` * kernel sense; \`.resolves()\` only designates which one THIS typed helper dispatches. */`,
2118
- ` ${helper}: (${conflictParam}chosen: ${payloadName}[${JSON.stringify(b.valuePayloadField)}], rest?: ${restType}): Promise<NomosOutcome> =>`,
2119
- ` _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(b.directiveId)}, ${payloadExpr}, { domainHash, actor, returnCreated: true }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>),`,
2703
+ ` ${lcFirst(camel(aggregateId))}: {`,
2704
+ ` /** Pending merges for ${aggregateId}. Querying creates typed merge references;`,
2705
+ ` * no aggregate id, field name, or directive id is supplied by the app. */`,
2706
+ ` pending: async () => {`,
2707
+ ` if (!holon.pendingConflicts) throw new Error("conflicts need the current @githolon/client");`,
2708
+ ` const rows = await holon.pendingConflicts(${JSON.stringify(runtimeAggregateType)});`,
2709
+ ` return rows.flatMap((conflict) => {`,
2710
+ ` if (conflict.aggregateType !== ${JSON.stringify(runtimeAggregateType)}) return [];`,
2711
+ ` if (typeof conflict.snapshot !== "string" || conflict.snapshot.length === 0 || typeof conflict.targetSchema !== "string" || conflict.targetSchema.length === 0) {`,
2712
+ ` throw new Error("conflicts need a runtime with immutable conflict snapshots; upgrade Nomos before merging");`,
2713
+ ` }`,
2714
+ ` switch (conflict.field) {`,
2715
+ );
2716
+ for (const b of bindings) {
2717
+ if (b.valuePayloadField === undefined || b.idPayloadField === undefined) continue;
2718
+ const payloadName = dirPayloadNames.get(b.directiveId);
2719
+ if (payloadName === undefined) continue;
2720
+ const valueType = `${payloadName}[${JSON.stringify(b.valuePayloadField)}]`;
2721
+ const bound = [b.valuePayloadField, b.idPayloadField];
2722
+ const restType = `Omit<${payloadName}, ${bound.map((f) => JSON.stringify(f)).join(" | ")}>`;
2723
+ const payloadExpr = `{ ...(rest ?? {}), ${JSON.stringify(b.idPayloadField)}: conflict.aggregateId, ${JSON.stringify(b.valuePayloadField)}: merged } as ${payloadName}`;
2724
+ methods.push(
2725
+ ` case ${JSON.stringify(b.field)}: {`,
2726
+ ` if (conflict.status === "evolutionRequired") {`,
2727
+ ` return [{ aggregateId: conflict.aggregateId, field: ${JSON.stringify(b.field)} as const, status: "evolutionRequired" as const, evolution: conflict.evolution! }];`,
2728
+ ` }`,
2729
+ ` if (!Array.isArray(conflict.observations)) throw new Error("Nomos returned a ready conflict without projected versions");`,
2730
+ ` const versions = conflict.observations as SurfacedObservation<${valueType}>[];`,
2731
+ ` return [{`,
2732
+ ` aggregateId: conflict.aggregateId,`,
2733
+ ` field: ${JSON.stringify(b.field)} as const,`,
2734
+ ` status: "ready" as const,`,
2735
+ ` versions,`,
2736
+ ` /** Produce and commit a merge result from the competing versions. The result`,
2737
+ ` * may equal either side or be a new business value, exactly like a Git merge. */`,
2738
+ ` merge: (reconcile: (versions: readonly SurfacedObservation<${valueType}>[]) => ${valueType}, rest?: ${restType}): Promise<NomosOutcome> => {`,
2739
+ ` const merged = reconcile(versions);`,
2740
+ ` return _outcome(holon.workspace, (holon.offerDirective as __NomosConflictOffer)(${JSON.stringify(domain)}, ${JSON.stringify(b.directiveId)}, ${payloadExpr}, { domainHash, actor, returnCreated: true, __nomosConflictSnapshots: [{ aggregateType: conflict.aggregateType, aggregateId: conflict.aggregateId, field: conflict.field, targetSchema: conflict.targetSchema, token: conflict.snapshot }] }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>);`,
2741
+ ` },`,
2742
+ ` }];`,
2743
+ ` }`,
2744
+ );
2745
+ }
2746
+ methods.push(
2747
+ ` default: return [];`,
2748
+ ` }`,
2749
+ ` });`,
2750
+ ` },`,
2751
+ ` },`,
2120
2752
  );
2121
2753
  }
2754
+ methods.push(` },`);
2122
2755
  }
2123
2756
  for (const agg of mod.aggregates as AggregateHandle[]) {
2124
2757
  const aScopes = readScopesFor(agg.id);
2758
+ const aOwned = aScopes.some((scope) => scope.owned === true);
2759
+ const aScopeJson = JSON.stringify(aScopes);
2760
+ const runtimeType = runtimeTypeOf(agg);
2125
2761
  methods.push(
2126
2762
  ` /** Read one ${agg.id} by aggregate id.${aScopes.length ? " Encrypted field(s) decrypt when the bound { keyStore } holds the scope key." : ""} */`,
2127
2763
  aScopes.length
2128
- ? ` ${lcFirst(camel(agg.id))}ById: async (id: string) => ${wrapP(`holon.queryById(id, await _readKeys(keyStore, ${JSON.stringify(aScopes)})) as Promise<Row<${agg.id}Data>[]>`)},`
2764
+ ? ` ${lcFirst(camel(agg.id))}ById: async (id: string) => ${wrapP(`holon.queryById(id, await _readKeys(holon, keyStore, ${aScopeJson}${aOwned ? `, [{ type: ${JSON.stringify(runtimeType)}, id }]` : ""})) as Promise<Row<${agg.id}Data>[]>`)},`
2129
2765
  : ` ${lcFirst(camel(agg.id))}ById: (id: string) => ${wrapP(`holon.queryById(id) as Promise<Row<${agg.id}Data>[]>`)},`,
2130
2766
  ` /** Every ${agg.id} (the type-index probe — O(result), not a scan). */`,
2131
2767
  aScopes.length
2132
- ? ` list${agg.id}s: async () => ${wrapP(`holon.query("aggregatesByType", { __type: ${JSON.stringify(agg.id)} }, await _readKeys(keyStore, ${JSON.stringify(aScopes)})) as Promise<Row<${agg.id}Data>[]>`)},`
2768
+ ? aOwned
2769
+ ? ` list${agg.id}s: async () => ${wrapP(`_readWithOwnedKeys(holon, keyStore, ${aScopeJson}, (keys) => holon.query("aggregatesByType", { __type: ${JSON.stringify(agg.id)} }, keys) as Promise<Row<${agg.id}Data>[]>)`)},`
2770
+ : ` list${agg.id}s: async () => ${wrapP(`holon.query("aggregatesByType", { __type: ${JSON.stringify(agg.id)} }, await _readKeys(holon, keyStore, ${aScopeJson})) as Promise<Row<${agg.id}Data>[]>`)},`
2133
2771
  : ` list${agg.id}s: () => ${wrapP(`holon.query("aggregatesByType", { __type: ${JSON.stringify(agg.id)} }) as Promise<Row<${agg.id}Data>[]>`)},`,
2134
2772
  ` /** Watch one ${agg.id} by aggregate id (local reactive read; returns stop()). */`,
2135
- ` watch${agg.id}ById: (id: string, cb: (rows: Row<${agg.id}Data>[]) => void, opts?: { intervalMs?: number }) =>`,
2136
- ` holon.watchById(id, ((rows: Row<${agg.id}Data>[]) => cb(${wrapRows("rows")})) as (rows: unknown) => void, opts),`,
2773
+ aOwned
2774
+ ? ` watch${agg.id}ById: (id: string, cb: (rows: Row<${agg.id}Data>[]) => void, opts?: { intervalMs?: number }) => _watchWithOwnedKeys((changed) => holon.watchById(id, changed as (rows: unknown) => void, opts), async () => ${wrapP(`holon.queryById(id, await _readKeys(holon, keyStore, ${aScopeJson}, [{ type: ${JSON.stringify(runtimeType)}, id }])) as Promise<Row<${agg.id}Data>[]>`)}, cb),`
2775
+ : ` watch${agg.id}ById: (id: string, cb: (rows: Row<${agg.id}Data>[]) => void, opts?: { intervalMs?: number }) => holon.watchById(id, ((rows: Row<${agg.id}Data>[]) => cb(${wrapRows("rows")})) as (rows: unknown) => void, opts),`,
2776
+ );
2777
+ pastMethods.push(
2778
+ ` /** Read one ${agg.id} by aggregate id at this moment. */`,
2779
+ aScopes.length
2780
+ ? ` ${lcFirst(camel(agg.id))}ById: async (id: string) => ${wrapP(`historic.queryById(id, await _readKeys(holon, keyStore, ${aScopeJson}${aOwned ? `, [{ type: ${JSON.stringify(runtimeType)}, id }]` : ""})) as Promise<Row<${agg.id}Data>[]>`)},`
2781
+ : ` ${lcFirst(camel(agg.id))}ById: (id: string) => ${wrapP(`historic.queryById(id) as Promise<Row<${agg.id}Data>[]>`)},`,
2782
+ ` /** Every ${agg.id} at this moment. */`,
2783
+ aScopes.length
2784
+ ? aOwned
2785
+ ? ` list${agg.id}s: async () => ${wrapP(`_readWithOwnedKeys(holon, keyStore, ${aScopeJson}, (keys) => historic.query("aggregatesByType", { __type: ${JSON.stringify(agg.id)} }, keys) as Promise<Row<${agg.id}Data>[]>)`)},`
2786
+ : ` list${agg.id}s: async () => ${wrapP(`historic.query("aggregatesByType", { __type: ${JSON.stringify(agg.id)} }, await _readKeys(holon, keyStore, ${aScopeJson})) as Promise<Row<${agg.id}Data>[]>`)},`
2787
+ : ` list${agg.id}s: () => ${wrapP(`historic.query("aggregatesByType", { __type: ${JSON.stringify(agg.id)} }) as Promise<Row<${agg.id}Data>[]>`)},`,
2137
2788
  );
2138
2789
  }
2139
2790
 
2791
+ methods.push(
2792
+ ` /** Historic scrubbing in the same generated business language as live reads. */`,
2793
+ ` past: {`,
2794
+ ` moments: async (opts: { limit?: number } = {}): Promise<NomosMoment[]> => {`,
2795
+ ` if (!holon.commits) throw new Error("past.moments() needs the current @githolon/client");`,
2796
+ ` const actions = new Set(${JSON.stringify([...dirs.map((directive) => directive.id), ...(correctionActions.length > 0 ? [BUSINESS_CORRECTION_DIRECTIVE] : [])])});`,
2797
+ ` return (await holon.commits(opts))`,
2798
+ ` .filter((commit) => commit.domain === ${JSON.stringify(domain)} && commit.directiveId !== null && actions.has(commit.directiveId))`,
2799
+ ` .map((commit) => ({`,
2800
+ ` id: commit.oid,`,
2801
+ ` occurredAt: commit.at == null ? null : new Date(commit.at).toISOString(),`,
2802
+ ` actorSubject: commit.actorSubject,`,
2803
+ ` domain: commit.domain,`,
2804
+ ` action: commit.directiveId === ${JSON.stringify(BUSINESS_CORRECTION_DIRECTIVE)} ? "correction" : commit.directiveId,`,
2805
+ ` }));`,
2806
+ ` },`,
2807
+ ` at: (moment: NomosMoment) => {`,
2808
+ ` if (!holon.at) throw new Error("past.at() needs the current @githolon/client");`,
2809
+ ` const historic = holon.at(moment.id);`,
2810
+ ` return {`,
2811
+ ...pastMethods,
2812
+ ` };`,
2813
+ ` },`,
2814
+ ` },`,
2815
+ );
2816
+
2140
2817
  if (activityPresent) {
2141
2818
  methods.push(
2142
- ` /** THE ACTIVITY FEED (declared \`.activity(...)\`): typed activity rows folded from the intent log.`,
2143
- ` * \`cb\` fires with the current rows + on every new committed intent; returns stop(). Directives with`,
2144
- ` * no declared activity are dropped. */`,
2819
+ ` /** THE ACCOUNTABLE ACTIVITY FEED: typed business rows folded from the verified intent log.`,
2820
+ ` * Every public directive has a baseline; \`.activity(...)\` enriches it. Framework/control work`,
2821
+ ` * outside this generated package is dropped. Returns stop(). */`,
2145
2822
  ` watchActivity: (cb: (events: ActivityEvent[]) => void, opts?: { base?: string }) =>`,
2146
2823
  ` holon.watchActivity((rows) => cb(rows.map(renderActivity).filter((e): e is ActivityEvent => e !== null)), opts),`,
2147
2824
  ` /** One-shot: the current committed activity log, folded to typed [ActivityEvent]s. */`,
2148
2825
  ` activityLog: async (opts?: { base?: string; limit?: number }): Promise<ActivityEvent[]> =>`,
2149
2826
  ` (await holon.activityLog(opts)).map(renderActivity).filter((e): e is ActivityEvent => e !== null),`,
2150
- ` /** NARRATIVE (for an in-app LLM): the workspace story — the activity log folded to typed events +`,
2151
- ` * a plain-English \`transcript\` grounded in the VERIFIED log. Filter by \`actor\` / \`category\` /`,
2152
- ` * \`since\` (ISO occurredAt lower bound); \`limit\` caps the rows pulled. Events are newest-first. */`,
2153
- ` describe: async (opts: { since?: string; actor?: string; category?: string; limit?: number } = {}): Promise<ActivityNarrative> => {`,
2827
+ ` /** THE SELF-DESCRIPTION: root links to exactly six grounded pages — model, current state, rules,`,
2828
+ ` * history, decisions and evidence. The old \`events\` / \`transcript\` fields remain, so this is a`,
2829
+ ` * strict enrichment of the activity narrative. \`section\` selects one page. */`,
2830
+ ` describe: async (opts: { section?: NomosDescribeSection; since?: string; actor?: string; category?: string; limit?: number } = {}): Promise<NomosDescription> => {`,
2154
2831
  ` const raw = await holon.activityLog(opts.limit !== undefined ? { limit: opts.limit } : {});`,
2155
2832
  ` let events = raw.map(renderActivity).filter((e): e is ActivityEvent => e !== null);`,
2156
2833
  ` if (opts.actor !== undefined) events = events.filter((e) => e.actorSubject === opts.actor);`,
2157
2834
  ` if (opts.category !== undefined) events = events.filter((e) => e.category === opts.category);`,
2158
2835
  ` if (opts.since !== undefined) events = events.filter((e) => e.occurredAt >= opts.since!);`,
2159
- ` return { events, transcript: _activityTranscript(events) };`,
2836
+ ` let organisation: NomosOrganisationDescription = { complete: false, children: [] };`,
2837
+ ` if (holon.__nomosChildWorkspaces !== undefined) {`,
2838
+ ` try {`,
2839
+ ` const children = await holon.__nomosChildWorkspaces();`,
2840
+ ` organisation = { complete: children.every((child) => child.status !== "unknown"), children: children.map((child) => ({`,
2841
+ ` ...child, section: "root" as const, label: child.workspace,`,
2842
+ ` question: "What does this part of the organisation do, and what has happened there?",`,
2843
+ ` summary: child.status === "active"`,
2844
+ ` ? "This part of the organisation is active. Follow its generated description to continue."`,
2845
+ ` : child.status === "retired"`,
2846
+ ` ? "This part of the organisation is retired. Its recorded history remains explainable."`,
2847
+ ` : child.status === "reclaimed"`,
2848
+ ` ? "This part of the organisation was reclaimed. Its birth remains part of this workspace's history."`,
2849
+ ` : "This part of the organisation has a newer lifecycle this client does not understand. Its link remains visible.",`,
2850
+ ` })) };`,
2851
+ ` } catch {`,
2852
+ ` // An old/unavailable runtime cannot prove a leaf. Keep incompleteness explicit.`,
2853
+ ` }`,
2854
+ ` }`,
2855
+ ` const section = opts.section ?? "root";`,
2856
+ ` let title: string = _NOMOS_DESCRIBE_MODEL.title;`,
2857
+ ` let summary: string = _NOMOS_DESCRIBE_MODEL.summary;`,
2858
+ ` let items: NomosDescribeItem[] = [];`,
2859
+ ` if (section === "model" || section === "rules") {`,
2860
+ ` const page = _NOMOS_DESCRIBE_MODEL.pages[section];`,
2861
+ ` title = page.title; summary = page.summary;`,
2862
+ ` items = (page.items as readonly NomosDescribeItem[]).map((item) => ({ ...item }));`,
2863
+ ` } else if (section === "current") {`,
2864
+ ` title = "Current state";`,
2865
+ ` summary = "The business records this session is allowed to read now. An unavailable type stays named without leaking its contents.";`,
2866
+ ` items = await Promise.all(_NOMOS_DESCRIBE_MODEL.aggregateTypes.map(async (aggregate): Promise<NomosDescribeItem> => {`,
2867
+ ` try {`,
2868
+ ` const rows = await holon.query("aggregatesByType", { __type: aggregate.queryType }) as { type: string; id: string }[];`,
2869
+ ` const refs = rows.map((row) => ({ workspace: holon.workspace, type: row.type, id: row.id }));`,
2870
+ ` const exact = rows.length > 0 || (aggregate.audience as string) === "public";`,
2871
+ ` return { kind: "current", id: \`${"${aggregate.domain}.${aggregate.type}"}\`, label: aggregate.label,`,
2872
+ ` summary: exact ? \`${"${rows.length} ${aggregate.label} record${rows.length === 1 ? \"\" : \"s\"} visible now."}\` : \`${"No ${aggregate.label} records are visible to this session."}\`,`,
2873
+ ` details: { domain: aggregate.domain, type: aggregate.type, audience: aggregate.audience, ...(exact ? { count: rows.length } : { status: "none-visible" }) }, refs };`,
2874
+ ` } catch (error) {`,
2875
+ ` return { kind: "current", id: \`${"${aggregate.domain}.${aggregate.type}"}\`, label: aggregate.label,`,
2876
+ ` summary: \`${"${aggregate.label} exists in the model, but this session cannot read its current records."}\`,`,
2877
+ ` details: { domain: aggregate.domain, type: aggregate.type, audience: aggregate.audience, status: "unavailable" } };`,
2878
+ ` }`,
2879
+ ` }));`,
2880
+ ` } else if (section === "history") {`,
2881
+ ` title = "Verified business history"; summary = "What changed, who changed it and when, read from verified committed testimony.";`,
2882
+ ` items = events.map((event) => ({ kind: "change", id: event.eventId, label: event.title, summary: event.message,`,
2883
+ ` details: { actor: event.actorSubject, occurredAt: event.occurredAt, domain: event.domain, kind: event.kind },`,
2884
+ ` refs: Object.values(event.refs) }));`,
2885
+ ` } else if (section === "decisions") {`,
2886
+ ` title = "Decisions and work needing attention"; summary = "Recorded business reasons, pending merges and retained rejected work, plus the model's declared conflict policy.";`,
2887
+ ` const decisions: NomosDescribeItem[] = events.filter((event) => event.kind.endsWith(".correction"))`,
2888
+ ` .map((event) => ({ kind: "decision", id: event.eventId, label: event.title, summary: event.message,`,
2889
+ ` details: { actor: event.actorSubject, occurredAt: event.occurredAt, domain: event.domain }, refs: Object.values(event.refs) }));`,
2890
+ ...(hasSurfaced
2891
+ ? [
2892
+ ` const pending: NomosDescribeItem[] = (holon.pendingConflicts ? await holon.pendingConflicts() : []).flatMap((conflict) => {`,
2893
+ ` const aggregate = _NOMOS_DESCRIBE_MODEL.aggregateTypes.find((candidate) => candidate.queryType === conflict.aggregateType);`,
2894
+ ` if (aggregate === undefined) return [];`,
2895
+ ` const rule = (_NOMOS_DESCRIBE_MODEL.pages.rules.items as readonly NomosDescribeItem[])`,
2896
+ ` .find((item) => item.id === \`${"${aggregate.domain}.${aggregate.type}.${conflict.field}.merge"}\`);`,
2897
+ ` const subject = rule?.label ?? aggregate.label + " " + conflict.field;`,
2898
+ ` const versions = conflict.status === "ready" ? conflict.observations?.length ?? 0 : conflict.evolution?.sourceSchemas?.length ?? 0;`,
2899
+ ` const summary = conflict.status === "ready"`,
2900
+ ` ? subject + " has " + versions + " competing versions waiting for one business merge."`,
2901
+ ` : subject + " cannot yet be compared because the model does not explain every earlier value. The disagreement remains pending while unrelated work continues.";`,
2902
+ ` return [{ kind: conflict.status === "ready" ? "pending-merge" : "evolution-required",`,
2903
+ ` id: \`${"decision:${aggregate.domain}.${aggregate.type}:${conflict.aggregateId}:${conflict.field}"}\`, label: "Review " + subject, summary,`,
2904
+ ` details: { domain: aggregate.domain, type: aggregate.type, field: conflict.field, status: conflict.status, versions, next: conflict.status === "ready" ? "merge" : "update-model" },`,
2905
+ ` refs: [{ workspace: holon.workspace, type: aggregate.type, id: conflict.aggregateId }] }];`,
2906
+ ` });`,
2907
+ ]
2908
+ : []),
2909
+ ` const actions = (_NOMOS_DESCRIBE_MODEL.pages.model.items as readonly NomosDescribeItem[]).filter((item) => item.kind === "verb");`,
2910
+ ` const rejected: NomosDescribeItem[] = (await holon.deadLetters()).flatMap((entry) => {`,
2911
+ ` if (typeof entry.domain !== "string" || typeof entry.directiveId !== "string") return [];`,
2912
+ ` const action = actions.find((item) => item.id === entry.domain + "." + entry.directiveId);`,
2913
+ ` if (action === undefined) return [];`,
2914
+ ` const reason = _nomosRefusalReason(entry.error);`,
2915
+ ` return [{ kind: "rejected-action", id: "rejected:" + (entry.id ?? entry.oid), label: "Review " + action.label,`,
2916
+ ` summary: _nomosRefusalSummary(reason, action.label),`,
2917
+ ` details: { domain: entry.domain, action: entry.directiveId, status: "retained", reason, refusedAt: entry.at, canRetry: true } }];`,
2918
+ ` });`,
2919
+ ` const policies = (_NOMOS_DESCRIBE_MODEL.pages.rules.items as readonly NomosDescribeItem[]).filter((item) => item.kind === "conflict-rule" || item.kind === "merge-rule")`,
2920
+ ` .map((item) => ({ ...item }));`,
2921
+ ` items = [...decisions, ${hasSurfaced ? "...pending, " : ""}...rejected, ...policies];`,
2922
+ ` } else if (section === "evidence") {`,
2923
+ ` title = "Evidence"; summary = "Opaque identities tying this answer to one workspace, one generated law and its verified business history.";`,
2924
+ ` const latestMoment = events[0];`,
2925
+ ` items = [`,
2926
+ ` { kind: "evidence", id: "workspace", label: "Workspace", summary: holon.workspace, details: { workspace: holon.workspace } },`,
2927
+ ` { kind: "evidence", id: "law", label: "Generated law", summary: domainHash, details: { domainHash } },`,
2928
+ ` { kind: "evidence", id: "reader", label: "Verified reader", summary: actor ?? "anonymous", details: { actor: actor ?? null } },`,
2929
+ ` ...(latestMoment === undefined ? [] : [{ kind: "evidence", id: latestMoment.eventId, label: "Latest verified moment",`,
2930
+ ` summary: latestMoment.eventId, details: { occurredAt: latestMoment.occurredAt, actor: latestMoment.actorSubject, domain: latestMoment.domain, kind: latestMoment.kind } }]),`,
2931
+ ` ];`,
2932
+ ` }`,
2933
+ ` return { section, title, summary, items, links: _NOMOS_DESCRIBE_MODEL.links.map((link) => ({ ...link })), organisation, events, transcript: _activityTranscript(events) };`,
2160
2934
  ` },`,
2161
2935
  ` /** NARRATIVE for ONE thing: the story of the aggregate \`aggregateId\` — events whose declared refs`,
2162
2936
  ` * point at it (only directives declaring a ref to the aggregate appear in its story) + a transcript. */`,
@@ -2174,13 +2948,33 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2174
2948
  ` * Pass \`{ actor }\` (the verified principal uid) to author AS that user — every write seals it as`,
2175
2949
  ` * the captured principal, so the ReBAC \`.requires(...)\` gate + the integrity-dual endorse model`,
2176
2950
  ` * judge the real author. Omit it for open (un-gated) domains. */`,
2177
- `export function ${tsClientFactoryName(domain)}(holon: NomosHolon, opts: { domainHash?: string; actor?: string${encPresent ? "; keyStore?: NomosKeyStore" : ""} } = {}) {`,
2951
+ `export function ${tsClientFactoryName(domain)}(holon: NomosHolon, opts: { domainHash?: string; actor?: string${domScopes.length > 0 ? "; keyStore?: NomosKeyStore" : ""} } = {}) {`,
2178
2952
  ` const domainHash = opts.domainHash ?? ${hashConst};`,
2179
2953
  ` const actor = opts.actor ?? holon.principal;`,
2180
- ...(encPresent ? [` const keyStore = opts.keyStore;`] : []),
2954
+ ...(encPresent ? [` const keyStore: NomosKeyStore | undefined = ${domScopes.length > 0 ? "opts.keyStore" : "undefined"};`] : []),
2955
+ ...(ownedScope !== undefined && Object.keys(ownedActions).length > 0
2956
+ ? [
2957
+ ` const _ownedActions: Record<string, Record<"share" | "unshare" | "transfer", string>> = ${JSON.stringify(ownedActions)};`,
2958
+ ` let _ownedReadyError: unknown;`,
2959
+ ` const _ownedReady = holon.__nomosEnsureOwnedIdentity`,
2960
+ ` ? holon.__nomosEnsureOwnedIdentity({ scope: ${JSON.stringify(ownedScope)}, domain: ${JSON.stringify(domain)}, domainHash }).catch((error) => { _ownedReadyError = error; })`,
2961
+ ` : Promise.resolve().then(() => { _ownedReadyError = new Error("owned records need the current @githolon/client"); });`,
2962
+ ` const _ownedRecordAction = async (action: "share" | "unshare" | "transfer", record: NomosRef, peer: string): Promise<NomosOutcome> => {`,
2963
+ ` await _ownedReady;`,
2964
+ ` if (_ownedReadyError !== undefined) throw _ownedReadyError;`,
2965
+ ` if (record.workspace !== holon.workspace) throw new Error(\`${"${action}()"} cannot use a record from another workspace\`);`,
2966
+ ` const directiveId = _ownedActions[record.type]?.[action];`,
2967
+ ` if (directiveId === undefined) throw new Error(\`${"${action}()"} needs a generated owned record reference\`);`,
2968
+ ` if (!peer.trim()) throw new Error(\`${"${action}()"} needs the person to act with\`);`,
2969
+ ` if (!holon.__nomosPrepareOwnedRecordAction) throw new Error("record sharing needs the current @githolon/client");`,
2970
+ ` const payload = await holon.__nomosPrepareOwnedRecordAction({ action, scope: ${JSON.stringify(ownedScope)}, record, peer });`,
2971
+ ` return _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, directiveId, payload, { domainHash, actor, returnCreated: true }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>, record);`,
2972
+ ` };`,
2973
+ ]
2974
+ : []),
2181
2975
  ...(opts.compat !== undefined
2182
2976
  ? [
2183
- opts.interface !== undefined
2977
+ clientInterface !== undefined
2184
2978
  ? ` holon.checkCompat?.(${compatConstName(opts.packageName)}, ${interfaceConstName(opts.packageName)});`
2185
2979
  : ` holon.checkCompat?.(${compatConstName(opts.packageName)});`,
2186
2980
  // RELEASE COHERENCE: throw a typed skew error AT BIND if the cloud's kernel lacks a capability
@@ -2191,9 +2985,41 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2191
2985
  ` return {`,
2192
2986
  ` domain: ${JSON.stringify(domain)} as const,`,
2193
2987
  ` domainHash,`,
2194
- ...(opts.interface !== undefined
2988
+ ` /** Adopt THIS generated model. Nomos proves it against current history; a deliberate`,
2989
+ ` * difference returns a decision whose accept(reason) re-proves and records the reason. */`,
2990
+ ` model: {`,
2991
+ ` adopt: (): Promise<NomosModelAdoption> => _nomosAdoptModel(holon, ${JSON.stringify(domain)}, domainHash),`,
2992
+ ` restorePrevious: (): Promise<NomosModelRestoration> => _nomosRestorePreviousModel(holon, ${JSON.stringify(domain)}, domainHash),`,
2993
+ ` },`,
2994
+ ...(clientInterface !== undefined
2195
2995
  ? [` interface: ${interfaceConstName(opts.packageName)}[${JSON.stringify(domain)}],`]
2196
2996
  : []),
2997
+ ...(ownedScope !== undefined
2998
+ ? [
2999
+ ` /** Opt into all-device-loss recovery with one explicitly designated person. Nomos owns the escrow and audit protocol. */`,
3000
+ ` recovery: {`,
3001
+ ` protectWith: async (person: string): Promise<{ status: "protected"; recoveryPrincipal: string; unchanged?: boolean }> => {`,
3002
+ ` if (!person.trim()) throw new Error("recovery.protectWith() needs the person who will recover you");`,
3003
+ ` if (!holon.__nomosProtectOwnedIdentity) throw new Error("recovery.protectWith() needs the current @githolon/client");`,
3004
+ ` return holon.__nomosProtectOwnedIdentity({ scope: ${JSON.stringify(ownedScope)}, domain: ${JSON.stringify(domain)}, domainHash, recoveryPrincipal: person });`,
3005
+ ` },`,
3006
+ ` },`,
3007
+ ` /** Add another of this person's devices without exposing keys, certificates or signer law. */`,
3008
+ ` devices: {`,
3009
+ ` request: async (): Promise<NomosDeviceRequest> => {`,
3010
+ ` if (!holon.__nomosOwnedDeviceRequest) throw new Error("devices.request() needs the current @githolon/client");`,
3011
+ ` return await holon.__nomosOwnedDeviceRequest({ scope: ${JSON.stringify(ownedScope)} }) as NomosDeviceRequest;`,
3012
+ ` },`,
3013
+ ` approve: async (request: NomosDeviceRequest): Promise<NomosOutcome> => {`,
3014
+ ` const principal = actor ?? holon.principal;`,
3015
+ ` if (!principal || !principal.trim()) throw new Error("devices.approve() needs a signed-in Nomos session");`,
3016
+ ` if (!holon.__nomosPrepareOwnedDeviceApproval) throw new Error("devices.approve() needs the current @githolon/client");`,
3017
+ ` const payload = await holon.__nomosPrepareOwnedDeviceApproval({ request, scope: ${JSON.stringify(ownedScope)} });`,
3018
+ ` return _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(OWNED_DEVICE_APPROVAL_DIRECTIVE)}, payload, { domainHash, actor: principal, returnCreated: true }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>);`,
3019
+ ` },`,
3020
+ ` },`,
3021
+ ]
3022
+ : []),
2197
3023
  // THE OWNER CEREMONY (slice 2): only for a domain that actually defines an encryption scope.
2198
3024
  ...(domScopes.length > 0
2199
3025
  ? [