@suluk/examples 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@suluk/examples",
3
+ "version": "0.1.0",
4
+ "description": "Example precedence + deterministic, origin-aware schema synthesis from a v4 'Suluk' contract. The shared, zero-dependency leaf both @suluk/journeys and @suluk/sdk read: resolveExample (public > maintainer > synthetic), a deterministic synthesizer, and the C041 field-origin discipline (x-suluk-origin input|sourced|computed + the wireable SourceRef). Pure, self-contained, faker-dep-free. CANDIDATE tooling.",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "license": "Apache-2.0",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/MahmoodKhalil57/suluk.git",
12
+ "directory": "tooling/ts/packages/examples"
13
+ },
14
+ "homepage": "https://github.com/MahmoodKhalil57/suluk#readme",
15
+ "bugs": "https://github.com/MahmoodKhalil57/suluk/issues",
16
+ "type": "module",
17
+ "main": "src/index.ts",
18
+ "exports": {
19
+ ".": "./src/index.ts"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "latest"
23
+ },
24
+ "scripts": {
25
+ "test": "bun test",
26
+ "typecheck": "tsc --noEmit -p ."
27
+ }
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,310 @@
1
+ /**
2
+ * @suluk/examples — example precedence + deterministic, origin-aware schema synthesis.
3
+ *
4
+ * The shared, ZERO-DEPENDENCY leaf both @suluk/journeys (BDD outlines + runnable suite) and @suluk/sdk (typed client
5
+ * metadata + sampling) read. Extracted from @suluk/journeys (C040-P2) when @suluk/sdk needed the same reader — journeys
6
+ * already depends on sdk, so the reader had to sit BELOW both (it was built self-contained for exactly this move).
7
+ *
8
+ * `resolveExample` picks a request/response example by precedence — a tester-curated PUBLIC example wins over a
9
+ * MAINTAINER example (an explicit one, or the schema's own `examples`/`example`/`const`), which wins over a SYNTHETIC
10
+ * value derived from the schema shape. The synthetic tier is a DETERMINISTIC synthesizer (no external faker dep): same
11
+ * (schema, hint) -> same value, so BDD tests don't flap, and a synthesized value is ALWAYS lowest-precedence, always
12
+ * overridable, and carries `synthetic: true`. That precedence IS the reconciliation with the journeys arc's
13
+ * never-launder discipline — nothing synthetic is ever presented as authoritative.
14
+ *
15
+ * This module imports NOTHING (no @suluk, no external) so it stays on the VALUE side of the C040 wall (a pure projector
16
+ * core must never import it) and is consumable by any package without a dependency cycle. Witnessed by test/wall.test.ts.
17
+ */
18
+
19
+ /** A JSON Schema 2020-12 object (the v4 inner-schema shape). Opaque-ish; we read a known subset. */
20
+ export type JsonSchema = Record<string, unknown>;
21
+
22
+ /** Which source supplied the resolved value. `public` (highest) > `maintainer` > `synthetic` (lowest). */
23
+ export type ExampleTier = "public" | "maintainer" | "synthetic";
24
+
25
+ /** The two human-authored tiers a caller may supply; the synthetic tier is derived from the schema. */
26
+ export interface ExampleSources {
27
+ /** tier 3 (highest) — a tester-curated, willing-to-expose example. After C040-P4 promotion it also lives in Zod meta. */
28
+ public?: unknown;
29
+ /** tier 2 — an explicit maintainer example (overrides the schema's own `examples`/`example`/`const`). */
30
+ maintainer?: unknown;
31
+ }
32
+
33
+ export interface ResolvedExample {
34
+ value: unknown;
35
+ /** which tier won. */
36
+ tier: ExampleTier;
37
+ /** true IFF the value was synthesized from the schema shape (the honest never-launder marker). */
38
+ synthetic: boolean;
39
+ /** a short, human-readable note on where the value came from (for reports / docs provenance). */
40
+ provenance: string;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------------------------------------------------
44
+ // Field origin (C041) — which fields a client may faker, which are sourced from elsewhere, which are server-computed.
45
+ // Authored in Zod `.meta({ "x-suluk-origin": ..., "x-suluk-from": ... })`, carried verbatim by zodToV4, read here only
46
+ // (the matcher never sees it). This makes the synthesizer CORRECT: it stops inventing ids/totals, and a `sourced` field
47
+ // becomes a machine-wireable edge both the journeys emitter and the @suluk/sdk generator can consume.
48
+ // ---------------------------------------------------------------------------------------------------------------------
49
+
50
+ export const ORIGIN_KEYWORD = "x-suluk-origin";
51
+ export const FROM_KEYWORD = "x-suluk-from";
52
+
53
+ /** `input` = the client is the authority (free, faker-able); `sourced` = retrieved elsewhere (wired); `computed` = server-derived. */
54
+ export type FieldOrigin = "input" | "sourced" | "computed";
55
+
56
+ /** A machine-wireable source edge for a `sourced` field: pull `select` (default "id") from operation `op`'s response. */
57
+ export interface SourceRef {
58
+ /** the source operation's v4 by-name handle (C009 identity: `op.name`). */
59
+ op: string;
60
+ /** a dotted path into the source op's RESPONSE to pull (default "id"). */
61
+ select?: string;
62
+ }
63
+
64
+ /** `x-suluk-from` is EITHER a free human note (string, doc-only) OR a structured, wireable `SourceRef`. */
65
+ export type FieldSource = string | SourceRef;
66
+
67
+ export interface FieldDescriptor {
68
+ name: string;
69
+ origin: FieldOrigin;
70
+ /** the raw `x-suluk-from` when it is a human note (string). */
71
+ from?: string;
72
+ /** the machine-wireable edge when `x-suluk-from` is structured `{ op, select? }`. */
73
+ source?: SourceRef;
74
+ /** true IFF a client may freely synthesize/fill it (origin === "input"). */
75
+ fakerable: boolean;
76
+ required: boolean;
77
+ }
78
+
79
+ /** Read a property's origin: explicit `x-suluk-origin` wins; else `readOnly` ⇒ `computed`; else default `input`. */
80
+ export function fieldOrigin(schema: JsonSchema | undefined): FieldOrigin {
81
+ if (!schema || typeof schema !== "object") return "input";
82
+ const o = schema[ORIGIN_KEYWORD];
83
+ if (o === "input" || o === "sourced" || o === "computed") return o;
84
+ if (schema.readOnly === true) return "computed";
85
+ return "input";
86
+ }
87
+
88
+ /** The structured source edge if `x-suluk-from` names an `op`; otherwise undefined (a free note is not wireable). */
89
+ export function asSourceRef(from: unknown): SourceRef | undefined {
90
+ if (from && typeof from === "object" && typeof (from as { op?: unknown }).op === "string") {
91
+ const r = from as { op: string; select?: unknown };
92
+ return { op: r.op, select: typeof r.select === "string" ? r.select : undefined };
93
+ }
94
+ return undefined;
95
+ }
96
+
97
+ /**
98
+ * Describe the TOP-LEVEL fields of an object schema by origin — the surface a client / the @suluk/sdk generator uses to
99
+ * know what it may freely fill (`fakerable`), what is wired from elsewhere (`source`), and what is server-computed.
100
+ */
101
+ export function describeInputs(schema: JsonSchema | undefined): FieldDescriptor[] {
102
+ const props = (schema?.properties ?? {}) as Record<string, JsonSchema>;
103
+ const required = new Set(Array.isArray(schema?.required) ? (schema!.required as string[]) : []);
104
+ return Object.entries(props).map(([name, sub]) => {
105
+ const from = sub[FROM_KEYWORD];
106
+ return {
107
+ name,
108
+ origin: fieldOrigin(sub),
109
+ from: typeof from === "string" ? from : undefined,
110
+ source: asSourceRef(from),
111
+ fakerable: fieldOrigin(sub) === "input",
112
+ required: required.has(name),
113
+ };
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Resolve a `sourced` field's value from a scenario-scoped bag of captured operation results (keyed by `op.name`). The
119
+ * shared primitive both the journeys emitter (carried-data across a journey) and an sdk chaining helper use. Pure.
120
+ */
121
+ export function resolveSourced(captured: Record<string, unknown>, ref: SourceRef): unknown {
122
+ let cur: unknown = captured[ref.op];
123
+ for (const seg of (ref.select ?? "id").split(".").filter(Boolean)) {
124
+ if (cur == null || typeof cur !== "object") return undefined;
125
+ cur = (cur as Record<string, unknown>)[seg];
126
+ }
127
+ return cur;
128
+ }
129
+
130
+ /** The maintainer example carried by the schema itself: `examples[0]` > `example` (3.x) > `const`. */
131
+ function schemaExample(schema: JsonSchema): { value: unknown; from: string } | undefined {
132
+ const exs = schema.examples;
133
+ if (Array.isArray(exs) && exs.length > 0) return { value: exs[0], from: "schema.examples" };
134
+ if ("example" in schema) return { value: schema.example, from: "schema.example" };
135
+ if ("const" in schema) return { value: schema.const, from: "schema.const" };
136
+ return undefined;
137
+ }
138
+
139
+ /**
140
+ * Resolve a single example by precedence. `hint` (typically the field/op name) only steers SYNTHETIC string values; it
141
+ * never changes which tier wins.
142
+ */
143
+ export function resolveExample(
144
+ schema: JsonSchema | undefined,
145
+ sources: ExampleSources = {},
146
+ hint = "value",
147
+ opts: SynthOptions = {},
148
+ ): ResolvedExample {
149
+ if (sources.public !== undefined) {
150
+ return { value: sources.public, tier: "public", synthetic: false, provenance: "tester-public" };
151
+ }
152
+ if (sources.maintainer !== undefined) {
153
+ return { value: sources.maintainer, tier: "maintainer", synthetic: false, provenance: "maintainer-explicit" };
154
+ }
155
+ const fromSchema = schema ? schemaExample(schema) : undefined;
156
+ if (fromSchema) {
157
+ return { value: fromSchema.value, tier: "maintainer", synthetic: false, provenance: fromSchema.from };
158
+ }
159
+ return { value: synthesize(schema ?? {}, hint, opts), tier: "synthetic", synthetic: true, provenance: "synthetic" };
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------------------------------------------------
163
+ // Deterministic synthesis — tier 1. No randomness, no Date.now: a fixed, readable representative per schema shape.
164
+ // ---------------------------------------------------------------------------------------------------------------------
165
+
166
+ const MAX_DEPTH = 6;
167
+
168
+ function num(v: unknown): number | undefined {
169
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
170
+ }
171
+
172
+ /** The effective primitive/compound kind: explicit `type` (first of an array) else inferred from `properties`/`items`. */
173
+ function pickType(schema: JsonSchema): string | undefined {
174
+ const t = schema.type;
175
+ if (typeof t === "string") return t;
176
+ if (Array.isArray(t) && t.length > 0 && typeof t[0] === "string") return t[0] as string;
177
+ if (schema.properties || schema.required || schema.additionalProperties) return "object";
178
+ if (schema.items) return "array";
179
+ return undefined;
180
+ }
181
+
182
+ /** Naive singularization for an array element hint (`files` -> `file`). Presentational only. */
183
+ function singular(hint: string): string {
184
+ return hint.endsWith("ies") ? `${hint.slice(0, -3)}y` : hint.endsWith("s") ? hint.slice(0, -1) : hint;
185
+ }
186
+
187
+ function synthString(schema: JsonSchema, hint: string): string {
188
+ const fmt = typeof schema.format === "string" ? schema.format : undefined;
189
+ switch (fmt) {
190
+ case "email":
191
+ return "user@example.com";
192
+ case "uuid":
193
+ return "00000000-0000-4000-8000-000000000000";
194
+ case "date-time":
195
+ return "2026-01-01T00:00:00Z";
196
+ case "date":
197
+ return "2026-01-01";
198
+ case "time":
199
+ return "00:00:00";
200
+ case "uri":
201
+ case "url":
202
+ case "uri-reference":
203
+ return "https://example.com";
204
+ case "hostname":
205
+ return "example.com";
206
+ case "ipv4":
207
+ return "192.0.2.1";
208
+ default:
209
+ break;
210
+ }
211
+ let s = hint && /^[a-z0-9_-]+$/i.test(hint) ? hint : "string";
212
+ const min = num(schema.minLength);
213
+ const max = num(schema.maxLength);
214
+ if (min !== undefined && s.length < min) s = s.padEnd(min, "x");
215
+ if (max !== undefined && s.length > max) s = s.slice(0, max);
216
+ return s;
217
+ }
218
+
219
+ function synthNumber(schema: JsonSchema, integer: boolean): number {
220
+ const min = num(schema.minimum);
221
+ const exclMin = num(schema.exclusiveMinimum);
222
+ const max = num(schema.maximum);
223
+ let v: number;
224
+ if (min !== undefined) v = min;
225
+ else if (exclMin !== undefined) v = exclMin + (integer ? 1 : 1);
226
+ else if (max !== undefined) v = max;
227
+ else v = integer ? 1 : 1;
228
+ return integer ? Math.trunc(v) : v;
229
+ }
230
+
231
+ /** Direction controls origin handling: a "request" example omits server-`computed` fields a client never sends; a
232
+ * "response" example omits `writeOnly` fields. Default "request". */
233
+ export type SynthDirection = "request" | "response";
234
+ export interface SynthOptions {
235
+ direction?: SynthDirection;
236
+ }
237
+
238
+ /**
239
+ * A deterministic, schema-shaped example value. `const`/`enum`/`default`/explicit `examples` win (so a synthesized
240
+ * object's fields respect pinned values); otherwise a fixed representative is chosen per type. Object fields are
241
+ * filtered by origin/direction (see SynthOptions). A `sourced` field IS synthesized (a type-valid representative) — the
242
+ * wiring layer overrides it via describeInputs/resolveSourced; it is never laundered as free input.
243
+ */
244
+ export function synthesize(schema: JsonSchema, hint = "value", opts: SynthOptions = {}): unknown {
245
+ return synthNode(schema ?? {}, hint, opts, 0);
246
+ }
247
+
248
+ function synthNode(schema: JsonSchema, hint: string, opts: SynthOptions, depth: number): unknown {
249
+ if (!schema || typeof schema !== "object") return null;
250
+
251
+ if ("const" in schema) return schema.const;
252
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
253
+ if (Array.isArray(schema.examples) && schema.examples.length > 0) return schema.examples[0];
254
+ if ("example" in schema) return schema.example;
255
+ if ("default" in schema) return schema.default;
256
+
257
+ // composition: merge allOf object branches; take the first anyOf/oneOf branch.
258
+ if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
259
+ const merged: JsonSchema = Object.assign({}, ...(schema.allOf as JsonSchema[]), schema);
260
+ delete merged.allOf;
261
+ return synthNode(merged, hint, opts, depth);
262
+ }
263
+ for (const key of ["anyOf", "oneOf"] as const) {
264
+ const parts = schema[key];
265
+ if (Array.isArray(parts) && parts.length > 0) return synthNode(parts[0] as JsonSchema, hint, opts, depth);
266
+ }
267
+
268
+ switch (pickType(schema)) {
269
+ case "string":
270
+ return synthString(schema, hint);
271
+ case "integer":
272
+ return synthNumber(schema, true);
273
+ case "number":
274
+ return synthNumber(schema, false);
275
+ case "boolean":
276
+ return true;
277
+ case "null":
278
+ return null;
279
+ case "array": {
280
+ if (depth >= MAX_DEPTH) return [];
281
+ const items = (schema.items ?? {}) as JsonSchema;
282
+ const count = Math.max(1, num(schema.minItems) ?? 1);
283
+ const out: unknown[] = [];
284
+ for (let i = 0; i < count; i++) out.push(synthNode(items, singular(hint), opts, depth + 1));
285
+ return out;
286
+ }
287
+ case "object":
288
+ return synthObject(schema, opts, depth);
289
+ default:
290
+ if (schema.properties) return synthObject(schema, opts, depth);
291
+ return `<${hint}>`;
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Synthesize the declared properties (a full example a tester can trim), filtered by origin/direction: a REQUEST example
297
+ * drops `computed` (server-derived) fields a client never sends; a RESPONSE example drops `writeOnly` fields.
298
+ */
299
+ function synthObject(schema: JsonSchema, opts: SynthOptions, depth: number): Record<string, unknown> {
300
+ if (depth >= MAX_DEPTH) return {};
301
+ const direction: SynthDirection = opts.direction ?? "request";
302
+ const props = (schema.properties ?? {}) as Record<string, JsonSchema>;
303
+ const out: Record<string, unknown> = {};
304
+ for (const [key, sub] of Object.entries(props)) {
305
+ if (direction === "request" && fieldOrigin(sub) === "computed") continue; // client never sends server-derived fields
306
+ if (direction === "response" && sub.writeOnly === true) continue; // write-only never appears in a response
307
+ out[key] = synthNode(sub, key, opts, depth + 1);
308
+ }
309
+ return out;
310
+ }
@@ -0,0 +1,151 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { resolveExample, synthesize, type JsonSchema } from "../src/index";
3
+
4
+ /**
5
+ * C040-P2 — the example precedence resolver + deterministic synthesizer (the witnessed slice).
6
+ * Precedence: tester-PUBLIC > MAINTAINER (explicit, or the schema's own examples/example/const) > SYNTHETIC.
7
+ * Synthetic is deterministic, always lowest, and always marked `synthetic: true` (never-launder).
8
+ */
9
+
10
+ const stringSchema: JsonSchema = { type: "string" };
11
+
12
+ describe("resolveExample — precedence", () => {
13
+ test("a tester-public example wins over everything", () => {
14
+ const r = resolveExample({ type: "string", examples: ["from-schema"] }, { public: "P", maintainer: "M" }, "name");
15
+ expect(r.value).toBe("P");
16
+ expect(r.tier).toBe("public");
17
+ expect(r.synthetic).toBe(false);
18
+ });
19
+
20
+ test("an explicit maintainer example wins over the schema example and synthetic", () => {
21
+ const r = resolveExample({ type: "string", examples: ["from-schema"] }, { maintainer: "M" }, "name");
22
+ expect(r.value).toBe("M");
23
+ expect(r.tier).toBe("maintainer");
24
+ });
25
+
26
+ test("the schema's own examples[] is the maintainer tier when no explicit source is given", () => {
27
+ const r = resolveExample({ type: "string", examples: ["from-schema"] }, {}, "name");
28
+ expect(r.value).toBe("from-schema");
29
+ expect(r.tier).toBe("maintainer");
30
+ expect(r.provenance).toBe("schema.examples");
31
+ });
32
+
33
+ test("`example` (3.x singular) and `const` are also maintainer tier", () => {
34
+ expect(resolveExample({ type: "string", example: "e" }).tier).toBe("maintainer");
35
+ expect(resolveExample({ const: 42 }).value).toBe(42);
36
+ expect(resolveExample({ const: 42 }).provenance).toBe("schema.const");
37
+ });
38
+
39
+ test("falls back to synthetic only when no human example exists — and marks it", () => {
40
+ const r = resolveExample(stringSchema, {}, "title");
41
+ expect(r.tier).toBe("synthetic");
42
+ expect(r.synthetic).toBe(true);
43
+ expect(r.provenance).toBe("synthetic");
44
+ expect(typeof r.value).toBe("string");
45
+ });
46
+
47
+ test("synthetic flag is true ONLY when synthesized", () => {
48
+ expect(resolveExample(stringSchema, { public: "x" }).synthetic).toBe(false);
49
+ expect(resolveExample(stringSchema, { maintainer: "x" }).synthetic).toBe(false);
50
+ expect(resolveExample({ type: "string", examples: ["x"] }).synthetic).toBe(false);
51
+ expect(resolveExample(stringSchema).synthetic).toBe(true);
52
+ });
53
+
54
+ test("a falsy public value (0, '', false) still counts as supplied — undefined does not", () => {
55
+ expect(resolveExample({ type: "integer" }, { public: 0 }).tier).toBe("public");
56
+ expect(resolveExample({ type: "string" }, { public: "" }).tier).toBe("public");
57
+ expect(resolveExample({ type: "boolean" }, { public: false }).tier).toBe("public");
58
+ expect(resolveExample({ type: "string" }, { public: undefined }).tier).toBe("synthetic");
59
+ });
60
+ });
61
+
62
+ describe("synthesize — deterministic, shape-honoring", () => {
63
+ test("respects const / enum / default (in that order)", () => {
64
+ expect(synthesize({ const: "C" })).toBe("C");
65
+ expect(synthesize({ enum: ["a", "b"] })).toBe("a");
66
+ expect(synthesize({ type: "string", default: "D" })).toBe("D");
67
+ });
68
+
69
+ test("string formats map to fixed representatives", () => {
70
+ expect(synthesize({ type: "string", format: "email" })).toBe("user@example.com");
71
+ expect(synthesize({ type: "string", format: "uuid" })).toBe("00000000-0000-4000-8000-000000000000");
72
+ expect(synthesize({ type: "string", format: "date-time" })).toBe("2026-01-01T00:00:00Z");
73
+ expect(synthesize({ type: "string", format: "uri" })).toBe("https://example.com");
74
+ });
75
+
76
+ test("string honors minLength / maxLength using the hint", () => {
77
+ expect(synthesize({ type: "string", minLength: 8 }, "id")).toHaveLength(8);
78
+ expect((synthesize({ type: "string", maxLength: 2 }, "name") as string).length).toBeLessThanOrEqual(2);
79
+ expect(synthesize({ type: "string" }, "title")).toBe("title");
80
+ });
81
+
82
+ test("integer/number honor minimum (and exclusiveMinimum)", () => {
83
+ expect(synthesize({ type: "integer", minimum: 100, maximum: 100_000 })).toBe(100);
84
+ expect(synthesize({ type: "integer", maximum: 5 })).toBe(5);
85
+ expect(synthesize({ type: "number", exclusiveMinimum: 0 })).toBeGreaterThan(0);
86
+ });
87
+
88
+ test("boolean and null", () => {
89
+ expect(synthesize({ type: "boolean" })).toBe(true);
90
+ expect(synthesize({ type: "null" })).toBeNull();
91
+ });
92
+
93
+ test("array honors minItems and recurses into items", () => {
94
+ const v = synthesize({ type: "array", items: { type: "string" }, minItems: 2 }, "tags") as unknown[];
95
+ expect(Array.isArray(v)).toBe(true);
96
+ expect(v).toHaveLength(2);
97
+ expect(typeof v[0]).toBe("string");
98
+ });
99
+
100
+ test("object synthesizes every declared property with the property name as hint", () => {
101
+ const v = synthesize({
102
+ type: "object",
103
+ properties: { email: { type: "string", format: "email" }, count: { type: "integer", minimum: 3 } },
104
+ }) as Record<string, unknown>;
105
+ expect(v).toEqual({ email: "user@example.com", count: 3 });
106
+ });
107
+
108
+ test("is fully deterministic — same schema yields a deep-equal value", () => {
109
+ const schema: JsonSchema = {
110
+ type: "object",
111
+ properties: { a: { type: "string" }, b: { type: "array", items: { type: "integer", minimum: 1 } } },
112
+ };
113
+ expect(synthesize(schema)).toEqual(synthesize(schema));
114
+ });
115
+ });
116
+
117
+ describe("synthesize — a realistic nested body (toolfactory ConvertSubtitleBody shape)", () => {
118
+ const ConvertBody: JsonSchema = {
119
+ type: "object",
120
+ required: ["to", "files"],
121
+ properties: {
122
+ to: { type: "string", enum: ["srt", "vtt", "ass"] },
123
+ from: { type: "string", enum: ["srt", "vtt"] },
124
+ requestId: { type: "string", minLength: 8, maxLength: 128 },
125
+ files: {
126
+ type: "array",
127
+ minItems: 1,
128
+ maxItems: 20,
129
+ items: {
130
+ type: "object",
131
+ required: ["name", "content"],
132
+ properties: {
133
+ name: { type: "string", minLength: 1, maxLength: 255 },
134
+ content: { type: "string", minLength: 1, maxLength: 1_000_000 },
135
+ },
136
+ },
137
+ },
138
+ },
139
+ };
140
+
141
+ test("produces a structurally valid, constraint-honoring seed row", () => {
142
+ const v = synthesize(ConvertBody) as any;
143
+ expect(v.to).toBe("srt"); // first enum
144
+ expect(typeof v.requestId).toBe("string");
145
+ expect(v.requestId.length).toBeGreaterThanOrEqual(8);
146
+ expect(Array.isArray(v.files)).toBe(true);
147
+ expect(v.files.length).toBe(1); // minItems
148
+ expect(typeof v.files[0].name).toBe("string");
149
+ expect(typeof v.files[0].content).toBe("string");
150
+ });
151
+ });
@@ -0,0 +1,139 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import {
3
+ fieldOrigin,
4
+ describeInputs,
5
+ asSourceRef,
6
+ resolveSourced,
7
+ synthesize,
8
+ type JsonSchema,
9
+ } from "../src/index";
10
+
11
+ /**
12
+ * C041 — field-origin discipline. `x-suluk-origin` (input|sourced|computed) + `x-suluk-from` authored in Zod .meta(),
13
+ * read here only. Makes the synthesizer CORRECT (stops inventing computed values) and makes a `sourced` field a
14
+ * machine-wireable edge the journeys emitter + the @suluk/sdk generator consume.
15
+ */
16
+
17
+ describe("fieldOrigin", () => {
18
+ test("explicit x-suluk-origin wins", () => {
19
+ expect(fieldOrigin({ type: "string", "x-suluk-origin": "sourced" })).toBe("sourced");
20
+ expect(fieldOrigin({ type: "integer", "x-suluk-origin": "computed" })).toBe("computed");
21
+ expect(fieldOrigin({ type: "string", "x-suluk-origin": "input" })).toBe("input");
22
+ });
23
+ test("readOnly ⇒ computed when no explicit origin", () => {
24
+ expect(fieldOrigin({ type: "string", readOnly: true })).toBe("computed");
25
+ });
26
+ test("explicit origin overrides readOnly", () => {
27
+ expect(fieldOrigin({ type: "string", readOnly: true, "x-suluk-origin": "input" })).toBe("input");
28
+ });
29
+ test("default is input (incl. unknown values and missing schema)", () => {
30
+ expect(fieldOrigin({ type: "string" })).toBe("input");
31
+ expect(fieldOrigin({ "x-suluk-origin": "bogus" })).toBe("input");
32
+ expect(fieldOrigin(undefined)).toBe("input");
33
+ });
34
+ });
35
+
36
+ describe("describeInputs — the client/SDK-facing surface", () => {
37
+ const schema: JsonSchema = {
38
+ type: "object",
39
+ required: ["amountCents", "subId"],
40
+ properties: {
41
+ amountCents: { type: "integer", minimum: 100, "x-suluk-origin": "input" },
42
+ subId: { type: "string", "x-suluk-origin": "sourced", "x-suluk-from": { op: "createSubscription", select: "id" } },
43
+ note: { type: "string", "x-suluk-origin": "sourced", "x-suluk-from": "from the create call" },
44
+ balance: { type: "integer", "x-suluk-origin": "computed", "x-suluk-from": "ledger sum" },
45
+ plain: { type: "string" },
46
+ },
47
+ };
48
+ const byName = Object.fromEntries(describeInputs(schema).map((d) => [d.name, d]));
49
+
50
+ test("classifies origin, fakerable, and required per field", () => {
51
+ expect(byName.amountCents).toMatchObject({ origin: "input", fakerable: true, required: true });
52
+ expect(byName.plain).toMatchObject({ origin: "input", fakerable: true, required: false });
53
+ expect(byName.balance).toMatchObject({ origin: "computed", fakerable: false });
54
+ });
55
+
56
+ test("a structured x-suluk-from becomes a wireable source edge", () => {
57
+ expect(byName.subId.source).toEqual({ op: "createSubscription", select: "id" });
58
+ expect(byName.subId.fakerable).toBe(false);
59
+ });
60
+
61
+ test("a free-string x-suluk-from is a note, not a wireable edge", () => {
62
+ expect(byName.note.from).toBe("from the create call");
63
+ expect(byName.note.source).toBeUndefined();
64
+ });
65
+ });
66
+
67
+ describe("asSourceRef", () => {
68
+ test("structured object with an op is a SourceRef", () => {
69
+ expect(asSourceRef({ op: "getX", select: "data.id" })).toEqual({ op: "getX", select: "data.id" });
70
+ expect(asSourceRef({ op: "getX" })).toEqual({ op: "getX", select: undefined });
71
+ });
72
+ test("a string or a shapeless object is not wireable", () => {
73
+ expect(asSourceRef("a note")).toBeUndefined();
74
+ expect(asSourceRef({ select: "id" })).toBeUndefined();
75
+ expect(asSourceRef(undefined)).toBeUndefined();
76
+ });
77
+ });
78
+
79
+ describe("resolveSourced — the shared wiring primitive (emitter + sdk chaining)", () => {
80
+ const captured = { createSubscription: { id: "sub_123", nested: { x: 7 } } };
81
+ test("default select is `id`", () => {
82
+ expect(resolveSourced(captured, { op: "createSubscription" })).toBe("sub_123");
83
+ });
84
+ test("a dotted select walks the captured response", () => {
85
+ expect(resolveSourced(captured, { op: "createSubscription", select: "nested.x" })).toBe(7);
86
+ });
87
+ test("a missing op or path resolves to undefined (never throws)", () => {
88
+ expect(resolveSourced(captured, { op: "missing" })).toBeUndefined();
89
+ expect(resolveSourced(captured, { op: "createSubscription", select: "nope.deep" })).toBeUndefined();
90
+ });
91
+ });
92
+
93
+ describe("synthesize — origin/direction aware", () => {
94
+ const body: JsonSchema = {
95
+ type: "object",
96
+ properties: {
97
+ amountCents: { type: "integer", minimum: 100, "x-suluk-origin": "input" },
98
+ subId: { type: "string", "x-suluk-origin": "sourced", "x-suluk-from": { op: "createSubscription", select: "id" } },
99
+ balance: { type: "integer", "x-suluk-origin": "computed" },
100
+ createdAt: { type: "string", readOnly: true },
101
+ },
102
+ };
103
+
104
+ test("a REQUEST example omits computed (and readOnly) fields a client never sends", () => {
105
+ const v = synthesize(body) as Record<string, unknown>;
106
+ expect(v).toHaveProperty("amountCents");
107
+ expect(v).toHaveProperty("subId");
108
+ expect(typeof v.subId).toBe("string");
109
+ expect(v).not.toHaveProperty("balance");
110
+ expect(v).not.toHaveProperty("createdAt");
111
+ });
112
+
113
+ test("a RESPONSE example includes computed/readOnly fields (they are output)", () => {
114
+ const v = synthesize(body, "body", { direction: "response" }) as Record<string, unknown>;
115
+ expect(v).toHaveProperty("balance");
116
+ expect(v).toHaveProperty("createdAt");
117
+ });
118
+
119
+ test("a RESPONSE example omits writeOnly fields", () => {
120
+ const v = synthesize(
121
+ { type: "object", properties: { token: { type: "string", writeOnly: true }, ok: { type: "boolean" } } },
122
+ "body",
123
+ { direction: "response" },
124
+ ) as Record<string, unknown>;
125
+ expect(v).not.toHaveProperty("token");
126
+ expect(v).toHaveProperty("ok");
127
+ });
128
+
129
+ test("computed fields are omitted at any depth on a request", () => {
130
+ const v = synthesize({
131
+ type: "object",
132
+ properties: {
133
+ item: { type: "object", properties: { name: { type: "string" }, total: { type: "number", "x-suluk-origin": "computed" } } },
134
+ },
135
+ }) as any;
136
+ expect(v.item).toHaveProperty("name");
137
+ expect(v.item).not.toHaveProperty("total");
138
+ });
139
+ });
@@ -0,0 +1,14 @@
1
+ import { test, expect } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ /**
6
+ * @suluk/examples is the SHARED LEAF — it must stay zero-dependency and self-contained so it can sit BELOW both
7
+ * @suluk/journeys and @suluk/sdk with no cycle, and on the VALUE side of the C040 wall (a pure projector core never
8
+ * imports it). This asserts src/index.ts imports NOTHING (no @suluk, no relative, no bare module).
9
+ */
10
+ test("src/index.ts is self-contained: no imports at all (zero-dep leaf)", () => {
11
+ const body = readFileSync(join(import.meta.dir, "..", "src", "index.ts"), "utf8");
12
+ expect(/^\s*import\s/m.test(body)).toBe(false);
13
+ expect(/from\s+["']/.test(body)).toBe(false);
14
+ });
package/tsconfig.json ADDED
@@ -0,0 +1 @@
1
+ { "extends": "../../tsconfig.base.json", "compilerOptions": { "types": ["bun"] }, "include": ["src", "test"] }