@githolon/dsl 0.114.0 → 0.116.2
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/assets/operation-contract.wasm +0 -0
- package/dart/CHANGELOG.md +55 -0
- package/dart/lib/src/application_framework_calls.dart +7 -1
- package/dart/lib/src/console_enrolment.dart +7 -0
- package/dart/lib/src/console_http_io.dart +22 -15
- package/dart/lib/src/dev_cloud.dart +62 -0
- package/dart/lib/src/nms_generated_runtime.dart +1 -0
- package/dart/lib/src/operation_receipts.dart +74 -7
- package/dart/lib/src/reception/dart_reception.dart +26 -6
- package/dart/lib/src/result_usda.dart +64 -27
- package/dart/lib/src/runner/node-entry.mjs +4733 -1534
- package/dart/lib/src/runtime_v2_transport.dart +3 -0
- package/dart/lib/src/sync_status_projection.dart +11 -0
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/console_enrolment_test.dart +47 -0
- package/dart/test/kernel_refused_follow_test.dart +36 -0
- package/dart/test/model_adoption_test.dart +49 -0
- package/dart/test/result_usda_test.dart +67 -0
- package/dart/test/runtime_projections_test.dart +41 -0
- package/dart_types/CHANGELOG.md +30 -0
- package/dart_types/lib/src/approved_model.dart +78 -1
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +528 -2
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +24 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +1897 -112
- package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +98 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +11 -1
- package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +16 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +43 -2
- package/dart_types/lib/src/vo_canonical.dart +185 -0
- package/dart_types/pubspec.lock +1 -1
- package/dart_types/pubspec.yaml +3 -1
- package/dart_types/test/vo_canonical_test.dart +91 -0
- package/package.json +4 -2
- package/src/aggregate.ts +95 -0
- package/src/anonymous_value_interning.ts +205 -0
- package/src/baseline.ts +27 -0
- package/src/baseline_export.ts +432 -0
- package/src/baseline_import.ts +312 -0
- package/src/baseline_usda.ts +185 -0
- package/src/birth_key_rule.ts +472 -0
- package/src/births.ts +207 -24
- package/src/build_package.ts +47 -7
- package/src/build_provenance.ts +10 -0
- package/src/business_birth.ts +50 -4
- package/src/check_package_main.ts +50 -6
- package/src/codegen_call_proto.ts +5 -18
- package/src/codegen_call_usda.ts +6 -3
- package/src/codegen_dart.ts +114 -60
- package/src/codegen_dart_client.ts +52 -6
- package/src/codegen_dart_facade.ts +57 -35
- package/src/codegen_dart_framework_calls.ts +20 -1
- package/src/codegen_proof.ts +5 -2
- package/src/codegen_result_proto.ts +8 -6
- package/src/codegen_result_usda.ts +12 -4
- package/src/codegen_ts.ts +109 -10
- package/src/compatibility_graph.ts +41 -4
- package/src/compatibility_usda.ts +11 -1
- package/src/compile_package_main.ts +222 -52
- package/src/composed_wire_identity.ts +24 -0
- package/src/directive.ts +6 -0
- package/src/domain_requirements.ts +75 -0
- package/src/engine_entry.ts +12 -4
- package/src/engine_javascript.ts +12 -6
- package/src/fields.ts +109 -0
- package/src/identity_usda.ts +80 -103
- package/src/index.ts +35 -1
- package/src/intent.ts +33 -3
- package/src/law_check.ts +34 -0
- package/src/law_graph.ts +142 -1
- package/src/law_graph_usda.ts +27 -144
- package/src/law_layer_prims.ts +174 -0
- package/src/law_unit_selection.ts +45 -0
- package/src/law_units.ts +7 -6
- package/src/law_version.ts +108 -0
- package/src/package_config.ts +44 -0
- package/src/package_dependencies.ts +8 -0
- package/src/point_guard.ts +5 -1
- package/src/release_inspection.ts +6 -4
- package/src/release_publication.ts +6 -5
- package/src/usda_text.ts +78 -3
- package/src/value_identity.ts +355 -0
- package/src/value_object.ts +35 -4
- package/src/wire.ts +2 -0
- package/src/wire_paths.ts +17 -0
- package/src/workspace_routing.ts +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PER-DOMAIN UNIT REQUIREMENTS (CAP-666): what a generated client pins beside the unit set.
|
|
3
|
+
*
|
|
4
|
+
* An application is compiled against one law, named by its unit set (CAP-773). That one identity is
|
|
5
|
+
* what a workspace serves as a whole, so an app pinned to it alone goes stale the moment a rollout moves
|
|
6
|
+
* a domain the app never touches. Each application domain has its own unit in `build/<name>.units`
|
|
7
|
+
* (`domain:<key>`) and its own slice of the generated compatibility requirement
|
|
8
|
+
* (`<name>.compatibility.usda`, the `NomosCompatibilityDomain` the kernel's Compatibility Desk judges).
|
|
9
|
+
* This module names both per domain key, from the same in-memory units and interfaces the compiler
|
|
10
|
+
* just emitted, so the generated Dart/TS clients, `NomosApprovedModel`, the model-adoption desk and the
|
|
11
|
+
* client inventory all carry one contract per domain and never infer it from installed law.
|
|
12
|
+
*
|
|
13
|
+
* Era rule: the requirement is derived from existing artifacts and written into no law, unit or
|
|
14
|
+
* package. Unchanged law keeps every identity it had; only the generated client gains the table.
|
|
15
|
+
*/
|
|
16
|
+
import { emitApplicationCompatibilityUsda } from "./compatibility_usda.js";
|
|
17
|
+
import type { DomainInterface } from "./compatibility_graph.js";
|
|
18
|
+
import type { LawUnit } from "./law_units.js";
|
|
19
|
+
|
|
20
|
+
export interface DomainUnitRequirement {
|
|
21
|
+
/** The application domain key. */
|
|
22
|
+
readonly domainKey: string;
|
|
23
|
+
/** The hash of its law unit (`domain:<key>` in the unit set), 64 lowercase hex. */
|
|
24
|
+
readonly unitSha256: string;
|
|
25
|
+
/** sha256 of the domain's own compatibility requirement USDA (the one-domain `emitApplicationCompatibilityUsda`). */
|
|
26
|
+
readonly requirementSha256: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
30
|
+
|
|
31
|
+
/** The USDA requirement for exactly one domain: the slice a client carries and the kernel judges. */
|
|
32
|
+
export function domainRequirementUsda(domainKey: string, domainInterface: DomainInterface): string {
|
|
33
|
+
return emitApplicationCompatibilityUsda({ [domainKey]: domainInterface });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One requirement per application domain the package exposes an interface for, sorted by domain key. A
|
|
38
|
+
* domain unit without an interface (a private or framework domain) names no requirement and is omitted;
|
|
39
|
+
* an interface whose domain has no unit is a compiler defect and refuses.
|
|
40
|
+
*/
|
|
41
|
+
export function domainRequirementsOf(
|
|
42
|
+
units: readonly Pick<LawUnit, "key" | "kind" | "domain" | "hash">[],
|
|
43
|
+
interfaces: Readonly<Record<string, DomainInterface>>,
|
|
44
|
+
sha256Hex: (utf8: string) => string,
|
|
45
|
+
): DomainUnitRequirement[] {
|
|
46
|
+
const unitByDomain = new Map<string, string>();
|
|
47
|
+
for (const unit of units) {
|
|
48
|
+
if (unit.kind === "domain" && unit.domain !== undefined) unitByDomain.set(unit.domain, unit.hash);
|
|
49
|
+
}
|
|
50
|
+
return Object.keys(interfaces).sort().map((domainKey) => {
|
|
51
|
+
const unitSha256 = unitByDomain.get(domainKey);
|
|
52
|
+
if (unitSha256 === undefined) {
|
|
53
|
+
throw new Error(`domain requirements: '${domainKey}' has a compatibility interface but no law unit; the unit split and the interface disagree`);
|
|
54
|
+
}
|
|
55
|
+
const requirementSha256 = sha256Hex(domainRequirementUsda(domainKey, interfaces[domainKey]!));
|
|
56
|
+
for (const [name, value] of [["unit", unitSha256], ["requirement", requirementSha256]] as const) {
|
|
57
|
+
if (!HEX64.test(value)) throw new Error(`domain requirements: '${domainKey}' ${name} identity must be 64 lowercase hex, not '${value}'`);
|
|
58
|
+
}
|
|
59
|
+
return { domainKey, unitSha256, requirementSha256 };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Refuse a hand-supplied table that is not what the compiler would emit (generators take it as an input). */
|
|
64
|
+
export function assertDomainRequirements(requirements: readonly DomainUnitRequirement[]): void {
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
for (const requirement of requirements) {
|
|
67
|
+
if (!requirement.domainKey || seen.has(requirement.domainKey)) {
|
|
68
|
+
throw new Error(`domain requirements: '${requirement.domainKey}' is empty or named twice`);
|
|
69
|
+
}
|
|
70
|
+
seen.add(requirement.domainKey);
|
|
71
|
+
for (const [name, value] of [["unit", requirement.unitSha256], ["requirement", requirement.requirementSha256]] as const) {
|
|
72
|
+
if (!HEX64.test(value)) throw new Error(`domain requirements: '${requirement.domainKey}' ${name} identity must be 64 lowercase hex, not '${value}'`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/engine_entry.ts
CHANGED
|
@@ -93,7 +93,8 @@ import {
|
|
|
93
93
|
assertApplicationLanguageCoversAuthoredDirectives,
|
|
94
94
|
executableDirectivesForLanguage,
|
|
95
95
|
publicOfferRoute,
|
|
96
|
-
|
|
96
|
+
carryWorkspaceInvariantsToPerformingIntents,
|
|
97
|
+
isBusinessIntent,
|
|
97
98
|
} from "./intent.js";
|
|
98
99
|
|
|
99
100
|
/** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
|
|
@@ -511,9 +512,16 @@ function buildDomainSlice(mod: DomainModuleExports, domain: string): DomainSlice
|
|
|
511
512
|
declaredWorkspaceInvariants.push(w);
|
|
512
513
|
}
|
|
513
514
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
515
|
+
// CAP-863: the SAME carry rule package composition applies, with the same keep-the-original predicate —
|
|
516
|
+
// an effect's invariant is registered for every routable intent that performs it, every original is kept
|
|
517
|
+
// without a language, and a language keeps an original only while its target is still dispatchable. The
|
|
518
|
+
// registered set feeds the routing table, so it must equal the manifest's set: an extra body would emit a
|
|
519
|
+
// dead invariant route into language law, a missing one would fail closed at dispatch.
|
|
520
|
+
for (const w of carryWorkspaceInvariantsToPerformingIntents(
|
|
521
|
+
declaredWorkspaceInvariants,
|
|
522
|
+
[...dirs.values()].filter(isBusinessIntent),
|
|
523
|
+
(directiveId) => applicationLanguage === undefined || dirs.has(directiveId),
|
|
524
|
+
)) {
|
|
517
525
|
wsInvariants.set(w.id, w);
|
|
518
526
|
}
|
|
519
527
|
// ── DESUGARED write-path preconditions (`.requiresExisting`/`.creates(Agg,idFn)`/`.where`) ──
|
package/src/engine_javascript.ts
CHANGED
|
@@ -2,21 +2,27 @@
|
|
|
2
2
|
* Canonicalise esbuild's non-semantic install-topology noise before the executable law is hashed.
|
|
3
3
|
*
|
|
4
4
|
* A workspace dependency is labelled `../../dsl/src/...`; the same published dependency is labelled
|
|
5
|
-
* `node_modules/@githolon/dsl/src/...`.
|
|
5
|
+
* `node_modules/@githolon/dsl/src/...`. A dependency reached through a symlinked `node_modules` (a git
|
|
6
|
+
* worktree links the main checkout's) is labelled by its real path, climbing out and back down through
|
|
7
|
+
* another directory: `../../../../platform/nomos2/node_modules/zod/...`. Everything up to the last
|
|
8
|
+
* `node_modules/` in an esbuild label describes the install, never the law. Esbuild can also leave either one or two newlines after its
|
|
6
9
|
* `"use strict"` prologue. None of those bytes describe business law, so they must not give the same
|
|
7
10
|
* law two identities.
|
|
8
11
|
*
|
|
9
12
|
* Replacements are deliberately restricted to esbuild's generated comment/function labels. Tenant
|
|
10
13
|
* strings and executable imports remain byte-significant.
|
|
11
14
|
*/
|
|
15
|
+
/** An esbuild label's install prefix: an optional climb (`../`), any directories, then `node_modules/`. */
|
|
16
|
+
const INSTALLED = String.raw`(?:(?:\.\.\/)+(?:[^\s"/]+\/)*?)?node_modules\/`;
|
|
17
|
+
|
|
12
18
|
export function canonicalizeEngineJavascript(source: string): string {
|
|
13
19
|
const body = source
|
|
14
20
|
.replace(/\r\n?/g, "\n")
|
|
15
21
|
.replace(/^[ \t]*"use strict";[ \t]*(?:\n|$)/gm, "")
|
|
16
|
-
.replace(
|
|
17
|
-
.replace(
|
|
18
|
-
.replace(
|
|
19
|
-
.replace(
|
|
22
|
+
.replace(new RegExp(`^(\\s*\\/\\/ )(?:(?:\\.\\.\\/)+dsl\\/src\\/|${INSTALLED}@githolon\\/dsl\\/src\\/)`, "gm"), "$1@githolon/dsl/src/")
|
|
23
|
+
.replace(new RegExp(`"(?:(?:\\.\\.\\/)+dsl\\/src\\/|${INSTALLED}@githolon\\/dsl\\/src\\/)([^"]+)"\\(\\) \\{`, "g"), '"@githolon/dsl/src/$1"() {')
|
|
24
|
+
.replace(new RegExp(`^(\\s*\\/\\/ )${INSTALLED}zod\\/`, "gm"), "$1zod/")
|
|
25
|
+
.replace(new RegExp(`"${INSTALLED}zod\\/([^"]+)"\\(\\) \\{`, "g"), '"zod/$1"() {')
|
|
20
26
|
.replace(/\n[ \t]*\n/g, "\n")
|
|
21
27
|
.trim();
|
|
22
28
|
return `"use strict";\n${body}\n`;
|
|
@@ -25,7 +31,7 @@ export function canonicalizeEngineJavascript(source: string): string {
|
|
|
25
31
|
/** Remove the same install-topology label from the sealed generated application bundle. */
|
|
26
32
|
export function canonicalizeGeneratedClientJavascript(source: string): string {
|
|
27
33
|
return source.replace(
|
|
28
|
-
|
|
34
|
+
new RegExp(`^(\\s*\\/\\/ )(?:(?:\\.\\.\\/)+dsl\\/runtime\\/|${INSTALLED}@githolon\\/dsl\\/runtime\\/)`, "gm"),
|
|
29
35
|
"$1@githolon/dsl/runtime/",
|
|
30
36
|
);
|
|
31
37
|
}
|
package/src/fields.ts
CHANGED
|
@@ -243,6 +243,97 @@ export function isGitObjectIdSchema(schema: z.ZodTypeAny): boolean {
|
|
|
243
243
|
return false;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
// ── TYPED IDS (aggregate fields) ─────────────────────────────────────────────────────────────────────
|
|
247
|
+
// An identifier is never a plain string. `t.brandedId(Name)` and `t.uuid()` are the aggregate-field forms of
|
|
248
|
+
// the payload-side `brandedId(...)` schema: wire-identical to a string, nominal at compile time (a plain
|
|
249
|
+
// `string` is not assignable), and recognised by codegen, which emits the branded type in both generated
|
|
250
|
+
// clients. `t.ref(Aggregate)` is the aggregate-field form of `idOf(Aggregate)`. A birth key MUST be one of
|
|
251
|
+
// these (see `births.ts`).
|
|
252
|
+
|
|
253
|
+
declare const NOMOS_TYPED_ID: unique symbol;
|
|
254
|
+
/** A nominal id: a string at run time and on the wire, a distinct type per `Brand` at compile time. */
|
|
255
|
+
export type TypedId<Brand extends string> = string & { readonly [NOMOS_TYPED_ID]: Brand };
|
|
256
|
+
/** The brand a `t.uuid()` field carries. */
|
|
257
|
+
export type Uuid = TypedId<"Uuid">;
|
|
258
|
+
|
|
259
|
+
/** The same global symbol `value_object.ts` stamps on `brandedId(...)` payload schemas (kept as `Symbol.for`
|
|
260
|
+
* here because value_object.ts imports this module). */
|
|
261
|
+
const BRANDED_ID_SYMBOL: unique symbol = Symbol.for("nomos.brandedId") as never;
|
|
262
|
+
/** Marks a `t.uuid()` schema, so a birth key can be classified as a uuid without reading names. */
|
|
263
|
+
export const NOMOS_UUID_BRAND: unique symbol = Symbol.for("nomos.uuid");
|
|
264
|
+
|
|
265
|
+
const TYPED_ID_BRAND_RE = /^[A-Z][A-Za-z0-9_]*$/;
|
|
266
|
+
/** Canonical lowercase RFC 9562 text form (any version) — the one spelling a `t.uuid()` admits. */
|
|
267
|
+
export const CANONICAL_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
268
|
+
/** The longest id (UTF-8 bytes) a typed id admits — mirrored by the kernel's birth-key shape check. */
|
|
269
|
+
export const TYPED_ID_MAX_BYTES = 256;
|
|
270
|
+
|
|
271
|
+
/** Why `value` is not a well-formed typed id value (`undefined` when it is). One rule set, mirrored by
|
|
272
|
+
* the kernel (`kernel/src/birth_stage.rs` `typed_birth_key_refusal`): a non-empty string of at most
|
|
273
|
+
* {@link TYPED_ID_MAX_BYTES} UTF-8 bytes with no control character (U+0000–U+001F, U+007F — which also keeps
|
|
274
|
+
* the unit separator that delimits `(namespace, key)` out of every key). */
|
|
275
|
+
export function typedIdShapeRefusal(value: unknown): string | undefined {
|
|
276
|
+
if (typeof value !== "string") return `an id is a string; got ${value === null ? "null" : typeof value}`;
|
|
277
|
+
if (value.length === 0) return "an id is non-empty";
|
|
278
|
+
if (utf8ByteLength(value) > TYPED_ID_MAX_BYTES) {
|
|
279
|
+
return `an id is at most ${TYPED_ID_MAX_BYTES} UTF-8 bytes`;
|
|
280
|
+
}
|
|
281
|
+
// eslint-disable-next-line no-control-regex
|
|
282
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) return "an id carries no control character";
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** UTF-8 length of a JS string, counted by code point. The sealed engine (QuickJS) has no TextEncoder, and
|
|
287
|
+
* this rule runs inside law plans and payload validation there. A lone surrogate counts 3 bytes (U+FFFD). */
|
|
288
|
+
function utf8ByteLength(value: string): number {
|
|
289
|
+
let bytes = 0;
|
|
290
|
+
for (const char of value) {
|
|
291
|
+
const code = char.codePointAt(0)!;
|
|
292
|
+
bytes += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
|
|
293
|
+
}
|
|
294
|
+
return bytes;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function typedIdSchema(): z.ZodString {
|
|
298
|
+
return z.string().superRefine((value, ctx) => {
|
|
299
|
+
const refusal = typedIdShapeRefusal(value);
|
|
300
|
+
if (refusal !== undefined) ctx.addIssue({ code: "custom", message: refusal });
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Make a typed id VALUE of `brand` from its wire text, checking the shared shape. This is the one
|
|
306
|
+
* sanctioned door from text to an id (a constant the law owns, a value read off another system); a
|
|
307
|
+
* `"Uuid"` brand also requires the canonical uuid spelling. Never cast a string to {@link TypedId}.
|
|
308
|
+
*/
|
|
309
|
+
export function typedId<const Brand extends string>(brand: Brand, value: string): TypedId<Brand> {
|
|
310
|
+
if (!TYPED_ID_BRAND_RE.test(brand)) {
|
|
311
|
+
throw new Error(`typedId: '${brand}' must be a capitalized identifier (the brand's type name)`);
|
|
312
|
+
}
|
|
313
|
+
const refusal = typedIdShapeRefusal(value);
|
|
314
|
+
if (refusal !== undefined) throw new Error(`typedId(${brand}): ${refusal}`);
|
|
315
|
+
if (brand === "Uuid" && !CANONICAL_UUID_RE.test(value)) {
|
|
316
|
+
throw new Error("typedId(Uuid): expected a canonical lowercase uuid");
|
|
317
|
+
}
|
|
318
|
+
return value as TypedId<Brand>;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Whether a schema is a `t.uuid()` id (through omission/null wrappers). */
|
|
322
|
+
export function isUuidIdSchema(schema: z.ZodTypeAny): boolean {
|
|
323
|
+
let current: z.ZodTypeAny | undefined = schema;
|
|
324
|
+
for (let hops = 0; current !== undefined && hops < 16; hops++) {
|
|
325
|
+
if ((current as unknown as Record<symbol, unknown>)[NOMOS_UUID_BRAND] === true) return true;
|
|
326
|
+
const raw = current as unknown as {
|
|
327
|
+
_def?: { type?: unknown; innerType?: z.ZodTypeAny };
|
|
328
|
+
def?: { type?: unknown; innerType?: z.ZodTypeAny };
|
|
329
|
+
};
|
|
330
|
+
const def = raw._def ?? raw.def;
|
|
331
|
+
if (def?.type !== "optional" && def?.type !== "default" && def?.type !== "nullable") return false;
|
|
332
|
+
current = def.innerType;
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
246
337
|
export const nomosRefValueSchema: z.ZodType<NomosRefValue> = z.object({
|
|
247
338
|
workspace: z.string().min(1),
|
|
248
339
|
type: z.string().min(1),
|
|
@@ -1107,6 +1198,24 @@ export const t = {
|
|
|
1107
1198
|
gitObjectId(): Field<GitObjectId, "string"> {
|
|
1108
1199
|
return makeField<GitObjectId, "string">("string", gitObjectId(), FirstWriterWins);
|
|
1109
1200
|
},
|
|
1201
|
+
/** A NOMINAL id field: `estateId: t.brandedId("EstateId")`. Wire-identical to a string; the DSL value
|
|
1202
|
+
* type is {@link TypedId}`<Name>` (a plain string is a compile error) and both generated clients emit
|
|
1203
|
+
* the branded `Name` type. Values come from a payload of the same brand or {@link typedId}. */
|
|
1204
|
+
brandedId<const Name extends string>(name: Name): Field<TypedId<Name>, "string"> {
|
|
1205
|
+
if (!TYPED_ID_BRAND_RE.test(name)) {
|
|
1206
|
+
throw new Error(`t.brandedId: '${name}' must be a capitalized identifier — it becomes the generated type's name.`);
|
|
1207
|
+
}
|
|
1208
|
+
const schema = typedIdSchema();
|
|
1209
|
+
Object.defineProperty(schema, BRANDED_ID_SYMBOL, { value: name, enumerable: false });
|
|
1210
|
+
return makeField<TypedId<Name>, "string">("string", schema as unknown as z.ZodType<TypedId<Name>>, FirstWriterWins);
|
|
1211
|
+
},
|
|
1212
|
+
/** A uuid id field in its canonical lowercase spelling. Generated clients emit the branded `Uuid`. */
|
|
1213
|
+
uuid(): Field<Uuid, "string"> {
|
|
1214
|
+
const schema = z.string().regex(CANONICAL_UUID_RE, "expected a canonical lowercase uuid");
|
|
1215
|
+
Object.defineProperty(schema, BRANDED_ID_SYMBOL, { value: "Uuid", enumerable: false });
|
|
1216
|
+
Object.defineProperty(schema, NOMOS_UUID_BRAND, { value: true, enumerable: false });
|
|
1217
|
+
return makeField<Uuid, "string">("string", schema as unknown as z.ZodType<Uuid>, FirstWriterWins);
|
|
1218
|
+
},
|
|
1110
1219
|
int(rules: IntRules = {}): Field<number, "int"> {
|
|
1111
1220
|
for (const [name, value] of [["minimum", rules.minimum], ["maximum", rules.maximum]] as const) {
|
|
1112
1221
|
if (value !== undefined && !Number.isSafeInteger(value)) {
|
package/src/identity_usda.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// NO JSON, INCLUDING FOR OPAQUE DESCRIPTORS. A field's canonical value `shape` and merge `driver`
|
|
13
13
|
// are compiler-derived structured values. They are written here through the generic typed-USDA value
|
|
14
14
|
// codec below rather than stringified, so this file has no JSON in it at any depth.
|
|
15
|
+
import { usdaString, usdaUnquote } from "./usda_text.js";
|
|
15
16
|
import {
|
|
16
17
|
emptyLawIdentityRegistry,
|
|
17
18
|
lawEntityKeyParts,
|
|
@@ -28,58 +29,8 @@ export const NOMOS_IDENTITY_LAYER_FORMAT = "nomos.identity.v1";
|
|
|
28
29
|
/** The committed identity layer's filename, beside the package config. */
|
|
29
30
|
export const NOMOS_IDENTITY_FILENAME = "nomos.identity.usda";
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
switch (character) {
|
|
34
|
-
case "\\": return "\\\\";
|
|
35
|
-
case '"': return '\\"';
|
|
36
|
-
case "\n": return "\\n";
|
|
37
|
-
case "\r": return "\\r";
|
|
38
|
-
case "\t": return "\\t";
|
|
39
|
-
case "\b": return "\\b";
|
|
40
|
-
case "\f": return "\\f";
|
|
41
|
-
default: return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
42
|
-
}
|
|
43
|
-
})}"`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseQuoted(raw: string): string {
|
|
47
|
-
const value = raw.trim();
|
|
48
|
-
if (!value.startsWith('"') || !value.endsWith('"')) {
|
|
49
|
-
throw new Error(`expected a quoted USDA string, got ${value}`);
|
|
50
|
-
}
|
|
51
|
-
let out = "";
|
|
52
|
-
for (let index = 1; index < value.length - 1; index += 1) {
|
|
53
|
-
const character = value[index]!;
|
|
54
|
-
if (character !== "\\") {
|
|
55
|
-
out += character;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
index += 1;
|
|
59
|
-
const escaped = value[index];
|
|
60
|
-
if (escaped === undefined || index >= value.length - 1) throw new Error("unterminated USDA escape");
|
|
61
|
-
if (escaped === "u") {
|
|
62
|
-
const digits = value.slice(index + 1, index + 5);
|
|
63
|
-
if (!/^[0-9a-fA-F]{4}$/u.test(digits)) throw new Error(`invalid USDA unicode escape \\u${digits}`);
|
|
64
|
-
out += String.fromCharCode(Number.parseInt(digits, 16));
|
|
65
|
-
index += 4;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const decoded: Record<string, string> = {
|
|
69
|
-
"\\": "\\",
|
|
70
|
-
'"': '"',
|
|
71
|
-
n: "\n",
|
|
72
|
-
r: "\r",
|
|
73
|
-
t: "\t",
|
|
74
|
-
b: "\b",
|
|
75
|
-
f: "\f",
|
|
76
|
-
};
|
|
77
|
-
const replacement = decoded[escaped];
|
|
78
|
-
if (replacement === undefined) throw new Error(`unsupported USDA escape \\${escaped}`);
|
|
79
|
-
out += replacement;
|
|
80
|
-
}
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
32
|
+
const quote = usdaString;
|
|
33
|
+
const parseQuoted = usdaUnquote;
|
|
83
34
|
|
|
84
35
|
function parseStringList(raw: string): string[] {
|
|
85
36
|
const value = raw.trim();
|
|
@@ -180,11 +131,13 @@ function emitValue(value: PlainValue, primName: string, indent: number, key?: st
|
|
|
180
131
|
// general USD parser: it reads `def Scope "<name>" { … }` nesting and `custom uniform <type>
|
|
181
132
|
// nomos:<attr> = <literal>` attributes, which is all this layer ever contains.
|
|
182
133
|
|
|
183
|
-
|
|
134
|
+
/** One prim of the exact typed subset this module writes (also the baseline artifact's host format). */
|
|
135
|
+
export interface IdentityPrim {
|
|
184
136
|
readonly name: string;
|
|
185
137
|
readonly attrs: Record<string, unknown>;
|
|
186
|
-
readonly children:
|
|
138
|
+
readonly children: IdentityPrim[];
|
|
187
139
|
}
|
|
140
|
+
type Prim = IdentityPrim;
|
|
188
141
|
|
|
189
142
|
// Attributes are written bare when the `NomosEntity` schema declares their type, and with
|
|
190
143
|
// `custom uniform` only where a prim carries something the schema does not describe.
|
|
@@ -437,9 +390,14 @@ function readEntityForest(
|
|
|
437
390
|
}
|
|
438
391
|
}
|
|
439
392
|
|
|
440
|
-
/**
|
|
441
|
-
|
|
393
|
+
/**
|
|
394
|
+
* Render the identity scopes (`Entities`, `ValueContracts`, `Domains`) at `indent`. The committed
|
|
395
|
+
* identity layer hosts them under `NomosIdentity`; a released baseline (baseline_usda.ts) hosts the
|
|
396
|
+
* same scopes under its own root so continuity has exactly one encoding.
|
|
397
|
+
*/
|
|
398
|
+
export function renderIdentityScopes(lock: CompilerIdentityModel, indent: number): string[] {
|
|
442
399
|
if (lock.identity !== undefined) validateLawIdentityRegistry(lock.identity);
|
|
400
|
+
const p = pad(indent);
|
|
443
401
|
const entities = Object.entries(lock.identity?.entities ?? {}).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
|
|
444
402
|
const domains = Object.entries(lock.domains).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
|
|
445
403
|
const contracts = new Map<string, unknown>();
|
|
@@ -456,7 +414,45 @@ export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
|
|
|
456
414
|
renderedContracts.set(field.contract, rendered);
|
|
457
415
|
}
|
|
458
416
|
}
|
|
417
|
+
return [
|
|
418
|
+
`${p}def Scope "Entities"`,
|
|
419
|
+
`${p}{`,
|
|
420
|
+
...emitEntityForest(Object.fromEntries(entities), indent + 4),
|
|
421
|
+
`${p}}`,
|
|
422
|
+
`${p}def Scope "ValueContracts"`,
|
|
423
|
+
`${p}{`,
|
|
424
|
+
...[...contracts.entries()].sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0)).flatMap(([identity, shape]) => [
|
|
425
|
+
`${p} def Scope ${quote(`c_${identity}`)}`,
|
|
426
|
+
`${p} {`,
|
|
427
|
+
...emitValue(shape, "Shape", indent + 8),
|
|
428
|
+
`${p} }`,
|
|
429
|
+
]),
|
|
430
|
+
`${p}}`,
|
|
431
|
+
`${p}def Scope "Domains"`,
|
|
432
|
+
`${p}{`,
|
|
433
|
+
...domains.flatMap(([name, domain]) => {
|
|
434
|
+
const aggregates = Object.entries(domain.stableIds.aggregates).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
|
|
435
|
+
return [
|
|
436
|
+
`${p} def Scope ${quote(`d_${domain.stableIds.domain}`)}`,
|
|
437
|
+
`${p} {`,
|
|
438
|
+
`${p} custom uniform string nomos:name = ${quote(name)}`,
|
|
439
|
+
`${p} custom uniform string nomos:sid = ${quote(domain.stableIds.domain)}`,
|
|
440
|
+
...(domain.schemas === undefined ? [] : emitValue(domain.schemas, "Schemas", indent + 8)),
|
|
441
|
+
...(aggregates.length === 0 ? [] : [
|
|
442
|
+
`${p} def Scope "Aggregates"`,
|
|
443
|
+
`${p} {`,
|
|
444
|
+
...aggregates.flatMap(([label, aggregate]) => emitAggregate(label, aggregate, indent + 12)),
|
|
445
|
+
`${p} }`,
|
|
446
|
+
]),
|
|
447
|
+
`${p} }`,
|
|
448
|
+
];
|
|
449
|
+
}),
|
|
450
|
+
`${p}}`,
|
|
451
|
+
];
|
|
452
|
+
}
|
|
459
453
|
|
|
454
|
+
/** Render the committed identity layer. Deterministic: every map is emitted in sorted key order. */
|
|
455
|
+
export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
|
|
460
456
|
return [
|
|
461
457
|
"#usda 1.0",
|
|
462
458
|
"(",
|
|
@@ -470,65 +466,30 @@ export function serializeIdentityUsda(lock: CompilerIdentityModel): string {
|
|
|
470
466
|
"",
|
|
471
467
|
'def Scope "NomosIdentity"',
|
|
472
468
|
"{",
|
|
473
|
-
|
|
474
|
-
" {",
|
|
475
|
-
...emitEntityForest(Object.fromEntries(entities), 8),
|
|
476
|
-
" }",
|
|
477
|
-
' def Scope "ValueContracts"',
|
|
478
|
-
" {",
|
|
479
|
-
...[...contracts.entries()].sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0)).flatMap(([identity, shape]) => [
|
|
480
|
-
` def Scope ${quote(`c_${identity}`)}`,
|
|
481
|
-
" {",
|
|
482
|
-
...emitValue(shape, "Shape", 12),
|
|
483
|
-
" }",
|
|
484
|
-
]),
|
|
485
|
-
" }",
|
|
486
|
-
' def Scope "Domains"',
|
|
487
|
-
" {",
|
|
488
|
-
...domains.flatMap(([name, domain]) => {
|
|
489
|
-
const aggregates = Object.entries(domain.stableIds.aggregates).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0));
|
|
490
|
-
return [
|
|
491
|
-
` def Scope ${quote(`d_${domain.stableIds.domain}`)}`,
|
|
492
|
-
" {",
|
|
493
|
-
` custom uniform string nomos:name = ${quote(name)}`,
|
|
494
|
-
` custom uniform string nomos:sid = ${quote(domain.stableIds.domain)}`,
|
|
495
|
-
...(domain.schemas === undefined ? [] : emitValue(domain.schemas, "Schemas", 12)),
|
|
496
|
-
...(aggregates.length === 0 ? [] : [
|
|
497
|
-
' def Scope "Aggregates"',
|
|
498
|
-
" {",
|
|
499
|
-
...aggregates.flatMap(([label, aggregate]) => emitAggregate(label, aggregate, 16)),
|
|
500
|
-
" }",
|
|
501
|
-
]),
|
|
502
|
-
" }",
|
|
503
|
-
];
|
|
504
|
-
}),
|
|
505
|
-
" }",
|
|
469
|
+
...renderIdentityScopes(lock, 4),
|
|
506
470
|
"}",
|
|
507
471
|
"",
|
|
508
472
|
].join("\n");
|
|
509
473
|
}
|
|
510
474
|
|
|
511
|
-
/**
|
|
512
|
-
export
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (!text.includes(NOMOS_IDENTITY_LAYER_FORMAT)) {
|
|
517
|
-
throw new Error(
|
|
518
|
-
`${NOMOS_IDENTITY_FILENAME} is not ${NOMOS_IDENTITY_LAYER_FORMAT} — refusing to guess an identity encoding`,
|
|
519
|
-
);
|
|
520
|
-
}
|
|
475
|
+
/** The `NomosEntity` schema block a layer hosting identity scopes declares once at its top. */
|
|
476
|
+
export const NOMOS_IDENTITY_ENTITY_SCHEMA = NOMOS_ENTITY_SCHEMA;
|
|
477
|
+
|
|
478
|
+
/** Read the root prims of a layer written in this module's typed subset. */
|
|
479
|
+
export function parseIdentityPrims(text: string, filename: string = NOMOS_IDENTITY_FILENAME): IdentityPrim[] {
|
|
521
480
|
const roots: Prim[] = [];
|
|
522
481
|
// Skip the layer header before scanning. `customLayerData = { … }` closes with a braced line of
|
|
523
482
|
// its own, which the prim reader would otherwise treat as the end of the enclosing scope and stop
|
|
524
483
|
// before reaching the first prim. Root prims sit at column 0, so that is where parsing starts.
|
|
525
484
|
const lines = text.split(/\r?\n/);
|
|
526
485
|
const firstPrim = lines.findIndex((line) => /^def\s+Scope\s/.test(line));
|
|
527
|
-
if (firstPrim === -1) throw new Error(`${
|
|
486
|
+
if (firstPrim === -1) throw new Error(`${filename} has no root prim`);
|
|
528
487
|
parsePrims(lines.slice(firstPrim), 0, roots);
|
|
529
|
-
|
|
530
|
-
|
|
488
|
+
return roots;
|
|
489
|
+
}
|
|
531
490
|
|
|
491
|
+
/** Read identity scopes (`Entities`, `ValueContracts`, `Domains`) hosted under `root`. */
|
|
492
|
+
export function readIdentityScopes(root: IdentityPrim, filename: string = NOMOS_IDENTITY_FILENAME): CompilerIdentityModel {
|
|
532
493
|
const entities: Record<string, LawIdentityEntity> = {};
|
|
533
494
|
readEntityForest(childNamed(root, "Entities")?.children ?? [], undefined, entities);
|
|
534
495
|
const identity: LawIdentityRegistry = { v: 1, entities };
|
|
@@ -538,7 +499,7 @@ export function parseIdentityUsda(text: string): CompilerIdentityModel {
|
|
|
538
499
|
for (const prim of childNamed(root, "ValueContracts")?.children ?? []) {
|
|
539
500
|
const identity = prim.name.replace(/^c_/u, "");
|
|
540
501
|
const shape = childNamed(prim, "Shape");
|
|
541
|
-
if (!identity || shape === undefined) throw new Error(`${
|
|
502
|
+
if (!identity || shape === undefined) throw new Error(`${filename} has an invalid value-contract prim ${prim.name}`);
|
|
542
503
|
contracts.set(identity, readValue(shape));
|
|
543
504
|
}
|
|
544
505
|
|
|
@@ -569,6 +530,22 @@ export function parseIdentityUsda(text: string): CompilerIdentityModel {
|
|
|
569
530
|
return { v: 1, domains, ...(Object.keys(entities).length === 0 ? {} : { identity }) };
|
|
570
531
|
}
|
|
571
532
|
|
|
533
|
+
/** Read the committed identity layer back into the compiler's in-memory lock model. */
|
|
534
|
+
export function parseIdentityUsda(text: string): CompilerIdentityModel {
|
|
535
|
+
if (!text.startsWith("#usda")) {
|
|
536
|
+
throw new Error(`${NOMOS_IDENTITY_FILENAME} is not a USDA layer`);
|
|
537
|
+
}
|
|
538
|
+
if (!text.includes(NOMOS_IDENTITY_LAYER_FORMAT)) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
`${NOMOS_IDENTITY_FILENAME} is not ${NOMOS_IDENTITY_LAYER_FORMAT} — refusing to guess an identity encoding`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const roots = parseIdentityPrims(text);
|
|
544
|
+
const root = roots.find((prim) => prim.name === "NomosIdentity");
|
|
545
|
+
if (root === undefined) throw new Error(`${NOMOS_IDENTITY_FILENAME} has no NomosIdentity prim`);
|
|
546
|
+
return readIdentityScopes(root);
|
|
547
|
+
}
|
|
548
|
+
|
|
572
549
|
/** An empty identity layer — the first compile of a package that has never been compiled. */
|
|
573
550
|
export function emptyIdentityLock(): CompilerIdentityModel {
|
|
574
551
|
return { v: 1, domains: {}, identity: emptyLawIdentityRegistry() };
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,9 @@ export {
|
|
|
49
49
|
forwardRef,
|
|
50
50
|
requires,
|
|
51
51
|
OWNER_FIELD,
|
|
52
|
+
BIRTH_KEY_WRITE_ONCE_CODE,
|
|
53
|
+
isBirthKeyWriteOnceRefusal,
|
|
54
|
+
birthKeyWriteOnceFields,
|
|
52
55
|
type AggregateHandle,
|
|
53
56
|
type AudienceMethods,
|
|
54
57
|
type Audience,
|
|
@@ -89,6 +92,14 @@ export {
|
|
|
89
92
|
isGitObjectIdSchema,
|
|
90
93
|
NOMOS_SHA256_DIGEST_BRAND,
|
|
91
94
|
NOMOS_GIT_OBJECT_ID_BRAND,
|
|
95
|
+
// TYPED IDS: `t.brandedId(Name)` / `t.uuid()` / `t.idOf(Agg)` fields carry `TypedId<Brand>` values;
|
|
96
|
+
// `typedId(brand, text)` is the one sanctioned door from text to an id.
|
|
97
|
+
type TypedId,
|
|
98
|
+
type Uuid,
|
|
99
|
+
typedId,
|
|
100
|
+
typedIdShapeRefusal,
|
|
101
|
+
isUuidIdSchema,
|
|
102
|
+
NOMOS_UUID_BRAND,
|
|
92
103
|
// FIELD FACETS (the endorse model — security_model.md): authority/standing/attribution gate
|
|
93
104
|
// set() acceptance of PrincipalEvidence; secret/public are the confidentiality lattice.
|
|
94
105
|
authorityField,
|
|
@@ -136,7 +147,17 @@ export {
|
|
|
136
147
|
type GeoPolygonValue,
|
|
137
148
|
type GeoMultiPointValue,
|
|
138
149
|
type ValueObjectUsage,
|
|
150
|
+
valueIdentity,
|
|
139
151
|
} from "./value_object.js";
|
|
152
|
+
// VALUE IDENTITY (`value_identity.ts`, CAP-853): every value object's `canonical` / `equals` /
|
|
153
|
+
// `digest` implement the versioned `nomos.vo-canonical.v1` encoding (RFC 8785 over the
|
|
154
|
+
// descriptor-judged value); `valueIdentity(schema)` derives them for lists/maps of VOs.
|
|
155
|
+
export {
|
|
156
|
+
VO_CANONICAL_ENCODING,
|
|
157
|
+
VoCanonicalError,
|
|
158
|
+
type ValueIdentity,
|
|
159
|
+
type VoCanonicalEncoding,
|
|
160
|
+
} from "./value_identity.js";
|
|
140
161
|
export {
|
|
141
162
|
enumType,
|
|
142
163
|
enumTypeNameOf,
|
|
@@ -391,9 +412,14 @@ export {
|
|
|
391
412
|
type GenesisChainOrBuilder,
|
|
392
413
|
type BusinessBirthDecl,
|
|
393
414
|
type BusinessBirthSpec,
|
|
415
|
+
CHOOSING_A_BIRTH_KEY_DOC,
|
|
394
416
|
type BusinessBirthPayload,
|
|
395
417
|
type BirthFactKeys,
|
|
396
|
-
type
|
|
418
|
+
type BirthKeyFactKeys,
|
|
419
|
+
type BirthKeyType,
|
|
420
|
+
type BirthKeyDeclaration,
|
|
421
|
+
birthKeyTypeOfField,
|
|
422
|
+
birthKeyShapeRefusal,
|
|
397
423
|
} from "./births.js";
|
|
398
424
|
// THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain): a home/child law exports
|
|
399
425
|
// `requireParentAttestedBirth` to declare it requires a parent-attested-birth cert chain (the
|
|
@@ -827,6 +853,14 @@ export {
|
|
|
827
853
|
seedGenesis,
|
|
828
854
|
type GenesisWarrantHandle,
|
|
829
855
|
} from "./framework/genesis.js";
|
|
856
|
+
// TOKEN-FREE FOUNDING (identity target model D22) — the founding record and the kernel-appended birth
|
|
857
|
+
// origin a founded child carries instead of a BirthAttestation: operator tooling reads its lineage by them.
|
|
858
|
+
export {
|
|
859
|
+
FoundingPrincipal,
|
|
860
|
+
FOUNDING_PRINCIPAL_ID,
|
|
861
|
+
BirthOrigin,
|
|
862
|
+
BIRTH_ORIGIN_ID,
|
|
863
|
+
} from "./framework/founding_genesis.js";
|
|
830
864
|
// E2E ENCRYPTION (slice 4) — the group-key events (RFC 9420 sender-keys on the ledger).
|
|
831
865
|
export {
|
|
832
866
|
KeyScope,
|
package/src/intent.ts
CHANGED
|
@@ -1136,6 +1136,11 @@ class IntentBuilder<I> {
|
|
|
1136
1136
|
? {}
|
|
1137
1137
|
: {
|
|
1138
1138
|
birthsChild: true,
|
|
1139
|
+
// The composed intent births through the declared birth's lowered plan, so it carries the same
|
|
1140
|
+
// typed-key declaration (the kernel's `nomos:birthKeyType` era key) as that birth directive.
|
|
1141
|
+
...(birthSteps[0]?.directive.birthKey === undefined
|
|
1142
|
+
? {}
|
|
1143
|
+
: { birthKey: birthSteps[0]!.directive.birthKey }),
|
|
1139
1144
|
...(bindBusinessBirthPolicy === undefined
|
|
1140
1145
|
? {}
|
|
1141
1146
|
: { [BUSINESS_BIRTH_POLICY_BINDING]: bindBusinessBirthPolicy }),
|
|
@@ -1444,17 +1449,42 @@ export function assertApplicationLanguageCoversAuthoredDirectives(
|
|
|
1444
1449
|
export function remapWorkspaceInvariantsForLanguage(
|
|
1445
1450
|
invariants: readonly WorkspaceInvariantDecl[],
|
|
1446
1451
|
language: ApplicationLanguage
|
|
1452
|
+
): WorkspaceInvariantDecl[] {
|
|
1453
|
+
return carryWorkspaceInvariantsToPerformingIntents(
|
|
1454
|
+
invariants,
|
|
1455
|
+
language.intents,
|
|
1456
|
+
() => false
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* THE ONE CARRY RULE for effect invariants (CAP-863). The kernel selects a workspace invariant by
|
|
1462
|
+
* the DISPATCHED directive id (`invariant.on == directive_id`), and a business intent dispatches
|
|
1463
|
+
* under its own id while planning its effects inline. An invariant declared `.on(effect)` therefore
|
|
1464
|
+
* judges an intent only if it is carried onto that intent. Carry it onto EVERY routable business
|
|
1465
|
+
* intent that performs the effect, whether or not the domain declares `language(...)`.
|
|
1466
|
+
*
|
|
1467
|
+
* The original declaration stays on the effect when nothing performs it, or when the caller says the
|
|
1468
|
+
* effect id may still be dispatched (`dispatchable(effect.id)`): a public directive dispatched directly,
|
|
1469
|
+
* or (without a language) a bare id another installed law may dispatch, must still be judged. A
|
|
1470
|
+
* language-closed effect is private, so its original is dropped exactly as before. Package composition
|
|
1471
|
+
* and the sealed engine both call this function with the same predicate, so the manifest and the
|
|
1472
|
+
* executable invariant registry (and its routing table) name exactly the same invariants.
|
|
1473
|
+
*/
|
|
1474
|
+
export function carryWorkspaceInvariantsToPerformingIntents(
|
|
1475
|
+
invariants: readonly WorkspaceInvariantDecl[],
|
|
1476
|
+
intents: readonly BusinessIntent<any>[],
|
|
1477
|
+
dispatchable: (directiveId: string) => boolean
|
|
1447
1478
|
): WorkspaceInvariantDecl[] {
|
|
1448
1479
|
const out: WorkspaceInvariantDecl[] = [];
|
|
1449
1480
|
for (const invariant of invariants) {
|
|
1450
|
-
const uses =
|
|
1481
|
+
const uses = intents.flatMap((candidate) =>
|
|
1451
1482
|
candidate.steps
|
|
1452
1483
|
.filter((step) => step.directive.id === invariant.on)
|
|
1453
1484
|
.map((step) => ({ intent: candidate, step }))
|
|
1454
1485
|
);
|
|
1455
|
-
if (uses.length === 0) {
|
|
1486
|
+
if (uses.length === 0 || dispatchable(invariant.on)) {
|
|
1456
1487
|
out.push(invariant);
|
|
1457
|
-
continue;
|
|
1458
1488
|
}
|
|
1459
1489
|
for (const use of uses) {
|
|
1460
1490
|
const mapFacts = (
|