@githolon/dsl 0.14.0 → 0.16.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/example/nomos_webview_transport.dart +2 -2
- package/dart/lib/nomos_dsl.dart +4 -4
- package/dart/lib/src/{dispatch.dart → offer.dart} +7 -7
- package/dart/lib/src/provider.dart +30 -30
- package/dart/lib/src/subscriptions.dart +3 -3
- package/dart/lib/src/webview_bridge.dart +56 -21
- package/dart/test/webview_bridge_test.dart +35 -34
- package/package.json +5 -3
- package/src/aggregate.ts +9 -1
- package/src/authoring.ts +10 -1
- package/src/build_package.ts +1 -1
- package/src/capability_exports.ts +2 -2
- package/src/codegen_dart.ts +34 -24
- package/src/codegen_dart_client.ts +74 -12
- package/src/codegen_provider_dart.ts +67 -61
- package/src/codegen_ts.ts +45 -17
- package/src/compile_package_main.ts +103 -36
- package/src/ctx.ts +13 -0
- package/src/engine_entry.ts +22 -5
- package/src/fields.ts +44 -0
- package/src/flow_gate.ts +208 -0
- package/src/framework/capability.ts +29 -11
- package/src/framework/encryption_scope.ts +9 -1
- package/src/framework/genesis.ts +5 -1
- package/src/framework/impure_capability.ts +73 -67
- package/src/framework/relations.ts +23 -4
- package/src/framework/signing_identity.ts +1 -1
- package/src/framework/sync_lifecycle.ts +3 -3
- package/src/framework/workspace_invariant.ts +29 -8
- package/src/framework/workspaces.ts +7 -0
- package/src/index.ts +25 -1
- package/src/manifest.ts +11 -17
- package/src/ops.ts +42 -1
- package/src/principal_evidence.ts +112 -0
- package/src/relation_sugar.ts +10 -4
- package/src/shape_compat.ts +208 -0
- package/src/usd.ts +287 -0
- package/src/usd_layers.ts +213 -26
- package/src/wire.ts +8 -0
- package/src/wire_encode.ts +75 -2
- package/src/workspace_sharding.ts +197 -24
- package/src/workspace_type.ts +1 -0
package/src/codegen_ts.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Dart apps get generated payload classes / read models / typed query accessors;
|
|
12
12
|
* until this file, a web app on `@githolon/client` hand-typed directive ids and
|
|
13
|
-
* payload shapes against the raw `
|
|
13
|
+
* payload shapes against the raw `offer()` surface. `generateTsClient(modules,
|
|
14
14
|
* opts)` closes that: ONE generated `.ts` file per package, emitted by
|
|
15
15
|
* `nomos-compile` AFTER the package is hashed (the deployed law's content hash is
|
|
16
16
|
* baked in as a constant), carrying:
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
* becomes `?`, `.int()` stays `number`), so a payload the type system accepts is
|
|
21
21
|
* a payload the engine accepts; required string fields named `…At` are
|
|
22
22
|
* CALLER-STAMPED TIMESTAMPS — optional in the type, auto-filled with ISO now()
|
|
23
|
-
* at
|
|
23
|
+
* at offer (see {@link autoStampFields});
|
|
24
24
|
* * a READ-MODEL interface per aggregate — derived from the DSL `Field` kinds the
|
|
25
25
|
* projection decodes by (set→`T[]`, map→`Record<string, V>` via `mapValueKind`,
|
|
26
26
|
* `t.jsonObject()`→`Record<string, unknown>`, plain `t.json()`→`unknown`), with
|
|
27
27
|
* derived/combined read fields merged in (typed off their declared zod returns).
|
|
28
28
|
* EVERY field is `?`: a projection row carries only what has folded so far;
|
|
29
|
-
* * a CLIENT FACTORY per domain — typed `
|
|
29
|
+
* * a CLIENT FACTORY per domain — typed `offer` wrappers bound to the baked
|
|
30
30
|
* domainHash, typed declared-query accessors, and by-id read/watch helpers —
|
|
31
31
|
* over a STRUCTURAL `NomosHolon` interface, so the file has ZERO imports and
|
|
32
32
|
* binds to `@githolon/client`'s `connect()` result (browser or node) as-is.
|
|
@@ -64,6 +64,9 @@ export const tsClientFactoryName = (domain: string): string => `${lcFirst(camel(
|
|
|
64
64
|
/** `tool-library` / `tool_library` / `Tool Library` → `TOOL_LIBRARY_DOMAIN_HASH` — the hash const. */
|
|
65
65
|
export const tsHashConstName = (packageName: string): string => `${upperSnake(packageName)}_DOMAIN_HASH`;
|
|
66
66
|
|
|
67
|
+
/** `tool-library` → `TOOL_LIBRARY_INTERFACE` — the baked structural-interface const (#72). */
|
|
68
|
+
export const interfaceConstName = (packageName: string): string => `${upperSnake(packageName)}_INTERFACE`;
|
|
69
|
+
|
|
67
70
|
// ── zod → TS (the same `_def ?? def` convention as codegen_dart's walker) ─────────
|
|
68
71
|
// The walkers are exported: codegen_proof.ts synthesizes SAMPLE payloads from the
|
|
69
72
|
// same zod shapes this file derives TYPES from — one introspection, two emitters.
|
|
@@ -173,7 +176,7 @@ function tsTypeOfZod(zt: z.ZodTypeAny, indent: string): string {
|
|
|
173
176
|
/**
|
|
174
177
|
* Top-level REQUIRED string payload fields named `…At` (`signedAt`, `updatedAt`,
|
|
175
178
|
* `stampedAt`, …) are CALLER-STAMPED TIMESTAMPS by convention: the generated client
|
|
176
|
-
* fills an omitted one with `new Date().toISOString()` at
|
|
179
|
+
* fills an omitted one with `new Date().toISOString()` at offer. The stamp rides
|
|
177
180
|
* IN the payload — the plan stays a pure function of `(payload, ports)` and replays
|
|
178
181
|
* byte-identical on every peer — so the domain dev stops thinking about clocks.
|
|
179
182
|
* Passing the field explicitly always wins (scheduling-shaped fields like
|
|
@@ -210,11 +213,11 @@ function tsTypeOfPayload(
|
|
|
210
213
|
f = zodDef(f).innerType!;
|
|
211
214
|
}
|
|
212
215
|
const note = stamped.has(name)
|
|
213
|
-
? " // auto-stamped with ISO now() at
|
|
216
|
+
? " // auto-stamped with ISO now() at offer when omitted"
|
|
214
217
|
: mintedSet.has(name)
|
|
215
|
-
? " // kernel-minted at
|
|
218
|
+
? " // kernel-minted at offer when omitted (the front-door mint; returned on the result)"
|
|
216
219
|
: placedSet.has(name)
|
|
217
|
-
? " // picked LEAST-LOADED over the coordinator's law-state at
|
|
220
|
+
? " // picked LEAST-LOADED over the coordinator's law-state at offer when omitted (sharding §3)"
|
|
218
221
|
: "";
|
|
219
222
|
return ` ${name}${optional ? "?" : ""}: ${tsTypeOfZod(f, " ")};${note}`;
|
|
220
223
|
});
|
|
@@ -287,8 +290,12 @@ function aggregatesOf(mod: Record<string, unknown>): AggregateHandle[] {
|
|
|
287
290
|
export interface TsClientOptions {
|
|
288
291
|
/** The package name (`nomos.package.mjs` `name`) — used for the hash constant. */
|
|
289
292
|
readonly packageName: string;
|
|
290
|
-
/** sha256 of the built `.package.usda` — the law this client
|
|
293
|
+
/** sha256 of the built `.package.usda` — the law this client offers under. */
|
|
291
294
|
readonly domainHash: string;
|
|
295
|
+
/** The package's STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates
|
|
296
|
+
* (read shapes, sid-keyed), keyed by domain. Baked in so `connect()` can negotiate
|
|
297
|
+
* shape-compatibility against the workspace's installed offer — NOT by hash. */
|
|
298
|
+
readonly interface?: Record<string, unknown>;
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
/** TRUE when any module declares a PACKED workspace taxonomy (the routed-holon shape).
|
|
@@ -319,15 +326,25 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
319
326
|
`// const holon = await connect({ cloud, workspace, clientId });`,
|
|
320
327
|
`// const app = ${tsClientFactoryName(modules[0]?.domain ?? "domain")}(holon);`,
|
|
321
328
|
``,
|
|
322
|
-
`/** sha256 of the deployed \`.package.usda\` — the installed law this client
|
|
329
|
+
`/** sha256 of the deployed \`.package.usda\` — the installed law this client offers under. */`,
|
|
323
330
|
`export const ${hashConst} = ${JSON.stringify(opts.domainHash)};`,
|
|
324
331
|
``,
|
|
332
|
+
...(opts.interface !== undefined
|
|
333
|
+
? [
|
|
334
|
+
`/** This client's STRUCTURAL INTERFACE REQUIREMENT (#72): the directives (payload shapes)`,
|
|
335
|
+
` * and aggregates (read shapes, sid-keyed) it depends on, keyed by domain. \`connect()\``,
|
|
336
|
+
` * negotiates this against the workspace's installed offer by SHAPE (stable-id subtyping),`,
|
|
337
|
+
` * not by law hash — so the domain can evolve without republishing this client. */`,
|
|
338
|
+
`export const ${interfaceConstName(opts.packageName)} = ${JSON.stringify(opts.interface)} as const;`,
|
|
339
|
+
``,
|
|
340
|
+
]
|
|
341
|
+
: []),
|
|
325
342
|
`/** One projection row: the aggregate's wire type, its id, and the folded fields. */`,
|
|
326
343
|
`export interface Row<T> { type: string; id: string; data: T }`,
|
|
327
344
|
``,
|
|
328
345
|
`/** The holon surface this client binds to (structurally satisfied by @githolon/client). */`,
|
|
329
346
|
`export interface NomosHolon {`,
|
|
330
|
-
`
|
|
347
|
+
` offer(intentBytes: string | Uint8Array): Promise<string>;`,
|
|
331
348
|
` query(queryId: string, params?: Record<string, unknown>): Promise<unknown>;`,
|
|
332
349
|
` queryById(aggregateId: string): Promise<unknown>;`,
|
|
333
350
|
` watchById(aggregateId: string, cb: (rows: unknown) => void, opts?: { intervalMs?: number }): () => void;`,
|
|
@@ -347,6 +364,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
347
364
|
` discardDeadLetter(id: string): Promise<{ ok: boolean; removed: number }>;`,
|
|
348
365
|
`}`,
|
|
349
366
|
``,
|
|
367
|
+
`const encodeIntentOffer = (domain: string, directiveId: string, payload: unknown, domainHash: string, actor?: string): Uint8Array =>`,
|
|
368
|
+
` new TextEncoder().encode(JSON.stringify({ v: 2, intent: { domain, directiveId, payload, domainHash }, ...(actor ? { security: { actor } } : {}) }));`,
|
|
369
|
+
``,
|
|
350
370
|
);
|
|
351
371
|
|
|
352
372
|
const anyAutoStamp = modules.some((mod) =>
|
|
@@ -553,7 +573,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
553
573
|
|
|
554
574
|
const mintPlan = mintPlans.get(mod);
|
|
555
575
|
// PLACEMENT directives (packed taxonomy births): their `shard` is AUTO-PICKED
|
|
556
|
-
// least-loaded by the routed holon at
|
|
576
|
+
// least-loaded by the routed holon at offer — optional on the typed surface
|
|
557
577
|
// (the §7 transparency contract: a dev never names a shard).
|
|
558
578
|
const placementDirs = new Set<string>();
|
|
559
579
|
if (mod.workspaceTypes !== undefined && mod.workspaceTypes.length > 0) {
|
|
@@ -573,7 +593,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
573
593
|
const mint = mintPlan?.get(d.id);
|
|
574
594
|
const placed = placementDirs.has(d.id) ? ["shard"] : [];
|
|
575
595
|
out.push(
|
|
576
|
-
`/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema).${stamps.length ? ` Omitted ${stamps.map((s) => `\`${s}\``).join("/")} auto-stamp(s) ISO now() at
|
|
596
|
+
`/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema).${stamps.length ? ` Omitted ${stamps.map((s) => `\`${s}\``).join("/")} auto-stamp(s) ISO now() at offer.` : ""}${mint ? ` Omitted \`${mint.field}\` is KERNEL-MINTED at offer (returned on the result).` : ""}${placed.length ? " Omitted `shard` is picked LEAST-LOADED at offer (the routed holon)." : ""} */`,
|
|
577
597
|
`export type ${payloadName} = ${tsTypeOfPayload(schema, stamps, mint ? [mint.field] : [], placed)};`,
|
|
578
598
|
``,
|
|
579
599
|
);
|
|
@@ -592,7 +612,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
592
612
|
` * — ids stay internal plumbing — and returns it with the new local head. */`,
|
|
593
613
|
` ${d.id}: async (payload: ${payloadName}): Promise<{ head: string; ${mint.field}: string }> => {`,
|
|
594
614
|
` const ${mint.field} = payload.${mint.field} ?? (await (holon as NomosRoutedHolon).mint(${JSON.stringify(mint.mintType)}${mintOpts}));`,
|
|
595
|
-
` const head = await holon.
|
|
615
|
+
` const head = await holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, domainHash, actor));`,
|
|
596
616
|
` return { head, ${mint.field} };`,
|
|
597
617
|
` },`,
|
|
598
618
|
);
|
|
@@ -601,8 +621,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
601
621
|
methods.push(
|
|
602
622
|
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head. */`,
|
|
603
623
|
stamps.length
|
|
604
|
-
? ` ${d.id}: (payload: ${payloadName}) => holon.
|
|
605
|
-
: ` ${d.id}: (payload: ${payloadName}) => holon.
|
|
624
|
+
? ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, stampNow(payload, ${JSON.stringify(stamps)}), domainHash, actor)),`
|
|
625
|
+
: ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, payload, domainHash, actor)),`,
|
|
606
626
|
);
|
|
607
627
|
}
|
|
608
628
|
|
|
@@ -665,11 +685,19 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
665
685
|
}
|
|
666
686
|
|
|
667
687
|
out.push(
|
|
668
|
-
`/** Typed client for the \`${domain}\` domain, bound to a connected holon
|
|
669
|
-
`
|
|
688
|
+
`/** Typed client for the \`${domain}\` domain, bound to a connected holon.`,
|
|
689
|
+
` * Pass \`{ actor }\` (the verified principal uid) to author AS that user — every write seals it as`,
|
|
690
|
+
` * the captured principal, so the ReBAC \`.requires(...)\` gate + the integrity-dual endorse model`,
|
|
691
|
+
` * judge the real author. Omit it for open (un-gated) domains. */`,
|
|
692
|
+
`export function ${tsClientFactoryName(domain)}(holon: NomosHolon, opts: { domainHash?: string; actor?: string } = {}) {`,
|
|
693
|
+
` const domainHash = opts.domainHash ?? ${hashConst};`,
|
|
694
|
+
` const actor = opts.actor;`,
|
|
670
695
|
` return {`,
|
|
671
696
|
` domain: ${JSON.stringify(domain)} as const,`,
|
|
672
697
|
` domainHash,`,
|
|
698
|
+
...(opts.interface !== undefined
|
|
699
|
+
? [` interface: ${interfaceConstName(opts.packageName)}[${JSON.stringify(domain)}],`]
|
|
700
|
+
: []),
|
|
673
701
|
...methods,
|
|
674
702
|
` };`,
|
|
675
703
|
`}`,
|
|
@@ -30,13 +30,10 @@
|
|
|
30
30
|
* It emits into `outDir` (default `<config dir>/build`):
|
|
31
31
|
* * `<name>.package.usda` — the deployable package (engine bundle + USD IR);
|
|
32
32
|
* its sha256 IS the deploy `domainHash` (`policy:{hash}`).
|
|
33
|
-
* *
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* identityHashes}`: the EXACT body `POST /v1/workspaces/:ws/domains` accepts as
|
|
38
|
-
* `application/json` on Nomos Cloud (package + per-workspace manifest overlay in
|
|
39
|
-
* one deploy).
|
|
33
|
+
* * `<name>.deploy.json` — `{packageUsda}`: the EXACT body
|
|
34
|
+
* `POST /v1/workspaces/:ws/domains` accepts as `application/json` on Nomos Cloud.
|
|
35
|
+
* Admission/projection manifests are embedded in the package USDA and are not host
|
|
36
|
+
* sidecars.
|
|
40
37
|
* * `<name>.proof.mts` — the GENERATED PROOF (codegen_proof.ts): a
|
|
41
38
|
* runnable, domain-shaped live e2e synthesized from the law's own directives /
|
|
42
39
|
* queries / counts. Run it with `githolon proof`. Never fatal: an unsampleable
|
|
@@ -50,7 +47,7 @@
|
|
|
50
47
|
*/
|
|
51
48
|
import { execFileSync } from "node:child_process";
|
|
52
49
|
import { expandCapabilityExports, isCapabilityDecl } from "./capability_exports.js";
|
|
53
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
50
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
54
51
|
import { createRequire } from "node:module";
|
|
55
52
|
import path from "node:path";
|
|
56
53
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -106,6 +103,7 @@ import {
|
|
|
106
103
|
} from "./stable_ids.js";
|
|
107
104
|
import type { UsdLayer } from "./usd.js";
|
|
108
105
|
import { generateTsClient, tsClientFactoryName } from "./codegen_ts.js";
|
|
106
|
+
import { domainInterface, type DomainInterface } from "./shape_compat.js";
|
|
109
107
|
import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
|
|
110
108
|
|
|
111
109
|
const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -152,6 +150,8 @@ interface DomainConfig {
|
|
|
152
150
|
};
|
|
153
151
|
}
|
|
154
152
|
|
|
153
|
+
type DartSupportImport = "vendored" | "package";
|
|
154
|
+
|
|
155
155
|
interface PackageConfig {
|
|
156
156
|
readonly name: string;
|
|
157
157
|
readonly domains: readonly DomainConfig[];
|
|
@@ -162,13 +162,16 @@ interface PackageConfig {
|
|
|
162
162
|
* Typed DART frontend (the Flutter sibling of the generated TS client; #codegen_dart).
|
|
163
163
|
* `false`/absent = skip (default). `true` = emit into `<outDir>/dart/`;
|
|
164
164
|
* `{ out: "relative/dir" }` = emit there (resolved against the config dir).
|
|
165
|
+
* `{ support: "package" }` keeps `package:nomos_dsl/nomos_dsl.dart` imports for Flutter apps that
|
|
166
|
+
* already depend on `nomos_dsl`/`nomos_flutter`. The default `"vendored"` mode preserves the old
|
|
167
|
+
* self-contained directory.
|
|
165
168
|
* Per composed domain one `<name>.dart` (payload classes + read models + typed read
|
|
166
169
|
* accessors via the SAME `generateDartDomain` the co2 tenant emit uses), PLUS the
|
|
167
170
|
* framework `nomos_dsl` Dart support files vendored alongside (the generated files'
|
|
168
171
|
* `package:nomos_dsl/...` import is rewritten to a relative one) — so a Flutter app
|
|
169
172
|
* can vendor the directory as-is, no pub deps.
|
|
170
173
|
*/
|
|
171
|
-
readonly dart?: boolean | { readonly out?: string };
|
|
174
|
+
readonly dart?: boolean | { readonly out?: string; readonly support?: DartSupportImport };
|
|
172
175
|
/**
|
|
173
176
|
* OPT-IN layered USD emission (also `--layered` on the CLI). When true, ALSO emit
|
|
174
177
|
* `build/layers/<domain>--<module>.usda` (+ `.layer.json`) — one REAL USD layer per
|
|
@@ -406,7 +409,7 @@ const DART_RELATIVE_IMPORT = `import 'nomos_dsl.dart';`;
|
|
|
406
409
|
const DART_SUPPORT_SRC = [
|
|
407
410
|
"wire.dart",
|
|
408
411
|
"driver.dart",
|
|
409
|
-
"
|
|
412
|
+
"offer.dart",
|
|
410
413
|
"subscriptions.dart",
|
|
411
414
|
"provider.dart",
|
|
412
415
|
// The async WebView bridge runtime (NomosBridge / BridgeTransport) the generated
|
|
@@ -414,25 +417,41 @@ const DART_SUPPORT_SRC = [
|
|
|
414
417
|
"webview_bridge.dart",
|
|
415
418
|
] as const;
|
|
416
419
|
|
|
420
|
+
function removeVendoredDartSupport(dartOut: string): void {
|
|
421
|
+
rmSync(path.join(dartOut, "nomos_dsl.dart"), { force: true });
|
|
422
|
+
rmSync(path.join(dartOut, "src"), { recursive: true, force: true });
|
|
423
|
+
}
|
|
424
|
+
|
|
417
425
|
/**
|
|
418
426
|
* Emit the typed Dart for the composed domain modules into `dartOut`: one
|
|
419
427
|
* `<name>.dart` per domain (the SAME `generateDartDomain` path the co2 tenant's
|
|
420
428
|
* `emit_dart.ts` drives) + the vendored `nomos_dsl.dart` barrel and its `src/`
|
|
421
429
|
* support files, with the generated files' `package:` import rewritten relative.
|
|
422
430
|
*/
|
|
423
|
-
function emitDart(
|
|
424
|
-
|
|
431
|
+
function emitDart(
|
|
432
|
+
modules: readonly DomainModule[],
|
|
433
|
+
dartOut: string,
|
|
434
|
+
domainHash: string,
|
|
435
|
+
packageInterface: Record<string, DomainInterface> = {},
|
|
436
|
+
supportImport: DartSupportImport = "vendored",
|
|
437
|
+
): string[] {
|
|
438
|
+
mkdirSync(dartOut, { recursive: true });
|
|
425
439
|
|
|
426
|
-
// 1.
|
|
440
|
+
// 1. optional vendored framework support (barrel + src/), copied byte-for-byte except
|
|
427
441
|
// the barrel's schema_validation export (see DART_SUPPORT_SRC).
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
442
|
+
if (supportImport === "vendored") {
|
|
443
|
+
mkdirSync(path.join(dartOut, "src"), { recursive: true });
|
|
444
|
+
const libDir = path.join(DSL_DIR, "dart", "lib");
|
|
445
|
+
const barrel = readFileSync(path.join(libDir, "nomos_dsl.dart"), "utf8")
|
|
446
|
+
.split("\n")
|
|
447
|
+
.filter((line) => !line.includes("src/schema_validation.dart"))
|
|
448
|
+
.join("\n");
|
|
449
|
+
writeFileSync(path.join(dartOut, "nomos_dsl.dart"), barrel, "utf8");
|
|
450
|
+
for (const f of DART_SUPPORT_SRC) {
|
|
451
|
+
copyFileSync(path.join(libDir, "src", f), path.join(dartOut, "src", f));
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
removeVendoredDartSupport(dartOut);
|
|
436
455
|
}
|
|
437
456
|
|
|
438
457
|
// 2. one generated file per composed domain — the TYPES (`<name>.dart`) + the async bridge
|
|
@@ -453,10 +472,18 @@ function emitDart(modules: readonly DomainModule[], dartOut: string, domainHash:
|
|
|
453
472
|
);
|
|
454
473
|
}
|
|
455
474
|
const p = path.join(dartOut, `${mod.name}.dart`);
|
|
456
|
-
writeFileSync(
|
|
475
|
+
writeFileSync(
|
|
476
|
+
p,
|
|
477
|
+
supportImport === "vendored" ? src.replace(DART_PACKAGE_IMPORT, DART_RELATIVE_IMPORT) : src,
|
|
478
|
+
"utf8",
|
|
479
|
+
);
|
|
457
480
|
written.push(p);
|
|
458
481
|
|
|
459
|
-
const clientSrc = generateDartBridgeClient(modForDart, {
|
|
482
|
+
const clientSrc = generateDartBridgeClient(modForDart, {
|
|
483
|
+
domainHash,
|
|
484
|
+
interface: packageInterface[modForDart.domain ?? modForDart.name],
|
|
485
|
+
supportImport,
|
|
486
|
+
});
|
|
460
487
|
const cp = path.join(dartOut, `${mod.name}.client.dart`);
|
|
461
488
|
writeFileSync(cp, clientSrc, "utf8");
|
|
462
489
|
written.push(cp);
|
|
@@ -519,7 +546,7 @@ async function main(): Promise<void> {
|
|
|
519
546
|
capability: string;
|
|
520
547
|
domain: string;
|
|
521
548
|
aggregateId: string;
|
|
522
|
-
|
|
549
|
+
obligation: string;
|
|
523
550
|
complete: string;
|
|
524
551
|
fail: string;
|
|
525
552
|
block: string;
|
|
@@ -750,7 +777,7 @@ async function main(): Promise<void> {
|
|
|
750
777
|
capability: cap,
|
|
751
778
|
domain: d.key,
|
|
752
779
|
aggregateId: c.aggregate.id,
|
|
753
|
-
|
|
780
|
+
obligation: c.obligation.id,
|
|
754
781
|
complete: c.complete.id,
|
|
755
782
|
fail: c.fail.id,
|
|
756
783
|
block: c.block.id,
|
|
@@ -993,6 +1020,30 @@ async function main(): Promise<void> {
|
|
|
993
1020
|
// identity across clean clones). Deterministic — re-compiles rewrite the same bytes.
|
|
994
1021
|
writeFileSync(lockPath, JSON.stringify(nextLock, null, 2) + "\n", "utf8");
|
|
995
1022
|
|
|
1023
|
+
// ── THE STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates (read
|
|
1024
|
+
// shapes, sid-keyed), keyed by DOMAIN. The SAME object is (a) baked into the generated
|
|
1025
|
+
// clients as their requirement and (b) staged on the deploy as the workspace's offer, so
|
|
1026
|
+
// `connect()` negotiates compatibility by SHAPE (stable-id subtyping), not by law hash.
|
|
1027
|
+
// architecture/shape_compatibility.md. Multiple modules sharing a domain key merge.
|
|
1028
|
+
const packageInterface: Record<string, DomainInterface> = {};
|
|
1029
|
+
for (const mod of domainModules) {
|
|
1030
|
+
const key = mod.domain ?? mod.name;
|
|
1031
|
+
const di = domainInterface(mod, nextLock.domains[mod.name]?.stableIds);
|
|
1032
|
+
const prior = packageInterface[key];
|
|
1033
|
+
packageInterface[key] = prior
|
|
1034
|
+
? {
|
|
1035
|
+
...prior,
|
|
1036
|
+
directives: { ...prior.directives, ...di.directives },
|
|
1037
|
+
aggregates: { ...prior.aggregates, ...di.aggregates },
|
|
1038
|
+
}
|
|
1039
|
+
: di;
|
|
1040
|
+
}
|
|
1041
|
+
writeFileSync(
|
|
1042
|
+
path.join(outDir, `${cfg.name}.interface.json`),
|
|
1043
|
+
JSON.stringify(packageInterface, null, 2) + "\n",
|
|
1044
|
+
"utf8",
|
|
1045
|
+
);
|
|
1046
|
+
|
|
996
1047
|
// ── reports ──
|
|
997
1048
|
const entryReports: string[] = [];
|
|
998
1049
|
for (const [reportId, ref] of Object.entries(cfg.reports ?? {})) {
|
|
@@ -1252,7 +1303,8 @@ async function main(): Promise<void> {
|
|
|
1252
1303
|
relPaths.push(`layers/${base}.usda`);
|
|
1253
1304
|
}
|
|
1254
1305
|
}
|
|
1255
|
-
const
|
|
1306
|
+
const flat = flattenLayeredUsd(allLayers);
|
|
1307
|
+
const flatJson = JSON.stringify(flat);
|
|
1256
1308
|
if (flatJson !== usdJson) {
|
|
1257
1309
|
fail(
|
|
1258
1310
|
`layered emission does not flatten to the canonical composed IR — the module fold and ` +
|
|
@@ -1264,11 +1316,13 @@ async function main(): Promise<void> {
|
|
|
1264
1316
|
}
|
|
1265
1317
|
for (const w of pendingWrites) writeFileSync(w.path, w.text, "utf8");
|
|
1266
1318
|
layeredRootPath = path.join(outDir, `${cfg.name}.layered.usda`);
|
|
1267
|
-
writeFileSync(layeredRootPath, layeredRootUsda(relPaths), "utf8");
|
|
1319
|
+
writeFileSync(layeredRootPath, layeredRootUsda(relPaths, flat), "utf8");
|
|
1268
1320
|
layeredFileCount = relPaths.length;
|
|
1269
1321
|
}
|
|
1270
1322
|
|
|
1271
1323
|
// ── 3. the READ manifest + IDENTITY artifacts ──
|
|
1324
|
+
// These are generated from the same source functions embedded into USDA by emitUsd().
|
|
1325
|
+
// They remain local diagnostics for now; the deploy/cloud/runtime contract ignores them.
|
|
1272
1326
|
const readManifest = buildReadManifest(domainModules);
|
|
1273
1327
|
const readPath = path.join(outDir, "domain_manifests.json");
|
|
1274
1328
|
writeFileSync(readPath, JSON.stringify(readManifest, null, 2) + "\n", "utf8");
|
|
@@ -1276,7 +1330,7 @@ async function main(): Promise<void> {
|
|
|
1276
1330
|
const identity = buildIdentity(domainModules);
|
|
1277
1331
|
const { manifestsPath } = writeIdentity(identity, outDir);
|
|
1278
1332
|
|
|
1279
|
-
// ── 4. the one-call deploy body (package
|
|
1333
|
+
// ── 4. the one-call deploy body (package only) ──
|
|
1280
1334
|
// SELF-DESCRIBING: `summary` states what the (otherwise hex-opaque) package
|
|
1281
1335
|
// contains — purely derived from the inputs (no timestamps), ignored by the
|
|
1282
1336
|
// deploy endpoint, and mirrored as build/<name>.summary.txt for a human or
|
|
@@ -1297,13 +1351,13 @@ async function main(): Promise<void> {
|
|
|
1297
1351
|
const deployBody = {
|
|
1298
1352
|
summary,
|
|
1299
1353
|
packageUsda: text,
|
|
1300
|
-
readManifest,
|
|
1301
|
-
identityManifests: identity.manifests,
|
|
1302
|
-
identityHashes: identity.hashes,
|
|
1303
1354
|
// The capability lowering (unhashed sidecar; omit-when-empty — see the
|
|
1304
1355
|
// collector's doc above). The worker stages it on the manifest overlay:
|
|
1305
1356
|
// the deploy-time binding warning + the executor's scan map read it there.
|
|
1306
1357
|
...(capabilityLowerings.length > 0 ? { capabilities: capabilityLowerings } : {}),
|
|
1358
|
+
// THE STRUCTURAL OFFER (#72): the per-domain interface, staged on the manifest overlay so
|
|
1359
|
+
// `connect()` pulls the workspace's installed shapes and negotiates by structural subtyping.
|
|
1360
|
+
interface: packageInterface,
|
|
1307
1361
|
};
|
|
1308
1362
|
const deployPath = path.join(outDir, `${cfg.name}.deploy.json`);
|
|
1309
1363
|
writeFileSync(deployPath, JSON.stringify(deployBody) + "\n", "utf8");
|
|
@@ -1353,14 +1407,25 @@ async function main(): Promise<void> {
|
|
|
1353
1407
|
// ── 5. the typed TS client (#10 — the web sibling of codegen_dart), with the
|
|
1354
1408
|
// deployed law's content hash baked in (emitted AFTER the package is hashed) ──
|
|
1355
1409
|
const clientPath = path.join(outDir, `${cfg.name}.client.ts`);
|
|
1356
|
-
writeFileSync(clientPath, generateTsClient(domainModules, { packageName: cfg.name, domainHash }), "utf8");
|
|
1410
|
+
writeFileSync(clientPath, generateTsClient(domainModules, { packageName: cfg.name, domainHash, interface: packageInterface }), "utf8");
|
|
1357
1411
|
|
|
1358
1412
|
// ── 6. the typed Dart frontend (opt-in; the Flutter sibling of step 5, emitted
|
|
1359
1413
|
// after the package hash like the TS client) ──
|
|
1360
1414
|
const dartCfg = cfg.dart ?? false;
|
|
1361
|
-
if (
|
|
1362
|
-
|
|
1415
|
+
if (
|
|
1416
|
+
dartCfg !== false &&
|
|
1417
|
+
dartCfg !== true &&
|
|
1418
|
+
(typeof dartCfg !== "object" ||
|
|
1419
|
+
dartCfg === null ||
|
|
1420
|
+
(dartCfg.out !== undefined && typeof dartCfg.out !== "string") ||
|
|
1421
|
+
(dartCfg.support !== undefined && dartCfg.support !== "vendored" && dartCfg.support !== "package"))
|
|
1422
|
+
) {
|
|
1423
|
+
fail(
|
|
1424
|
+
`config key 'dart' must be false, true, or { out?: "relative/dir", support?: "vendored" | "package" }; got ${JSON.stringify(dartCfg)}`,
|
|
1425
|
+
);
|
|
1363
1426
|
}
|
|
1427
|
+
const dartSupportImport: DartSupportImport =
|
|
1428
|
+
typeof dartCfg === "object" && dartCfg.support !== undefined ? dartCfg.support : "vendored";
|
|
1364
1429
|
let dartOut: string | undefined;
|
|
1365
1430
|
let dartFiles: string[] = [];
|
|
1366
1431
|
if (dartCfg !== false) {
|
|
@@ -1368,7 +1433,7 @@ async function main(): Promise<void> {
|
|
|
1368
1433
|
cfgDir,
|
|
1369
1434
|
typeof dartCfg === "object" && dartCfg.out !== undefined ? dartCfg.out : path.join(outDir, "dart"),
|
|
1370
1435
|
);
|
|
1371
|
-
dartFiles = emitDart(domainModules, dartOut, domainHash);
|
|
1436
|
+
dartFiles = emitDart(domainModules, dartOut, domainHash, packageInterface, dartSupportImport);
|
|
1372
1437
|
}
|
|
1373
1438
|
|
|
1374
1439
|
// ── summary ──
|
|
@@ -1410,7 +1475,9 @@ async function main(): Promise<void> {
|
|
|
1410
1475
|
console.log(` proof SKIPPED — ${proofSkip}`);
|
|
1411
1476
|
}
|
|
1412
1477
|
if (dartOut !== undefined) {
|
|
1413
|
-
|
|
1478
|
+
const supportText =
|
|
1479
|
+
dartSupportImport === "vendored" ? "vendored nomos_dsl support" : "package nomos_dsl imports";
|
|
1480
|
+
console.log(` dart ${rel(dartOut)}${path.sep} (${dartFiles.length} domain file(s) + ${supportText} — typed Dart for Flutter)`);
|
|
1414
1481
|
}
|
|
1415
1482
|
console.log(` deploy ${rel(deployPath)}`);
|
|
1416
1483
|
console.log(``);
|
package/src/ctx.ts
CHANGED
|
@@ -14,11 +14,19 @@
|
|
|
14
14
|
* - ctx.rng -> Random port
|
|
15
15
|
*/
|
|
16
16
|
import type { WireHlc } from "./wire.js";
|
|
17
|
+
import { principalEvidence, type PrincipalEvidence } from "./principal_evidence.js";
|
|
17
18
|
|
|
18
19
|
export interface Ports {
|
|
19
20
|
clock(): WireHlc;
|
|
20
21
|
id(): string;
|
|
21
22
|
rng(): number;
|
|
23
|
+
/**
|
|
24
|
+
* THE IDENTITY PORT (security_model.md — the endorse model): the captured, gate-verified
|
|
25
|
+
* authoring principal as opaque {@link PrincipalEvidence}. NOT a string — it can only become an
|
|
26
|
+
* attribution or an ENDORSED authority/standing value via its conversions, so identity cannot
|
|
27
|
+
* decide authority in plan code (the confused-deputy defence). Replay-stable (a captured port).
|
|
28
|
+
*/
|
|
29
|
+
principal(): PrincipalEvidence;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
/** A deterministic stub set of ports — seeded, replayable. */
|
|
@@ -27,6 +35,7 @@ export function deterministicPorts(seed: {
|
|
|
27
35
|
logical?: number;
|
|
28
36
|
replica?: number;
|
|
29
37
|
idPrefix?: string;
|
|
38
|
+
principal?: string;
|
|
30
39
|
} = {}): Ports {
|
|
31
40
|
let physical = seed.physical ?? 1;
|
|
32
41
|
let logical = seed.logical ?? 0;
|
|
@@ -34,7 +43,11 @@ export function deterministicPorts(seed: {
|
|
|
34
43
|
const idPrefix = seed.idPrefix ?? "id";
|
|
35
44
|
let idCounter = 0;
|
|
36
45
|
let rngState = (seed.physical ?? 1) >>> 0 || 1;
|
|
46
|
+
const evidence = principalEvidence(seed.principal ?? "");
|
|
37
47
|
return {
|
|
48
|
+
principal(): PrincipalEvidence {
|
|
49
|
+
return evidence;
|
|
50
|
+
},
|
|
38
51
|
clock(): WireHlc {
|
|
39
52
|
const h: WireHlc = { physical, logical, replica };
|
|
40
53
|
// Advance: same physical -> bump logical (mirrors merge.md local-event rule).
|
package/src/engine_entry.ts
CHANGED
|
@@ -57,6 +57,7 @@ import { RelationTuple } from "./framework/relations.js";
|
|
|
57
57
|
import type { Directive } from "./directive.js";
|
|
58
58
|
import type { AggregateHandle, AggregateInvariantFn, AggregateInvariantVerdict } from "./aggregate.js";
|
|
59
59
|
import type { Ports } from "./ctx.js";
|
|
60
|
+
import { principalEvidence, type PrincipalEvidence } from "./principal_evidence.js";
|
|
60
61
|
import type { WireEvent, WireHlc } from "./wire.js";
|
|
61
62
|
import type { DerivedDecl } from "./derived.js";
|
|
62
63
|
import type { CombinedDecl } from "./combined.js";
|
|
@@ -365,12 +366,18 @@ export function collectEngineRouting(
|
|
|
365
366
|
* intent's HLC — the committed HLC/id is host-injected in Rust); `id()` derives a
|
|
366
367
|
* deterministic per-dispatch value; `rng()` replays the captured draw.
|
|
367
368
|
*/
|
|
368
|
-
function portsFromHost(): Ports {
|
|
369
|
+
function portsFromHost(principalRaw = ""): Ports {
|
|
369
370
|
const hostPorts = (globalThis as { __ports?: { clock(): number; rng(): number } }).__ports;
|
|
370
371
|
const clockScalar = hostPorts ? hostPorts.clock() : 0;
|
|
371
372
|
const rngValue = hostPorts ? hostPorts.rng() : 0;
|
|
372
373
|
let idCounter = 0;
|
|
374
|
+
// The captured, gate-verified principal (the engine serialises it into the dispatch job —
|
|
375
|
+
// host.rs EnginePortsSnapshot.principal) becomes the opaque PrincipalEvidence the plan reads.
|
|
376
|
+
const evidence = principalEvidence(principalRaw);
|
|
373
377
|
return {
|
|
378
|
+
principal(): PrincipalEvidence {
|
|
379
|
+
return evidence;
|
|
380
|
+
},
|
|
374
381
|
clock(): WireHlc {
|
|
375
382
|
return { physical: clockScalar, logical: 0, replica: 0 };
|
|
376
383
|
},
|
|
@@ -386,7 +393,7 @@ function portsFromHost(): Ports {
|
|
|
386
393
|
|
|
387
394
|
/** The compiled engine surface `registerEngine` returns (and assigns onto `globalThis`). */
|
|
388
395
|
export interface RegisteredEngine {
|
|
389
|
-
plan(job: unknown): WireEvent[] | { events: WireEvent[]; strikes: string[] };
|
|
396
|
+
plan(job: unknown): WireEvent[] | { events: WireEvent[]; strikes?: string[]; endorse?: { sink: string; facet: string; reason: string | null }[] };
|
|
390
397
|
planReport(job: unknown): string;
|
|
391
398
|
}
|
|
392
399
|
|
|
@@ -709,7 +716,8 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
709
716
|
|
|
710
717
|
function plan(job: {
|
|
711
718
|
intent?: { domain?: string; directiveId?: string; payload?: unknown };
|
|
712
|
-
|
|
719
|
+
capturedPorts?: { principal?: string };
|
|
720
|
+
}): WireEvent[] | { events: WireEvent[]; strikes?: string[]; endorse?: { sink: string; facet: string; reason: string | null }[] } {
|
|
713
721
|
const intent = job.intent ?? {};
|
|
714
722
|
const domain = intent.domain;
|
|
715
723
|
const directiveId = intent.directiveId;
|
|
@@ -723,11 +731,20 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
723
731
|
if (entry === undefined) {
|
|
724
732
|
throw new Error(`engine plan: no directive registered for (${domain}, ${directiveId})`);
|
|
725
733
|
}
|
|
726
|
-
const ctx = portsFromHost();
|
|
734
|
+
const ctx = portsFromHost(job.capturedPorts?.principal);
|
|
727
735
|
const wire = executeDirectiveToIntent(entry.directive, entry.agg, intent.payload as never, ctx);
|
|
728
736
|
// Emit {events, strikes} ONLY when a strike is present, else the bare event array —
|
|
729
737
|
// so every strikes-free directive canonicalizes byte-identically to the pre-strikeout contract.
|
|
730
|
-
|
|
738
|
+
// The OBSERVED ENDORSE footprint (Slice B) rides the object form too — TRANSIENT (the gate reads
|
|
739
|
+
// it to check the law's nomosFlowGate.endorse; decode_plan_output ignores it for the byte-compare),
|
|
740
|
+
// so a principal-free intent still emits the bare array, byte-identical.
|
|
741
|
+
const endorse = wire.endorse;
|
|
742
|
+
if (!wire.strikes.length && !(endorse && endorse.length)) return wire.events;
|
|
743
|
+
return {
|
|
744
|
+
events: wire.events,
|
|
745
|
+
...(wire.strikes.length ? { strikes: wire.strikes } : {}),
|
|
746
|
+
...(endorse && endorse.length ? { endorse } : {}),
|
|
747
|
+
};
|
|
731
748
|
}
|
|
732
749
|
|
|
733
750
|
// The engine evals the bundle as a classic script and FREEZES globalThis before
|
package/src/fields.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { Conflict, Counter, LastPosition, Lww, type Driver } from "./drivers.js";
|
|
19
19
|
import type { AggregateHandle } from "./aggregate.js";
|
|
20
|
+
import type { FieldFacet } from "./principal_evidence.js";
|
|
20
21
|
|
|
21
22
|
export type FieldKind =
|
|
22
23
|
| "string"
|
|
@@ -143,8 +144,20 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
|
|
143
144
|
* Flag named `isMeta` — the `meta()` METHOD sets it, mirroring `encrypted()`/`optional()`.
|
|
144
145
|
*/
|
|
145
146
|
readonly isMeta?: true;
|
|
147
|
+
/**
|
|
148
|
+
* THE INTEGRITY/CONFIDENTIALITY FACET (security_model.md — the endorse model). Set by the
|
|
149
|
+
* `authorityField`/`attributionField`/`standingField`/`secretField`/`publicField` wrappers (or the
|
|
150
|
+
* `.facet()` sugar). Drives `set()` acceptance of PrincipalEvidence: an `authority`/`standing` sink
|
|
151
|
+
* accepts only the matching ENDORSED conversion (never un-endorsed identity), so the confused deputy
|
|
152
|
+
* is unrepresentable. OMITTED on a plain field (accepts a plain value). The field's READ/wire type is
|
|
153
|
+
* unchanged — the facet governs WRITES, not what's stored.
|
|
154
|
+
*/
|
|
155
|
+
readonly facet?: FieldFacet;
|
|
146
156
|
/** Returns a copy with the chosen driver (kind literal preserved). */
|
|
147
157
|
merge(driver: Driver): Field<T, K>;
|
|
158
|
+
/** Tag this field's facet, threaded through merge/optional (the canonical form is the
|
|
159
|
+
* `authorityField(...)`/`attributionField(...)`/… wrappers, which call this). */
|
|
160
|
+
withFacet<Fa extends FieldFacet>(facet: Fa): Field<T, K> & { readonly facet: Fa };
|
|
148
161
|
/** Mark this field's value ENCRYPTED (sets {@link Field.isEncrypted}). Chainable + non-destructive. */
|
|
149
162
|
encrypted(): Field<T, K>;
|
|
150
163
|
/** Mark this field plaintext METADATA (sets {@link Field.isMeta}) — the indexable opt-out on an owned aggregate. */
|
|
@@ -173,6 +186,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
173
186
|
jsonShape?: "object";
|
|
174
187
|
encrypted?: true;
|
|
175
188
|
meta?: true;
|
|
189
|
+
facet?: FieldFacet;
|
|
176
190
|
} = {},
|
|
177
191
|
): Field<T, K> {
|
|
178
192
|
const isOptional = extra.isOptional ?? false;
|
|
@@ -181,6 +195,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
181
195
|
zod,
|
|
182
196
|
driver,
|
|
183
197
|
isOptional,
|
|
198
|
+
...(extra.facet !== undefined ? { facet: extra.facet } : {}),
|
|
184
199
|
...(extra.refWorkspace !== undefined ? { refWorkspace: extra.refWorkspace } : {}),
|
|
185
200
|
...(extra.refAggregateId !== undefined ? { refAggregateId: extra.refAggregateId } : {}),
|
|
186
201
|
...(extra.viaField !== undefined ? { viaField: extra.viaField } : {}),
|
|
@@ -211,10 +226,39 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
211
226
|
// Driver/schema unchanged; the fact rides the aggregate's audience surface, not the wire kind.
|
|
212
227
|
return makeField<T, K>(kind, zod, driver, { ...extra, meta: true });
|
|
213
228
|
},
|
|
229
|
+
withFacet<Fa extends FieldFacet>(facet: Fa): Field<T, K> & { readonly facet: Fa } {
|
|
230
|
+
// The facet governs WRITES (set() acceptance), never the wire/read type — so the schema +
|
|
231
|
+
// driver are unchanged. Threaded through merge/optional (it rides `extra`).
|
|
232
|
+
return makeField<T, K>(kind, zod, driver, { ...extra, facet }) as Field<T, K> & { readonly facet: Fa };
|
|
233
|
+
},
|
|
214
234
|
};
|
|
215
235
|
return f;
|
|
216
236
|
}
|
|
217
237
|
|
|
238
|
+
// ── FIELD FACETS (security_model.md — the endorse model) ─────────────────────────────────────
|
|
239
|
+
// Canonical wrappers that tag a field's facet; `set()` then gates which PrincipalEvidence
|
|
240
|
+
// conversions it accepts. The field's read/wire type is unchanged — only WRITES are governed.
|
|
241
|
+
|
|
242
|
+
/** An AUTHORITY sink (relation subject/relation/object, token audience/scope, policy/role/identity
|
|
243
|
+
* write). Accepts a plain value OR `principal().asAuthoritySubject(reason)` — never un-endorsed identity. */
|
|
244
|
+
export const authorityField = <T, K extends FieldKind>(field: Field<T, K>): Field<T, K> & { readonly facet: "authority" } =>
|
|
245
|
+
field.withFacet("authority");
|
|
246
|
+
/** A STANDING sink (accountability: owedBy/owedTo/responsible/issuer/escalation). Accepts a plain
|
|
247
|
+
* value OR `principal().asStandingParty(reason)`. */
|
|
248
|
+
export const standingField = <T, K extends FieldKind>(field: Field<T, K>): Field<T, K> & { readonly facet: "standing" } =>
|
|
249
|
+
field.withFacet("standing");
|
|
250
|
+
/** An ATTRIBUTION sink (audit: createdBy/grantedBy/submittedBy). Accepts a plain value OR
|
|
251
|
+
* `principal().asAttribution()` — free, no endorsement. Promote to `authorityField` if law later
|
|
252
|
+
* reads it to decide authority. */
|
|
253
|
+
export const attributionField = <T, K extends FieldKind>(field: Field<T, K>): Field<T, K> & { readonly facet: "attribution" } =>
|
|
254
|
+
field.withFacet("attribution");
|
|
255
|
+
/** A confidentiality SOURCE (the read-down lattice). */
|
|
256
|
+
export const secretField = <T, K extends FieldKind>(field: Field<T, K>): Field<T, K> & { readonly facet: "secret" } =>
|
|
257
|
+
field.withFacet("secret");
|
|
258
|
+
/** Explicitly PUBLIC (⊤) — the read-down lattice top. */
|
|
259
|
+
export const publicField = <T, K extends FieldKind>(field: Field<T, K>): Field<T, K> & { readonly facet: "public" } =>
|
|
260
|
+
field.withFacet("public");
|
|
261
|
+
|
|
218
262
|
/** Infer the authored value type of a Field. */
|
|
219
263
|
export type FieldValue<F> = F extends Field<infer T> ? T : never;
|
|
220
264
|
|