@githolon/dsl 0.103.18 → 0.104.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.
@@ -0,0 +1,187 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded
4
+ // QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
5
+ // wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
6
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
7
+
8
+ /**
9
+ * fulfilment.ts — COMPLETENESS AS LAW.
10
+ *
11
+ * ── The problem this dissolves ───────────────────────────────────────────────
12
+ * Real business data arrives in stages. A site is dropped as a PIN long before anyone knows its
13
+ * postcode; a voice note exists before its transcript; a photo before its thumbnail. Modelled
14
+ * naively, the fields those later stages fill are either
15
+ *
16
+ * • REQUIRED — and then the early write is REFUSED, so the app fabricates placeholders to get
17
+ * past the gate (`city: ''`), which is how empty strings and `'Unknown City'` end up
18
+ * permanently in a ledger; or
19
+ * • OPTIONAL — and then nothing ever guarantees they arrive, "is this complete?" becomes a
20
+ * null-check scattered through the UI, and no two screens agree on the answer.
21
+ *
22
+ * Both are the same mistake: completeness is a FACT ABOUT THE RECORD, and facts belong in law.
23
+ *
24
+ * ── The model ────────────────────────────────────────────────────────────────
25
+ * A tenant declares TIERS of completeness in ascending order. Each tier names the fields it
26
+ * GUARANTEES, and optionally the capability that gets a record from the tier below it to here:
27
+ *
28
+ * ```ts
29
+ * export const locationFulfilment = fulfilment("locationFulfilment", {
30
+ * pinned: { guarantees: ["location.geoPoint"] },
31
+ * resolved: { guarantees: ["location.postalAddress"], fulfilledBy: reverseGeocode },
32
+ * });
33
+ *
34
+ * export const EstateSite = aggregate("EstateSite", { … }, { fulfils: locationFulfilment });
35
+ * ```
36
+ *
37
+ * The dev has said only business things. What follows is derived:
38
+ *
39
+ * • a tier FIELD (law-state — "how complete is this" is recorded, never inferred from which
40
+ * fields happen to be null),
41
+ * • a TIER-SCOPED invariant: at tier T, the guarantees of T *and every tier below it* must
42
+ * hold — so a pin-only site is fully LEGAL at `pinned` rather than a refusal to work around,
43
+ * while a record CLAIMING `resolved` without an address is a typed refusal,
44
+ * • a terminal BLOCKED state, so "no address exists here" is distinguishable from "still
45
+ * resolving" (the ambiguity that makes progress indicators lie).
46
+ *
47
+ * This inverts the failure mode. Missing data used to mean THE WRITE IS REFUSED; now it means a
48
+ * lower tier, and refusal is reserved for data that is genuinely WRONG — a record asserting a
49
+ * completeness it does not have.
50
+ *
51
+ * ── MONOTONIC (Jack's ruling) ────────────────────────────────────────────────
52
+ * Tiers only ever climb. If a resolved address is later invalidated that is a NEW FACT (a
53
+ * correction), not a demotion: the tier is a durable claim about what was once established, so
54
+ * "was this ever resolved?" stays answerable and the value cannot flap under concurrent merges.
55
+ * The invariant enforces the claim, and monotonicity is what makes the claim worth having.
56
+ */
57
+ import type { AggregateInvariantVerdict } from "./aggregate.js";
58
+
59
+ /** The reserved suffix for the sibling field carrying a terminal blocked REASON. */
60
+ export const FULFILMENT_BLOCKED_SUFFIX = "Blocked";
61
+
62
+ /** One tier: what it guarantees, and (optionally) what fulfils the climb into it. */
63
+ export interface FulfilmentTierSpec {
64
+ /**
65
+ * Field paths this tier guarantees are PRESENT. Dotted paths address value-object subfields
66
+ * (`"location.postalAddress"`). Cumulative: a tier's guarantees include every lower tier's.
67
+ */
68
+ readonly guarantees: readonly string[];
69
+ /**
70
+ * The capability that promotes a record INTO this tier. Declaring it is what makes the climb
71
+ * automatic; omitting it means the tier is reached by ordinary business writes.
72
+ *
73
+ * Typed loosely on purpose — this module must not import the capability framework (it would
74
+ * make every aggregate declaration drag the capability surface). The compiler resolves the
75
+ * declaration by name.
76
+ */
77
+ readonly fulfilledBy?: { readonly name: string };
78
+ }
79
+
80
+ export interface FulfilmentDecl<Name extends string = string> {
81
+ readonly __isFulfilmentDecl: true;
82
+ /** The tier FIELD's name on the aggregate, and the declaration's own id. */
83
+ readonly name: Name;
84
+ /** Tier names in ASCENDING order of completeness — declaration order IS the order. */
85
+ readonly tiers: readonly string[];
86
+ readonly spec: Readonly<Record<string, FulfilmentTierSpec>>;
87
+ /** The sibling field carrying a terminal blocked reason. */
88
+ readonly blockedField: string;
89
+ /** Every guarantee at `tier` INCLUDING the tiers below it (the cumulative claim). */
90
+ guaranteesAt(tier: string): readonly string[];
91
+ /** The invariant the aggregate composes — pure, snapshot-in/verdict-out. */
92
+ invariant(snapshot: Record<string, unknown>): AggregateInvariantVerdict;
93
+ }
94
+
95
+ const NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
96
+
97
+ /** Read a dotted path out of a folded snapshot; `undefined` when any hop is absent. */
98
+ function readPath(snapshot: Record<string, unknown>, path: string): unknown {
99
+ let cursor: unknown = snapshot;
100
+ for (const hop of path.split(".")) {
101
+ if (cursor === null || cursor === undefined || typeof cursor !== "object") return undefined;
102
+ cursor = (cursor as Record<string, unknown>)[hop];
103
+ }
104
+ return cursor;
105
+ }
106
+
107
+ /**
108
+ * Declare completeness tiers. Refusals are COMPILE-TIME loud (thrown at module load so
109
+ * `nomos-compile` names them): tiers must be a non-empty, well-named, ascending set, and a tier
110
+ * must actually guarantee something or it is not a tier.
111
+ */
112
+ export function fulfilment<const Name extends string>(
113
+ name: Name,
114
+ spec: Record<string, FulfilmentTierSpec>,
115
+ ): FulfilmentDecl<Name> {
116
+ if (!NAME_RE.test(name)) {
117
+ throw new Error(
118
+ `fulfilment('${name}'): the name must match ${NAME_RE} — it becomes a FIELD on every aggregate that declares it`,
119
+ );
120
+ }
121
+ const tiers = Object.keys(spec);
122
+ if (tiers.length === 0) {
123
+ throw new Error(`fulfilment('${name}'): declare at least one tier (a fulfilment with no tiers claims nothing)`);
124
+ }
125
+ for (const tier of tiers) {
126
+ if (!NAME_RE.test(tier)) {
127
+ throw new Error(`fulfilment('${name}'): tier '${tier}' must match ${NAME_RE}`);
128
+ }
129
+ if (spec[tier]!.guarantees.length === 0) {
130
+ throw new Error(
131
+ `fulfilment('${name}'): tier '${tier}' guarantees nothing — a tier exists to make a claim about the record; drop it or give it a guarantee`,
132
+ );
133
+ }
134
+ }
135
+ const blockedField = `${name}${FULFILMENT_BLOCKED_SUFFIX}`;
136
+
137
+ const guaranteesAt = (tier: string): readonly string[] => {
138
+ const index = tiers.indexOf(tier);
139
+ if (index < 0) return [];
140
+ // CUMULATIVE: tiers are ascending, so reaching one asserts every claim beneath it too.
141
+ return tiers.slice(0, index + 1).flatMap((t) => spec[t]!.guarantees);
142
+ };
143
+
144
+ return {
145
+ __isFulfilmentDecl: true,
146
+ name,
147
+ tiers,
148
+ spec,
149
+ blockedField,
150
+ guaranteesAt,
151
+ invariant(snapshot) {
152
+ const raw = snapshot[name];
153
+ // Absent tier ⇒ nothing claimed yet. A record is free to exist below the first tier; it
154
+ // simply guarantees nothing, which is honest.
155
+ if (raw === undefined || raw === null || raw === "") return { accept: true };
156
+ const tier = String(raw);
157
+ if (!tiers.includes(tier)) {
158
+ return {
159
+ reject: `${name} is '${tier}', which is not a declared tier (${tiers.join(" < ")})`,
160
+ };
161
+ }
162
+ // A record parked as terminally unfulfillable keeps whatever it had established; the
163
+ // point of the blocked state is that it stops CLAIMING more, not that it loses ground.
164
+ const blocked = snapshot[blockedField];
165
+ const isBlocked = typeof blocked === "string" && blocked.trim() !== "";
166
+ for (const path of guaranteesAt(tier)) {
167
+ const value = readPath(snapshot, path);
168
+ if (value === undefined || value === null || value === "") {
169
+ if (isBlocked) continue;
170
+ return {
171
+ reject: `${name} claims tier '${tier}' but '${path}' is absent — a tier is a guarantee about the record, so either fill it, drop to a lower tier, or record ${blockedField} to say it can never be filled`,
172
+ };
173
+ }
174
+ }
175
+ return { accept: true };
176
+ },
177
+ };
178
+ }
179
+
180
+ /** Duck-type guard (the exports walk discovers declarations by shape, never by import). */
181
+ export function isFulfilmentDecl(v: unknown): v is FulfilmentDecl {
182
+ return (
183
+ typeof v === "object" &&
184
+ v !== null &&
185
+ (v as { __isFulfilmentDecl?: boolean }).__isFulfilmentDecl === true
186
+ );
187
+ }
package/src/index.ts CHANGED
@@ -226,6 +226,14 @@ export {
226
226
  // surface). The dev never sees a "prim" — that is the GitHolon's internal OpenUSD IR.
227
227
  export { create, existing, ensure, type AggregateRef } from "./authoring.js";
228
228
  export { recordContract, isContractHandle, satisfiesContract, type ContractHandle, type ContractField, type ContractSatisfaction } from "./contract.js";
229
+ // COMPLETENESS AS LAW — tiered fulfilment (`fulfilment.ts`).
230
+ export {
231
+ fulfilment,
232
+ isFulfilmentDecl,
233
+ FULFILMENT_BLOCKED_SUFFIX,
234
+ type FulfilmentDecl,
235
+ type FulfilmentTierSpec,
236
+ } from "./fulfilment.js";
229
237
  export { normalizeActivity, renderActivityTemplate, activityKey, type ActivityInput, type ActivityDescriptor, type ActivityRefInput, type ActivitySeverity } from "./activity.js";
230
238
  // The REBIRTH-boundary repair DSL (architecture/repair_dsl.md) — distinct from framework/repair.js (the
231
239
  // IN-CHAIN compensating-intent lane): repair() binds a typed replay refusal to a declared re-fold remedy.
@@ -572,6 +572,11 @@ export function emitSparseLawGraphUsda(
572
572
  ` def Scope "${domain.entity.id}"`,
573
573
  " {",
574
574
  ...entityLabelOpinions(domain.entity, 12),
575
+ // THE FIELD-PROVENANCE ERA KEY (field_evolution_read_path.md): declared by every
576
+ // bundle this compiler emits. Only under a declaring bundle does the kernel fold
577
+ // stamp Field.schema; chains sealed under pre-key law replay byte-identically
578
+ // forever (the era rule — the law is the era).
579
+ " custom uniform bool nomos:fieldProvenance = true",
575
580
  ];
576
581
  if (domain.parentAttestedBirth !== undefined) {
577
582
  lines.push(
Binary file