@githolon/dsl 0.102.5 → 0.103.1
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/compile_package.mjs +43 -2
- package/compile_proof.d.mts +47 -0
- package/compile_proof.mjs +343 -0
- package/dart/CHANGELOG.md +14 -0
- package/dart/lib/src/dev_cloud.dart +8 -8
- package/dart/lib/src/nms_generated_runtime.dart +2 -0
- package/dart/lib/src/runner/node-entry.mjs +5539 -1228
- package/dart/lib/src/runner/offline-install.mjs +4578 -528
- package/dart/lib/src/test_harness.dart +206 -152
- package/dart/lib/src/webview_bridge.dart +239 -102
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +4 -3
- package/dart/test/acquire_session_test.dart +9 -0
- package/dart/test/adv_realm_interference_test.dart +7 -6
- package/dart/test/cloud_bound_topology_test.dart +3 -3
- package/dart/test/cluster_births_test.dart +4 -3
- package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
- package/dart/test/cluster_local_autosync_test.dart +3 -2
- package/dart/test/cluster_scale_test.dart +3 -2
- package/dart/test/dlq_session_surface_test.dart +7 -6
- package/dart/test/first_app_gauntlet_test.dart +14 -21
- package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
- package/dart/test/four_layer_nested_topology_test.dart +4 -4
- package/dart/test/harness_phase_timeout_test.dart +2 -2
- package/dart/test/local_sync_inert_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +2 -2
- package/dart/test/sealing_client_test.dart +4 -3
- package/dart/test/session_bound_test.dart +8 -7
- package/dart/test/test_harness_test.dart +13 -12
- package/dart/test/vo_read_typing_test.dart +4 -3
- package/dart/test/watch_after_offer_stall_test.dart +4 -3
- package/dart/test/webview_bridge_test.dart +102 -5
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/nomos_types.dart +9 -0
- package/dart_types/lib/src/nomos_ref.dart +20 -0
- package/dart_types/lib/src/optional_ref.dart +85 -0
- package/dart_types/lib/src/sha256_digest.dart +41 -0
- package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/optional_ref_test.dart +44 -0
- package/dart_types/test/sha256_digest_test.dart +18 -0
- package/package.json +9 -5
- package/runtime/generated-application-host.js +5365 -1105
- package/src/aggregate.ts +18 -0
- package/src/application_composition.ts +6 -32
- package/src/attested_core.ts +6 -3
- package/src/birth_meter.ts +1 -0
- package/src/birth_meter_ensure.ts +28 -2
- package/src/births.ts +1 -1
- package/src/build_package.ts +162 -538
- package/src/business_birth.ts +19 -3
- package/src/canonical_json.ts +21 -0
- package/src/check_package_main.ts +73 -33
- package/src/clear_directive.ts +19 -8
- package/src/codegen_dart.ts +305 -22
- package/src/codegen_dart_application.ts +862 -292
- package/src/codegen_dart_client.ts +1084 -407
- package/src/codegen_dart_scenario.ts +12 -12
- package/src/codegen_proof.ts +169 -39
- package/src/codegen_provider_dart.ts +17 -1
- package/src/codegen_ts.ts +298 -106
- package/src/compat_manifest.ts +2 -2
- package/src/compat_package_main.ts +8 -12
- package/src/compile_package_main.ts +1860 -592
- package/src/compose.ts +1 -1
- package/src/directive.ts +208 -15
- package/src/engine_entry.ts +165 -111
- package/src/evolve_preview.ts +29 -69
- package/src/extremum.ts +1 -1
- package/src/fields.ts +183 -4
- package/src/flow_gate.ts +75 -2
- package/src/framework/application_home.ts +2 -0
- package/src/framework/auth_provider_law.ts +2 -2
- package/src/framework/birth_meter.ts +7 -19
- package/src/framework/bootstrap.ts +0 -2
- package/src/framework/business_corrections.ts +9 -5
- package/src/framework/domain_lifecycle.ts +15 -29
- package/src/framework/encryption_scope.ts +2 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +16 -14
- package/src/framework/home.ts +20 -14
- package/src/framework/owned_custody.ts +9 -3
- package/src/framework/platform_admin.ts +3 -3
- package/src/framework/relations.ts +21 -13
- package/src/framework/signing_identity.ts +45 -40
- package/src/framework/workspace_invariant.ts +245 -0
- package/src/framework/workspaces.ts +23 -29
- package/src/index.ts +36 -16
- package/src/intent.ts +178 -6
- package/src/law_check.ts +32 -11
- package/src/law_graph.ts +2902 -0
- package/src/law_graph_usda.ts +1799 -0
- package/src/law_identity.ts +435 -0
- package/src/law_value_type.ts +230 -0
- package/src/lsp_server.ts +58 -29
- package/src/ops.ts +7 -0
- package/src/package_config.ts +141 -0
- package/src/package_dependencies.ts +108 -0
- package/src/package_dependency_usage.ts +150 -0
- package/src/package_source_imports.ts +71 -0
- package/src/plan_source_slice.ts +335 -0
- package/src/point_guard.ts +192 -0
- package/src/precondition.ts +44 -9
- package/src/read_projection_namespace.ts +1 -1
- package/src/runtime_zod.ts +44 -0
- package/src/secondary_ensure.ts +67 -14
- package/src/shape_compat.ts +1 -1
- package/src/stable_id_identity_bridge.ts +149 -0
- package/src/stable_id_lockfile.ts +140 -6
- package/src/stable_ids.ts +1 -1
- package/src/stable_ids_for_module.ts +348 -0
- package/src/stable_ids_types.ts +1 -1
- package/src/sum.ts +1 -1
- package/src/usd_state.ts +1 -1
- package/src/usda_relationship_integrity.ts +66 -0
- package/src/usda_text.ts +18 -0
- package/src/usdz.ts +78 -0
- package/src/value_object.ts +95 -0
- package/src/wire.ts +19 -0
- package/src/wire_encode.ts +22 -0
- package/src/workspace_sharding.ts +52 -36
- package/src/workspace_sharing.ts +28 -4
- package/src/zod_introspection.ts +1 -1
- package/dart/test/cluster_attested_test.dart +0 -201
- package/src/compile_engine.ts +0 -595
- package/src/manifest.ts +0 -1476
- package/src/read_projection_composition.ts +0 -117
- package/src/read_projection_manifest.ts +0 -115
- package/src/relation_composition.ts +0 -286
- package/src/usd.ts +0 -1967
- package/src/usd_layers.ts +0 -1776
|
@@ -70,8 +70,8 @@ export const GLOBAL_READ_PROJECTION_AGGREGATE_IDS: ReadonlySet<string> = new Set
|
|
|
70
70
|
// disclosure
|
|
71
71
|
"Disclosure",
|
|
72
72
|
// domain lifecycle (the law records themselves)
|
|
73
|
-
"PolicyBundle",
|
|
74
73
|
"DomainInstallation",
|
|
74
|
+
"nomos_internal_CorrectionRecord_a91e73",
|
|
75
75
|
"nomos_internal_ReleaseReceipt_6f0d3a",
|
|
76
76
|
// encryption identity + scope (the operator-blind key machinery)
|
|
77
77
|
"EncKey",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Runtime-only Zod surface for compiled intent plans.
|
|
2
|
+
//
|
|
3
|
+
// The public DSL deliberately exposes Zod's full `z` namespace for authoring and type inference.
|
|
4
|
+
// Bundling that namespace, however, marks every Zod export live: all locales plus JSON-schema
|
|
5
|
+
// import/export machinery enter every exact plan. The plan slicer rewrites only the runtime `z`
|
|
6
|
+
// binding to this curated value; tenant source and TypeScript types continue to use the public API.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ZodIssueCode,
|
|
10
|
+
array,
|
|
11
|
+
boolean,
|
|
12
|
+
discriminatedUnion,
|
|
13
|
+
enum as zEnum,
|
|
14
|
+
lazy,
|
|
15
|
+
literal,
|
|
16
|
+
null as zNull,
|
|
17
|
+
number,
|
|
18
|
+
object,
|
|
19
|
+
record,
|
|
20
|
+
string,
|
|
21
|
+
toJSONSchema,
|
|
22
|
+
union,
|
|
23
|
+
unknown,
|
|
24
|
+
url,
|
|
25
|
+
} from "zod/v4";
|
|
26
|
+
|
|
27
|
+
export const z = Object.freeze({
|
|
28
|
+
ZodIssueCode,
|
|
29
|
+
array,
|
|
30
|
+
boolean,
|
|
31
|
+
discriminatedUnion,
|
|
32
|
+
enum: zEnum,
|
|
33
|
+
lazy,
|
|
34
|
+
literal,
|
|
35
|
+
null: zNull,
|
|
36
|
+
number,
|
|
37
|
+
object,
|
|
38
|
+
record,
|
|
39
|
+
string,
|
|
40
|
+
toJSONSchema,
|
|
41
|
+
union,
|
|
42
|
+
unknown,
|
|
43
|
+
url,
|
|
44
|
+
});
|
package/src/secondary_ensure.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* SECONDARY-
|
|
7
|
+
* SECONDARY-OUTPUT EFFECT CHECK + ENSURE SANCTION (the co2-dev id-mint-gate DX).
|
|
8
8
|
*
|
|
9
9
|
* A directive carries ONE referential marker for its PRIMARY aggregate (`.creates(A)` /
|
|
10
10
|
* `.ensures(A)` / …). But a plan may ALSO introduce a SECOND aggregate at a caller-chosen
|
|
@@ -16,29 +16,77 @@
|
|
|
16
16
|
* (an unsanctioned dev-chosen id can't be forged) but an opaque footgun for the author.
|
|
17
17
|
*
|
|
18
18
|
* This module closes the footgun WITHOUT weakening the gate: `composeDomainModule` PLAN-PROBES each
|
|
19
|
-
* directive (the same probe machinery `codegen_proof.ts` uses)
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
19
|
+
* directive (the same probe machinery `codegen_proof.ts` uses) and first refuses any observed marked
|
|
20
|
+
* output absent from that directive's typed effect signature. An author must explicitly declare a
|
|
21
|
+
* secondary output with `.alsoEnsures(Type)` (or its marker-specific sibling). For a DECLARED secondary
|
|
22
|
+
* Ensure whose type has no primary Ensure declaration, it then injects a NO-OP sanctioning
|
|
23
|
+
* `.ensures(Type)` directive (a {@link sanctioningEnsureDirective}, modelled on
|
|
24
|
+
* `nomosBirthRecordEnsure`). The manifest/USD carries both the honest per-plan effect and the legacy
|
|
25
|
+
* id-mint sanction. The synthesized directive is deliberately useless to dispatch.
|
|
26
26
|
*
|
|
27
27
|
* ERA-SAFE: a domain whose plans do NO secondary ensures is returned untouched (byte-identical). A
|
|
28
28
|
* domain that DOES gains the sanction — and such a domain could not have deployed before this without
|
|
29
29
|
* the author hand-writing the declaration, so no already-shippable manifest hash silently drifts.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* before (no regression, no forged-id hole). Auto-declare only ever ADDS a sanction the author's own
|
|
35
|
-
* plan already proves it intends; it never blocks a valid deploy.
|
|
31
|
+
* PROBE BOUNDARY: the probe runs each plan once under a synthesized lawful payload. Every observed
|
|
32
|
+
* mismatch is a compile error. A conditional branch not exercised by that sample still requires a
|
|
33
|
+
* future static effect analysis to prove exhaustively; the runtime id-mint gate remains fail-closed.
|
|
36
34
|
*/
|
|
37
35
|
import { z } from "zod";
|
|
38
36
|
import { directive } from "./directive.js";
|
|
39
37
|
import type { Directive } from "./directive.js";
|
|
40
38
|
import type { AggregateHandle } from "./aggregate.js";
|
|
41
|
-
import { probeEnsuredTypes } from "./codegen_proof.js";
|
|
39
|
+
import { probeEnsuredTypes, probePlanOps } from "./codegen_proof.js";
|
|
40
|
+
|
|
41
|
+
const effectMethod = (marker: string): string => {
|
|
42
|
+
switch (marker) {
|
|
43
|
+
case "creates": return "alsoCreates";
|
|
44
|
+
case "ensures": return "alsoEnsures";
|
|
45
|
+
case "mutates": return "alsoMutates";
|
|
46
|
+
case "archives": return "alsoArchives";
|
|
47
|
+
default: return "alsoWrites";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Refuse a plan whose observable marked effects exceed its certified directive signature.
|
|
53
|
+
* TypeScript can prove that each individual field operation is lawful for its aggregate, but
|
|
54
|
+
* it cannot currently express "this function may write exactly these aggregate types". The
|
|
55
|
+
* compile probe closes that effect-system gap for every path it can execute: a secondary output
|
|
56
|
+
* must be named with `.alsoCreates/.alsoEnsures/.alsoMutates/.alsoArchives` on the directive that
|
|
57
|
+
* emits it. A domain-wide sanction is not a substitute for an honest per-plan signature.
|
|
58
|
+
*/
|
|
59
|
+
export function assertPlanEffectsDeclared(
|
|
60
|
+
directive: Directive<unknown>,
|
|
61
|
+
domain: string,
|
|
62
|
+
): void {
|
|
63
|
+
if (directive.manifestOnly === true || directive.birthsChild === true) return;
|
|
64
|
+
const probed = probePlanOps(directive, `${domain}/${directive.id}`);
|
|
65
|
+
if (probed === undefined) return;
|
|
66
|
+
|
|
67
|
+
const declared = new Set<string>([`${directive.marker}:${directive.aggregateId}`]);
|
|
68
|
+
for (const output of directive.secondaryOutputs ?? []) {
|
|
69
|
+
declared.add(`${output.marker}:${output.aggregateId}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const op of probed.ops) {
|
|
73
|
+
const marker = (op as { marker?: unknown }).marker;
|
|
74
|
+
const aggregateType = op.aggregateType;
|
|
75
|
+
if (typeof marker !== "string" || typeof aggregateType !== "string") continue;
|
|
76
|
+
if (declared.has(`${marker}:${aggregateType}`)) continue;
|
|
77
|
+
const method = effectMethod(marker);
|
|
78
|
+
const markerLabel = marker === "creates" ? "Create"
|
|
79
|
+
: marker === "ensures" ? "Ensure"
|
|
80
|
+
: marker === "mutates" ? "Mutate"
|
|
81
|
+
: marker === "archives" ? "Archive"
|
|
82
|
+
: marker;
|
|
83
|
+
throw new Error(
|
|
84
|
+
`[effect signature] ${domain}/${directive.id}: the plan emits ${markerLabel} -> ${aggregateType}, ` +
|
|
85
|
+
`but that output is absent from the directive's typed law. Declare ` +
|
|
86
|
+
`.${method}(${aggregateType}) on this directive.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
42
90
|
|
|
43
91
|
/** The synthesized directive id sanctioning `Ensure → <aggId>`. Derived purely from the aggregate id
|
|
44
92
|
* so recompiles are stable; non-id chars are flattened so the id stays route-safe. */
|
|
@@ -61,6 +109,10 @@ export function sanctioningEnsureDirective(agg: AggregateHandle): Directive<unkn
|
|
|
61
109
|
// tenant action and must not appear in generated clients, applications, or
|
|
62
110
|
// OpenAPI, irrespective of whether the aggregate itself is framework-owned.
|
|
63
111
|
(dir as { businessBirthInternal?: true }).businessBirthInternal = true;
|
|
112
|
+
// This no-op is a certified manifest statement, not an action. Keeping that
|
|
113
|
+
// distinction explicit prevents the plan router from advertising a runtime
|
|
114
|
+
// selector that no source module can register.
|
|
115
|
+
(dir as { manifestOnly?: true }).manifestOnly = true;
|
|
64
116
|
// FRAMEWORK-SHARED DEDUP (Dart codegen): when the sanctioned aggregate is a FRAMEWORK singleton
|
|
65
117
|
// (e.g. RelationTuple — auto-ensured into EVERY relation-using domain), this directive is injected
|
|
66
118
|
// per-domain but its payload class (`NomosEnsure_<Agg>Payload`, zero-field) is byte-identical across
|
|
@@ -86,6 +138,7 @@ export function discoverUnsanctionedEnsures(
|
|
|
86
138
|
directives: readonly Directive<unknown>[],
|
|
87
139
|
domain: string,
|
|
88
140
|
): string[] {
|
|
141
|
+
for (const directive of directives) assertPlanEffectsDeclared(directive, domain);
|
|
89
142
|
// Types some directive already DECLARES as its primary `.ensures(...)` target — already sanctioned.
|
|
90
143
|
const declared = new Set<string>();
|
|
91
144
|
for (const d of directives) {
|
package/src/shape_compat.ts
CHANGED
|
@@ -126,7 +126,7 @@ function payloadFieldKind(spec: PayloadFieldSpec): string {
|
|
|
126
126
|
return "json";
|
|
127
127
|
case "wrapped":
|
|
128
128
|
// typed-id (string wire) or unit (number wire) — the interface sees the WIRE kind.
|
|
129
|
-
return spec.
|
|
129
|
+
return spec.wrappedScalarKind === "string" ? "string" : "double";
|
|
130
130
|
case "ref":
|
|
131
131
|
// NomosRef<T> is aggregate-typed in the client and remains a string id on the wire.
|
|
132
132
|
return "string";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LAW_FRAMEWORK_PARENT,
|
|
3
|
+
createLawIdentityReconciler,
|
|
4
|
+
type LawIdentityRegistry,
|
|
5
|
+
} from "./law_identity.js";
|
|
6
|
+
import type { InferredRename } from "./stable_ids.js";
|
|
7
|
+
import type { StableAggregateId, StableFieldId, StableIds } from "./stable_ids_types.js";
|
|
8
|
+
|
|
9
|
+
export interface StableIdentityAggregateInput {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly fields: readonly string[];
|
|
12
|
+
readonly global?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compatibility bridge for W4.1. The existing shape-aware continuity pass proposes
|
|
17
|
+
* carries; the generalized registry owns the final SIDs and durable tombstones. Later
|
|
18
|
+
* W4 work replaces those proposals with typed-graph/previous-USDA evidence.
|
|
19
|
+
*/
|
|
20
|
+
export function reconcileStableIdsWithIdentity(
|
|
21
|
+
source: LawIdentityRegistry,
|
|
22
|
+
domainName: string,
|
|
23
|
+
currentAggregates: readonly StableIdentityAggregateInput[],
|
|
24
|
+
proposed: StableIds,
|
|
25
|
+
inferred: readonly InferredRename[],
|
|
26
|
+
): { registry: LawIdentityRegistry; continuity: StableIds } {
|
|
27
|
+
const identities = createLawIdentityReconciler(source);
|
|
28
|
+
let result = identities.reconcile({
|
|
29
|
+
kind: "domain",
|
|
30
|
+
labels: [domainName],
|
|
31
|
+
preferredSids: { [domainName]: proposed.domain },
|
|
32
|
+
// One config is only a slice of the project-wide domain namespace.
|
|
33
|
+
retireAbsent: false,
|
|
34
|
+
});
|
|
35
|
+
let registry = result.registry;
|
|
36
|
+
const domainKey = result.resolved[domainName]!;
|
|
37
|
+
const aggregateKeys: Record<string, string> = {};
|
|
38
|
+
|
|
39
|
+
const local = currentAggregates.filter((aggregate) => aggregate.global !== true);
|
|
40
|
+
const localResult = identities.reconcile({
|
|
41
|
+
kind: "aggregate",
|
|
42
|
+
parent: domainKey,
|
|
43
|
+
labels: local.map((aggregate) => aggregate.id),
|
|
44
|
+
carries: aggregateCarries(inferred, new Set(local.map((aggregate) => aggregate.id))),
|
|
45
|
+
preferredSids: preferredAggregateSids(local, proposed),
|
|
46
|
+
});
|
|
47
|
+
registry = localResult.registry;
|
|
48
|
+
Object.assign(aggregateKeys, localResult.resolved);
|
|
49
|
+
|
|
50
|
+
const framework = currentAggregates.filter((aggregate) => aggregate.global === true);
|
|
51
|
+
if (framework.length > 0) {
|
|
52
|
+
const frameworkResult = identities.reconcile({
|
|
53
|
+
kind: "aggregate",
|
|
54
|
+
parent: LAW_FRAMEWORK_PARENT,
|
|
55
|
+
labels: framework.map((aggregate) => aggregate.id),
|
|
56
|
+
preferredSids: preferredAggregateSids(framework, proposed),
|
|
57
|
+
// Framework declarations are shared by independently compiled packages.
|
|
58
|
+
retireAbsent: false,
|
|
59
|
+
});
|
|
60
|
+
registry = frameworkResult.registry;
|
|
61
|
+
Object.assign(aggregateKeys, frameworkResult.resolved);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const aggregateInputs = new Map(currentAggregates.map((aggregate) => [aggregate.id, aggregate]));
|
|
65
|
+
const aggregates: Record<string, StableAggregateId> = {};
|
|
66
|
+
for (const aggregateName of Object.keys(proposed.aggregates).sort(compareText)) {
|
|
67
|
+
const proposedAggregate = proposed.aggregates[aggregateName]!;
|
|
68
|
+
const aggregateInput = aggregateInputs.get(aggregateName);
|
|
69
|
+
if (aggregateInput === undefined) {
|
|
70
|
+
throw new Error(`NMS-ID-006 proposed aggregate '${aggregateName}' was not discovered`);
|
|
71
|
+
}
|
|
72
|
+
const aggregateKey = aggregateKeys[aggregateName];
|
|
73
|
+
if (aggregateKey === undefined) {
|
|
74
|
+
throw new Error(`NMS-ID-006 aggregate '${aggregateName}' has no reconciled identity`);
|
|
75
|
+
}
|
|
76
|
+
const fieldResult = identities.reconcile({
|
|
77
|
+
kind: "field",
|
|
78
|
+
parent: aggregateKey,
|
|
79
|
+
labels: aggregateInput.fields,
|
|
80
|
+
carries: fieldCarries(inferred, aggregateName),
|
|
81
|
+
preferredSids: Object.fromEntries(
|
|
82
|
+
Object.entries(proposedAggregate.fields).map(([label, field]) => [label, field.sid]),
|
|
83
|
+
),
|
|
84
|
+
});
|
|
85
|
+
registry = fieldResult.registry;
|
|
86
|
+
|
|
87
|
+
const fields: Record<string, StableFieldId> = {};
|
|
88
|
+
for (const [fieldName, proposedField] of Object.entries(proposedAggregate.fields).sort(
|
|
89
|
+
([a], [b]) => compareText(a, b),
|
|
90
|
+
)) {
|
|
91
|
+
const key = fieldResult.resolved[fieldName];
|
|
92
|
+
if (key === undefined) throw new Error(`NMS-ID-006 field '${aggregateName}.${fieldName}' has no identity`);
|
|
93
|
+
const identity = registry.entities[key]!;
|
|
94
|
+
fields[fieldName] = {
|
|
95
|
+
...proposedField,
|
|
96
|
+
sid: identity.sid,
|
|
97
|
+
...(identity.priorLabels === undefined ? {} : { was: [...identity.priorLabels] }),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const identity = registry.entities[aggregateKey]!;
|
|
101
|
+
aggregates[aggregateName] = {
|
|
102
|
+
sid: identity.sid,
|
|
103
|
+
...(identity.priorLabels === undefined ? {} : { was: [...identity.priorLabels] }),
|
|
104
|
+
fields,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const finalRegistry = identities.finish();
|
|
109
|
+
return {
|
|
110
|
+
registry: finalRegistry,
|
|
111
|
+
continuity: {
|
|
112
|
+
v: 1,
|
|
113
|
+
domain: finalRegistry.entities[domainKey]!.sid,
|
|
114
|
+
aggregates,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function aggregateCarries(
|
|
120
|
+
inferred: readonly InferredRename[],
|
|
121
|
+
currentLocalLabels: ReadonlySet<string>,
|
|
122
|
+
): { from: string; to: string }[] {
|
|
123
|
+
return inferred
|
|
124
|
+
.filter((rename) => !rename.what.includes(".") && currentLocalLabels.has(rename.to))
|
|
125
|
+
.map(({ from, to }) => ({ from, to }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function fieldCarries(
|
|
129
|
+
inferred: readonly InferredRename[],
|
|
130
|
+
aggregateName: string,
|
|
131
|
+
): { from: string; to: string }[] {
|
|
132
|
+
const prefix = `${aggregateName}.`;
|
|
133
|
+
return inferred
|
|
134
|
+
.filter((rename) => rename.what.startsWith(prefix))
|
|
135
|
+
.map(({ from, to }) => ({ from, to }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function preferredAggregateSids(
|
|
139
|
+
aggregates: readonly StableIdentityAggregateInput[],
|
|
140
|
+
proposed: StableIds,
|
|
141
|
+
): Record<string, string> {
|
|
142
|
+
return Object.fromEntries(
|
|
143
|
+
aggregates.map((aggregate) => [aggregate.id, proposed.aggregates[aggregate.id]!.sid]),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function compareText(a: string, b: string): number {
|
|
148
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
149
|
+
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { StableAggregateId, StableFieldId, StableIds } from "./stable_ids_types.js";
|
|
2
2
|
import type { WireSchema } from "./wire.js";
|
|
3
|
+
import { mintGlobalAggregateSid } from "./stable_ids.js";
|
|
4
|
+
import {
|
|
5
|
+
LAW_FRAMEWORK_PARENT,
|
|
6
|
+
emptyLawIdentityRegistry,
|
|
7
|
+
lawEntityKey,
|
|
8
|
+
validateLawIdentityRegistry,
|
|
9
|
+
type LawIdentityEntity,
|
|
10
|
+
type LawIdentityRegistry,
|
|
11
|
+
} from "./law_identity.js";
|
|
3
12
|
|
|
4
13
|
export interface StableIdLockDomain {
|
|
5
14
|
stableIds: StableIds;
|
|
@@ -10,6 +19,8 @@ export interface StableIdLockDomain {
|
|
|
10
19
|
export interface StableIdLockfile {
|
|
11
20
|
v: 1;
|
|
12
21
|
domains: Record<string, StableIdLockDomain>;
|
|
22
|
+
/** W4 compiler identity custody. Semantics remain exclusively in USDA. */
|
|
23
|
+
identity?: LawIdentityRegistry;
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
type CompactStableFieldId = Omit<StableFieldId, "contract" | "shape"> & {
|
|
@@ -31,6 +42,15 @@ interface CompactStableIdLockfile {
|
|
|
31
42
|
domains: Record<string, { stableIds: CompactStableIds }>;
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
interface IdentityStableIdLockfile {
|
|
46
|
+
v: 3;
|
|
47
|
+
identity: LawIdentityRegistry;
|
|
48
|
+
/** Temporary W4 compatibility evidence; deleted once evolution traverses prior USDA. */
|
|
49
|
+
contracts: readonly (readonly [string, unknown])[];
|
|
50
|
+
/** Temporary aggregate/field compatibility view consumed by the v2 compiler path. */
|
|
51
|
+
domains: Record<string, { stableIds: CompactStableIds }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
55
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
56
|
}
|
|
@@ -53,11 +73,22 @@ function sortedJson(value: unknown): unknown {
|
|
|
53
73
|
export function parseStableIdLockfile(value: unknown): StableIdLockfile {
|
|
54
74
|
if (!isRecord(value)) throw new Error("stable-id lock must be a JSON object");
|
|
55
75
|
if (value.v === 1) return value as unknown as StableIdLockfile;
|
|
56
|
-
if (
|
|
76
|
+
if (
|
|
77
|
+
(value.v !== 2 && value.v !== 3) ||
|
|
78
|
+
!Array.isArray(value.contracts) ||
|
|
79
|
+
!isRecord(value.domains)
|
|
80
|
+
) {
|
|
57
81
|
throw new Error("unsupported stable-id lock encoding");
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
|
|
84
|
+
let identity: LawIdentityRegistry | undefined;
|
|
85
|
+
if (value.v === 3) {
|
|
86
|
+
if (!isRecord(value.identity)) throw new Error("stable-id lock v3 identity is malformed");
|
|
87
|
+
identity = value.identity as unknown as LawIdentityRegistry;
|
|
88
|
+
validateLawIdentityRegistry(identity);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const compact = value as unknown as CompactStableIdLockfile | IdentityStableIdLockfile;
|
|
61
92
|
const contracts = compact.contracts.map((entry, index) => {
|
|
62
93
|
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") {
|
|
63
94
|
throw new Error(`stable-id lock contract ${index} is malformed`);
|
|
@@ -98,7 +129,7 @@ export function parseStableIdLockfile(value: unknown): StableIdLockfile {
|
|
|
98
129
|
};
|
|
99
130
|
}
|
|
100
131
|
|
|
101
|
-
return { v: 1, domains };
|
|
132
|
+
return { v: 1, domains, ...(identity === undefined ? {} : { identity }) };
|
|
102
133
|
}
|
|
103
134
|
|
|
104
135
|
/**
|
|
@@ -107,6 +138,7 @@ export function parseStableIdLockfile(value: unknown): StableIdLockfile {
|
|
|
107
138
|
* the contract table retains the full descriptor needed to prove safe widening.
|
|
108
139
|
*/
|
|
109
140
|
export function serializeStableIdLockfile(lock: StableIdLockfile): string {
|
|
141
|
+
if (lock.identity !== undefined) validateLawIdentityRegistry(lock.identity);
|
|
110
142
|
const contracts: [string, unknown][] = [];
|
|
111
143
|
const contractIndexes = new Map<string, number>();
|
|
112
144
|
const domains: CompactStableIdLockfile["domains"] = {};
|
|
@@ -138,7 +170,109 @@ export function serializeStableIdLockfile(lock: StableIdLockfile): string {
|
|
|
138
170
|
|
|
139
171
|
// This is a machine-owned lockfile. Compact JSON is intentional: unlike generated
|
|
140
172
|
// law and clients, it must be committed so identity survives a clean clone.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
173
|
+
const encoded =
|
|
174
|
+
lock.identity === undefined
|
|
175
|
+
? ({ v: 2, contracts, domains } satisfies CompactStableIdLockfile)
|
|
176
|
+
: ({ v: 3, identity: lock.identity, contracts, domains } satisfies IdentityStableIdLockfile);
|
|
177
|
+
return JSON.stringify(sortedJson(encoded)) + "\n";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* One-time W4 import of the existing domain/aggregate/field SIDs. The legacy view has
|
|
182
|
+
* already forgotten removals, so only currently present entities can be imported;
|
|
183
|
+
* from this point onward the returned registry retains every retirement.
|
|
184
|
+
*/
|
|
185
|
+
export function identityRegistryFromStableIdLockfile(
|
|
186
|
+
lock: StableIdLockfile | undefined,
|
|
187
|
+
): LawIdentityRegistry {
|
|
188
|
+
if (lock?.identity !== undefined) {
|
|
189
|
+
validateLawIdentityRegistry(lock.identity);
|
|
190
|
+
return lock.identity;
|
|
191
|
+
}
|
|
192
|
+
if (lock === undefined) return emptyLawIdentityRegistry();
|
|
193
|
+
|
|
194
|
+
const entities: Record<string, LawIdentityEntity> = {};
|
|
195
|
+
const aggregateOccurrences = new Map<string, number>();
|
|
196
|
+
for (const domain of Object.values(lock.domains)) {
|
|
197
|
+
for (const aggregate of Object.values(domain.stableIds.aggregates)) {
|
|
198
|
+
aggregateOccurrences.set(aggregate.sid, (aggregateOccurrences.get(aggregate.sid) ?? 0) + 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const [domainName, domain] of Object.entries(lock.domains).sort(([a], [b]) =>
|
|
203
|
+
compareText(a, b),
|
|
204
|
+
)) {
|
|
205
|
+
const domainKey = importIdentityEntity(entities, {
|
|
206
|
+
kind: "domain",
|
|
207
|
+
sid: domain.stableIds.domain,
|
|
208
|
+
label: domainName,
|
|
209
|
+
generation: 0,
|
|
210
|
+
status: "active",
|
|
211
|
+
});
|
|
212
|
+
for (const [aggregateName, aggregate] of Object.entries(domain.stableIds.aggregates).sort(
|
|
213
|
+
([a], [b]) => compareText(a, b),
|
|
214
|
+
)) {
|
|
215
|
+
const parent =
|
|
216
|
+
(aggregateOccurrences.get(aggregate.sid) ?? 0) > 1 ||
|
|
217
|
+
aggregate.sid === mintGlobalAggregateSid(aggregateName)
|
|
218
|
+
? LAW_FRAMEWORK_PARENT
|
|
219
|
+
: domainKey;
|
|
220
|
+
const aggregateKey = importIdentityEntity(entities, {
|
|
221
|
+
kind: "aggregate",
|
|
222
|
+
sid: aggregate.sid,
|
|
223
|
+
parent,
|
|
224
|
+
label: aggregateName,
|
|
225
|
+
...(aggregate.was === undefined ? {} : { priorLabels: aggregate.was }),
|
|
226
|
+
generation: 0,
|
|
227
|
+
status: "active",
|
|
228
|
+
});
|
|
229
|
+
for (const [fieldName, field] of Object.entries(aggregate.fields).sort(([a], [b]) =>
|
|
230
|
+
compareText(a, b),
|
|
231
|
+
)) {
|
|
232
|
+
importIdentityEntity(entities, {
|
|
233
|
+
kind: "field",
|
|
234
|
+
sid: field.sid,
|
|
235
|
+
parent: aggregateKey,
|
|
236
|
+
label: fieldName,
|
|
237
|
+
...(field.was === undefined ? {} : { priorLabels: field.was }),
|
|
238
|
+
generation: 0,
|
|
239
|
+
status: "active",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const registry: LawIdentityRegistry = {
|
|
246
|
+
v: 1,
|
|
247
|
+
entities: Object.fromEntries(Object.entries(entities).sort(([a], [b]) => compareText(a, b))),
|
|
248
|
+
};
|
|
249
|
+
validateLawIdentityRegistry(registry);
|
|
250
|
+
return registry;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function importIdentityEntity(
|
|
254
|
+
entities: Record<string, LawIdentityEntity>,
|
|
255
|
+
entity: LawIdentityEntity,
|
|
256
|
+
): string {
|
|
257
|
+
const key = lawEntityKey(entity.kind, entity.sid);
|
|
258
|
+
const prior = entities[key];
|
|
259
|
+
if (prior === undefined) {
|
|
260
|
+
entities[key] = entity;
|
|
261
|
+
return key;
|
|
262
|
+
}
|
|
263
|
+
if (
|
|
264
|
+
prior.kind !== entity.kind ||
|
|
265
|
+
prior.parent !== entity.parent ||
|
|
266
|
+
prior.label !== entity.label ||
|
|
267
|
+
JSON.stringify(prior.priorLabels ?? []) !== JSON.stringify(entity.priorLabels ?? [])
|
|
268
|
+
) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`NMS-ID-002 legacy SID ${key} aliases incompatible ${prior.kind} '${prior.label}' and ${entity.kind} '${entity.label}'`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return key;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function compareText(a: string, b: string): number {
|
|
277
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
144
278
|
}
|
package/src/stable_ids.ts
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
* projection reads it to fold old-era field/type labels into the current ones.
|
|
47
47
|
*
|
|
48
48
|
* BUILD-TIME ONLY (imports `node:crypto`): reached via `@githolon/dsl/stable-ids` /
|
|
49
|
-
*
|
|
49
|
+
* `stable_ids_for_module.ts`, never the runtime barrel. The pure TYPE surface lives in
|
|
50
50
|
* `stable_ids_types.ts` so the runtime type graph stays node-free.
|
|
51
51
|
*/
|
|
52
52
|
import { createHash } from "node:crypto";
|