@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.
- package/dart/CHANGELOG.md +12 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +220 -71
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +1 -1
- package/dart/test/adv_realm_interference_test.dart +1 -1
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/cluster_attested_test.dart +1 -1
- package/dart/test/cluster_births_test.dart +1 -1
- package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
- package/dart/test/cluster_local_autosync_test.dart +1 -1
- package/dart/test/cluster_scale_test.dart +1 -1
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/dlq_session_surface_test.dart +1 -1
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +132 -87
- package/dart/test/local_sync_inert_test.dart +10 -5
- package/dart/test/parent_signer_recovery_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +1 -1
- package/dart/test/session_bound_test.dart +1 -1
- package/dart/test/test_harness_test.dart +2 -2
- package/dart/test/vo_read_typing_test.dart +1 -1
- package/dart/test/watch_after_offer_stall_test.dart +1 -1
- package/dart/test/webview_bridge_test.dart +274 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/errors.dart +136 -30
- package/dart_types/lib/src/surfaced.dart +48 -2
- package/dart_types/lib/src/wire.dart +3 -1
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +19 -0
- package/package.json +1 -1
- package/src/activity.ts +43 -4
- package/src/aggregate.ts +132 -22
- package/src/attested_core.ts +1 -1
- package/src/authoring.ts +119 -6
- package/src/birth_meter.ts +4 -4
- package/src/birth_meter_ensure.ts +165 -29
- package/src/births.ts +53 -22
- package/src/build_package.ts +96 -20
- package/src/business_birth.ts +79 -16
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +930 -87
- package/src/codegen_openapi.ts +28 -5
- package/src/codegen_proof.ts +107 -11
- package/src/codegen_ts.ts +961 -135
- package/src/compile_package_main.ts +172 -53
- package/src/conflicts.ts +252 -0
- package/src/describe_model.ts +448 -0
- package/src/directive.ts +5 -38
- package/src/drivers.ts +12 -4
- package/src/engine_entry.ts +4 -0
- package/src/evolve_preview.ts +72 -11
- package/src/fields.ts +201 -55
- package/src/framework/application_home.ts +103 -0
- package/src/framework/application_home_domain.ts +2 -0
- package/src/framework/attested_receipts.ts +2 -2
- package/src/framework/auth_provider_law.ts +51 -24
- package/src/framework/business_corrections.ts +170 -0
- package/src/framework/disclosure.ts +1 -1
- package/src/framework/domain_lifecycle.ts +250 -16
- package/src/framework/encryption_identity.ts +1 -1
- package/src/framework/encryption_scope.ts +21 -13
- package/src/framework/era_certification.ts +1 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +1 -1
- package/src/framework/home.ts +22 -9
- package/src/framework/identity.ts +5 -5
- package/src/framework/impure_capability.ts +2 -2
- package/src/framework/owned_custody.ts +561 -0
- package/src/framework/platform_admin.ts +1 -1
- package/src/framework/relations.ts +9 -1
- package/src/framework/repair.ts +1 -1
- package/src/framework/signing_identity.ts +2 -2
- package/src/framework/source_roster.ts +1 -1
- package/src/framework/workspaces.ts +17 -10
- package/src/generated_client_names.ts +39 -0
- package/src/index.ts +20 -15
- package/src/kernel_capabilities.ts +25 -1
- package/src/law_check.ts +4 -1
- package/src/manifest.ts +356 -63
- package/src/read_projection_manifest.ts +8 -0
- package/src/read_projection_namespace.ts +4 -0
- package/src/relation.ts +1 -1
- package/src/shape_compat.ts +61 -5
- package/src/stable_ids.ts +38 -6
- package/src/stable_ids_types.ts +30 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd.ts +62 -11
- package/src/usd_layers.ts +54 -3
- package/src/value_object.ts +23 -19
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +48 -4
- package/src/workspace_routing.ts +57 -0
- package/src/workspace_sharding.ts +9 -9
- 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
|
|
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 {
|
|
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(/[
|
|
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
|
-
|
|
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] = {
|
|
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
|
|
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-
|
|
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
|
-
*
|
|
698
|
-
|
|
699
|
-
|
|
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 =
|
|
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
|
-
? `
|
|
709
|
-
:
|
|
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:
|
|
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:
|
|
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 —
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
` *
|
|
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
|
|
766
|
-
` if (
|
|
767
|
-
` for (const c of
|
|
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:
|
|
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
|
|
778
|
-
` *
|
|
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
|
-
`
|
|
781
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
...(
|
|
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(
|
|
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
|
-
|
|
917
|
-
|
|
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
|
|
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:
|
|
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
|
|
1031
|
-
`
|
|
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 &&
|
|
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
|
|
1037
|
-
` * a (aggregate, field) whose folded state awaits a
|
|
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
|
-
`
|
|
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
|
-
` /**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
1256
|
-
` const
|
|
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 (
|
|
1837
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
?
|
|
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
|
-
// ──
|
|
2080
|
-
//
|
|
2081
|
-
//
|
|
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
|
-
` /**
|
|
2086
|
-
` *
|
|
2087
|
-
`
|
|
2088
|
-
`
|
|
2089
|
-
`
|
|
2090
|
-
`
|
|
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
|
|
2104
|
-
|
|
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
|
-
`
|
|
2115
|
-
`
|
|
2116
|
-
`
|
|
2117
|
-
`
|
|
2118
|
-
`
|
|
2119
|
-
`
|
|
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(
|
|
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
|
-
?
|
|
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
|
-
|
|
2136
|
-
|
|
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
|
|
2143
|
-
` *
|
|
2144
|
-
` *
|
|
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
|
-
` /**
|
|
2151
|
-
` *
|
|
2152
|
-
` *
|
|
2153
|
-
` describe: async (opts: { since?: string; actor?: string; category?: string; limit?: number } = {}): Promise<
|
|
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
|
-
`
|
|
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${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
? [
|