@githolon/dsl 0.70.2 → 0.72.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,144 @@
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
+ * THE PAYLOAD-REF idOf LINT (`architecture/dart_codegen_parity.md` P2 — ref ergonomics). A
10
+ * `t.ref(Target)` / `t.foreignRef(Target)` field on the AGGREGATE/read-model side already
11
+ * generates a TYPED id class (`SiteId`, `codegen_dart.ts`). Nothing enforced the payload SIDE
12
+ * carried the matching brand — a directive author can write `siteId: z.string()` in the
13
+ * payload and never discover `idOf(Site)` exists, so the generated Dart payload class carries a
14
+ * raw `String` while the read model carries `SiteId`: an asymmetry that forces manual
15
+ * `.value` unwrap/rewrap at the UI boundary and lets a mistyped id compile silently.
16
+ *
17
+ * THIS PASS closes that gap at COMPILE time: for every directive, plan-PROBE it (reusing the
18
+ * exact primitive the proof generator's own discovery legs use — `probePlanOps` /
19
+ * `synthesizePayload`, `codegen_proof.ts` — NOT a new AST interpreter) and, for every emitted
20
+ * field op that targets a ref-kind field, check whether the op's value is traceable to a
21
+ * TOP-LEVEL payload field whose zod schema is a bare (unbranded) string. If so: REFUSE, naming
22
+ * the directive, the payload field, the ref field, and its target aggregate, with the fix.
23
+ *
24
+ * SCOPE (deliberately narrow — a narrower lint with zero false positives beats a broad one with
25
+ * false refusals, per the authoring guidance):
26
+ * - Only DIRECT, first-order plans: `create(Agg).set("ref", payload.path)` /
27
+ * `setEntry(instance(Agg,...), "ref", key, payload.path)` — i.e. shapes the plan-probe
28
+ * mechanism already observes as ONE emitted op whose `value` is byte-identical to a
29
+ * TOP-LEVEL synthesized payload field's sample value. A value that is transformed,
30
+ * concatenated, or nested more than one level deep inside the payload does NOT match
31
+ * (no false positive — it simply isn't flagged; this is intentionally NOT full dataflow
32
+ * analysis over the plan body).
33
+ * - Only fires when the payload field's own sampled value round-trips EXACTLY into the op
34
+ * (relies on `synthesizePayload`'s per-field-distinguishable string samples,
35
+ * `"sample-<field>"` — the same discriminator the proof generator's own subject/grant-field
36
+ * resolution already trusts, see `resolveGrantSubjectFields`).
37
+ * - A directive whose plan can't be probed (unsynthesizable payload, or the plan throws under
38
+ * the synthesized sample) is SKIPPED — best-effort, matching every other plan-probe
39
+ * consumer in `codegen_proof.ts` (never a false refusal from an exotic shape).
40
+ * - `idOf(Agg)` AND `brandedId(Name)` both satisfy the lint (either brand types the Dart/TS
41
+ * side); only a truly BARE `z.string()` (through `.optional()`/`.default()`/`.nullable()`)
42
+ * refuses.
43
+ *
44
+ * NOT era-gated: this is a pure Dart/TS-codegen build-time guard (like the domain-contracts
45
+ * circular-import guard in `compile_package_main.ts`) — it has no wire/manifest/kernel effect,
46
+ * changes no hash, and never touches already-deployed law. It only judges whether `nomos-compile`
47
+ * accepts a directive's SOURCE shape, exactly the same posture as that circular-dependency guard.
48
+ * A domain already using `idOf`/`brandedId` (or one with no ref-fed payload field at all) compiles
49
+ * exactly as before; only a directive whose payload feeds a ref field with a provably-bare string
50
+ * newly refuses — which is the intended fail-closed behaviour, not an accidental regression.
51
+ */
52
+ import type { z } from "zod";
53
+
54
+ import type { Directive } from "./directive.js";
55
+ import type { FieldOp } from "./ops.js";
56
+ import { probePlanOps } from "./codegen_proof.js";
57
+ import { zodKind, zodDef, zodObjectShape } from "./codegen_ts.js";
58
+ import { idOfMetaOf, brandedIdMetaOf } from "./value_object.js";
59
+
60
+ /** A typed idOf-lint violation — the message names the directive, field, ref, and target. */
61
+ export class IdOfLintError extends Error {
62
+ constructor(message: string) {
63
+ super(message);
64
+ this.name = "IdOfLintError";
65
+ }
66
+ }
67
+
68
+ /** Short, lower-camel/PascalCase-preserving stem of an aggregate id (`inventory.EstateSite` → `EstateSite`),
69
+ * matching the same "identity is the tail segment" convention `directive.ts`'s `shortName` uses. */
70
+ function shortAggName(aggregateId: string): string {
71
+ return aggregateId.includes(".") ? aggregateId.slice(aggregateId.lastIndexOf(".") + 1) : aggregateId;
72
+ }
73
+
74
+ /** Peel `.optional()`/`.default()`/`.nullable()` to the innermost schema (mirrors `idOfMetaOf`/
75
+ * `brandedIdMetaOf`'s own peeling, so a wrapped bare string is still caught). */
76
+ function isBareString(zt: z.ZodTypeAny): boolean {
77
+ let cur: z.ZodTypeAny | undefined = zt;
78
+ for (let hops = 0; cur !== undefined && hops < 16; hops++) {
79
+ if (idOfMetaOf(cur) !== undefined || brandedIdMetaOf(cur) !== undefined) return false;
80
+ const kind = zodKind(cur);
81
+ if (kind === "string") return true;
82
+ if (kind !== "optional" && kind !== "default" && kind !== "nullable") return false;
83
+ cur = zodDef(cur).innerType;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ /** Resolve a ref-kind field op's TARGET aggregate id (`t.ref` → `refAggregateId`/thunk; `t.foreignRef` →
89
+ * `foreignRef.targetAggregateId`). `undefined` when neither meta is present (an exotic/hand-built field). */
90
+ function refTargetOf(op: FieldOp): string | undefined {
91
+ const f = op.targetField as
92
+ | { refAggregateId?: string; foreignRef?: { targetAggregateId?: string } }
93
+ | undefined;
94
+ if (f === undefined) return undefined;
95
+ return f.refAggregateId ?? f.foreignRef?.targetAggregateId;
96
+ }
97
+
98
+ /**
99
+ * Run the idOf lint over one directive: plan-probe it and refuse on the first payload-fed
100
+ * ref-field mismatch. Throws {@link IdOfLintError}; a directive the prober can't run through
101
+ * (unsynthesizable payload / plan throws) is silently skipped — best-effort, no false refusal.
102
+ */
103
+ export function assertIdOfLintForDirective(dir: Directive<unknown>, where: string): void {
104
+ const probed = probePlanOps(dir, where);
105
+ if (probed === undefined) return;
106
+ let shape: Record<string, z.ZodTypeAny>;
107
+ try {
108
+ shape = zodObjectShape(dir.payloadSchema as z.ZodTypeAny);
109
+ } catch {
110
+ return;
111
+ }
112
+ // Reverse index: sampled STRING value → the top-level payload field name(s) that produced it.
113
+ // `synthesizePayload` gives every string-kind field a DISTINCT `"sample-<field>"` value (or a
114
+ // schema-constrained fallback), so an exact match is unambiguous in practice.
115
+ const byValue = new Map<string, string>();
116
+ for (const [name, value] of Object.entries(probed.synth.value)) {
117
+ if (typeof value === "string") byValue.set(value, name);
118
+ }
119
+ for (const op of probed.ops) {
120
+ if (op.kind !== "set" && op.kind !== "setEntry") continue;
121
+ if (typeof op.value !== "string") continue;
122
+ const targetAgg = refTargetOf(op);
123
+ if (targetAgg === undefined) continue; // not a ref-kind field (or one the DSL can't name a target for)
124
+ const payloadField = byValue.get(op.value);
125
+ if (payloadField === undefined) continue; // not traceable to a top-level payload field — no verdict
126
+ const fieldSchema = shape[payloadField];
127
+ if (fieldSchema === undefined || !isBareString(fieldSchema)) continue; // already idOf/brandedId — fine
128
+ const onAgg = shortAggName(op.aggregateType ?? op.aggregateId);
129
+ const targetName = shortAggName(targetAgg);
130
+ throw new IdOfLintError(
131
+ `directive '${dir.id}': payload field '${payloadField}' feeds ref field '${onAgg}.${op.field}' ` +
132
+ `(-> ${targetName}) but is a plain z.string() — wrap it as idOf(${targetName}) so the generated ` +
133
+ `Dart/TS carry the typed id, matching the read model. (dart_codegen_parity.md P2)`,
134
+ );
135
+ }
136
+ }
137
+
138
+ /** Run the idOf lint over every directive in a domain. `domain` names the module for the probe's
139
+ * error-context strings (`synthesizePayload`'s `where`). Throws on the FIRST violation. */
140
+ export function assertIdOfLint(directives: readonly Directive<unknown>[], domain: string): void {
141
+ for (const dir of directives) {
142
+ assertIdOfLintForDirective(dir, `${domain}/${dir.id}`);
143
+ }
144
+ }
package/src/index.ts CHANGED
@@ -93,6 +93,7 @@ export {
93
93
  LastPosition,
94
94
  RemoveWins,
95
95
  Counter,
96
+ Surfaced,
96
97
  encodeDriver,
97
98
  type Driver,
98
99
  type DriverKind,
Binary file
package/src/usd_layers.ts CHANGED
@@ -109,6 +109,8 @@ export function usdaStringArray(items: readonly string[]): string {
109
109
  export function driverToken(driver: WireDriver): string {
110
110
  if (typeof driver === "string") return driver;
111
111
  if ("LwwBy" in driver) return `LwwBy(${driver.LwwBy})`;
112
+ // `Surfaced(<inner>)` — the user-resolvable posture stays legible in the tree too.
113
+ if ("Surfaced" in driver) return `Surfaced(${driverToken(driver.Surfaced)})`;
112
114
  return `MapOf(${driverToken(driver.MapOf)})`;
113
115
  }
114
116
 
@@ -117,6 +119,9 @@ export function parseDriverToken(token: string): WireDriver {
117
119
  if (token.startsWith("MapOf(") && token.endsWith(")")) {
118
120
  return { MapOf: parseDriverToken(token.slice("MapOf(".length, -1)) };
119
121
  }
122
+ if (token.startsWith("Surfaced(") && token.endsWith(")")) {
123
+ return { Surfaced: parseDriverToken(token.slice("Surfaced(".length, -1)) };
124
+ }
120
125
  // `LwwBy(<principal>)` — the principal is an opaque non-empty string (uids carry
121
126
  // hyphens, so it is NOT the bare-identifier shape the unit-driver branch validates).
122
127
  if (token.startsWith("LwwBy(") && token.endsWith(")")) {
package/src/wire.ts CHANGED
@@ -219,7 +219,10 @@ export type WireDriver =
219
219
  | "Counter"
220
220
  | "LastPosition"
221
221
  | { LwwBy: string }
222
- | { MapOf: WireDriver };
222
+ | { MapOf: WireDriver }
223
+ // `Surfaced(Box<Driver>)` — the user-resolvable third merge posture
224
+ // (architecture/surfaced_merge_conflict.md): `{ "Surfaced": <inner> }`.
225
+ | { Surfaced: WireDriver };
223
226
 
224
227
  /** `Schema = BTreeMap<FieldId, Driver>` — a plain object, field -> Driver. */
225
228
  export type WireSchema = Record<string, WireDriver>;