@boboddy/sdk 0.2.10-alpha → 0.2.12-alpha

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.
@@ -121,7 +121,7 @@ export declare function defineStep<TInput extends ZodType = ZodType, TResult ext
121
121
  sourcePath: string;
122
122
  key?: string;
123
123
  }> = never[], const TFeatures extends ReadonlyArray<AnyStepFeature> = never[]>(config: Omit<DefineStepInput<TInput, TResult>, "signals" | "features"> & {
124
- signals?: TSignals;
124
+ signals?: TSignals & readonly SignalSpecInput<TResult["_output"]>[];
125
125
  features?: TFeatures;
126
126
  }): TypedStepDefinitionSpec<TInput["_output"], TResult["_output"] & FeatureResultExtensions<TFeatures>, SignalKeysOf<TSignals> | FeatureSignalKeys<TFeatures>, SignalTypeMapOf<TSignals, TResult["_output"]>>;
127
127
  export {};
@@ -0,0 +1,2 @@
1
+ export * from "./json-schema-paths";
2
+ export * from "./validate-definition-specs";
@@ -0,0 +1,409 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/definitions/validation/json-schema-paths.ts
18
+ var SEGMENT_PATTERN = /([^.[\]]+)|(\[(\d+)\])/g;
19
+ var NUMERIC_SEGMENT = /^\d+$/;
20
+ var SCALAR_TYPES = new Set([
21
+ "string",
22
+ "number",
23
+ "integer",
24
+ "boolean",
25
+ "null"
26
+ ]);
27
+ var MAX_REF_HOPS = 16;
28
+ var MAX_CANDIDATES = 64;
29
+ function parseSourcePath(sourcePath) {
30
+ const trimmed = sourcePath.trim();
31
+ const normalized = trimmed === "$" ? "" : trimmed.startsWith("$.") ? trimmed.slice(2) : trimmed;
32
+ if (!normalized)
33
+ return [];
34
+ return [...normalized.matchAll(SEGMENT_PATTERN)].map((match) => match[1] ?? match[3]).filter((segment) => Boolean(segment));
35
+ }
36
+ function isSchemaNode(value) {
37
+ return typeof value === "boolean" || typeof value === "object" && value !== null && !Array.isArray(value);
38
+ }
39
+ function asRecord(node) {
40
+ return typeof node === "boolean" ? null : node;
41
+ }
42
+ function resolveRef(root, ref) {
43
+ if (!ref.startsWith("#"))
44
+ return null;
45
+ const pointer = ref.slice(1);
46
+ if (pointer === "" || pointer === "/")
47
+ return root;
48
+ if (!pointer.startsWith("/"))
49
+ return null;
50
+ let current = root;
51
+ for (const rawToken of pointer.slice(1).split("/")) {
52
+ const token = rawToken.replace(/~1/g, "/").replace(/~0/g, "~");
53
+ if (typeof current !== "object" || current === null)
54
+ return null;
55
+ current = current[token];
56
+ }
57
+ return isSchemaNode(current) ? current : null;
58
+ }
59
+ function flatten(node, root) {
60
+ const out = [];
61
+ const queue = [
62
+ { node, hops: 0 }
63
+ ];
64
+ while (queue.length > 0) {
65
+ const entry = queue.shift();
66
+ if (!entry)
67
+ break;
68
+ if (out.length >= MAX_CANDIDATES)
69
+ return null;
70
+ if (entry.hops > MAX_REF_HOPS)
71
+ return null;
72
+ const record = asRecord(entry.node);
73
+ if (!record) {
74
+ out.push(entry.node);
75
+ continue;
76
+ }
77
+ const ref = record["$ref"];
78
+ if (typeof ref === "string") {
79
+ const target = resolveRef(root, ref);
80
+ if (!target)
81
+ return null;
82
+ queue.push({ node: target, hops: entry.hops + 1 });
83
+ continue;
84
+ }
85
+ const branches = ["anyOf", "oneOf", "allOf"].flatMap((keyword) => {
86
+ const value = record[keyword];
87
+ return Array.isArray(value) ? value.filter(isSchemaNode) : [];
88
+ });
89
+ if (branches.length > 0) {
90
+ for (const branch of branches) {
91
+ queue.push({ node: branch, hops: entry.hops + 1 });
92
+ }
93
+ continue;
94
+ }
95
+ out.push(entry.node);
96
+ }
97
+ return out;
98
+ }
99
+ function typeNames(record) {
100
+ const raw = record["type"];
101
+ if (typeof raw === "string")
102
+ return new Set([raw]);
103
+ if (Array.isArray(raw)) {
104
+ return new Set(raw.filter((entry) => typeof entry === "string"));
105
+ }
106
+ return new Set;
107
+ }
108
+ var INDETERMINATE = { kind: "indeterminate" };
109
+ function stepIntoObject(record, segment) {
110
+ const properties = asRecord(isSchemaNode(record["properties"]) ? record["properties"] : {});
111
+ const declared = properties ?? {};
112
+ const child = declared[segment];
113
+ if (isSchemaNode(child))
114
+ return { kind: "child", node: child };
115
+ if (record["patternProperties"] !== undefined)
116
+ return INDETERMINATE;
117
+ if (record["additionalProperties"] !== false)
118
+ return INDETERMINATE;
119
+ return {
120
+ kind: "invalid",
121
+ reason: "unknown-property",
122
+ availablePaths: Object.keys(declared).sort()
123
+ };
124
+ }
125
+ function stepIntoArray(record, segment) {
126
+ if (!NUMERIC_SEGMENT.test(segment)) {
127
+ return {
128
+ kind: "invalid",
129
+ reason: "not-an-array-index",
130
+ availablePaths: []
131
+ };
132
+ }
133
+ const prefixItems = record["prefixItems"];
134
+ if (Array.isArray(prefixItems)) {
135
+ const positional = prefixItems[Number(segment)];
136
+ if (isSchemaNode(positional))
137
+ return { kind: "child", node: positional };
138
+ }
139
+ const items = record["items"];
140
+ if (isSchemaNode(items))
141
+ return { kind: "child", node: items };
142
+ return INDETERMINATE;
143
+ }
144
+ function stepInto(node, segment) {
145
+ const record = asRecord(node);
146
+ if (!record || Object.keys(record).length === 0)
147
+ return INDETERMINATE;
148
+ const types = typeNames(record);
149
+ const objectLike = types.has("object") || record["properties"] !== undefined || record["patternProperties"] !== undefined || record["additionalProperties"] !== undefined;
150
+ const arrayLike = types.has("array") || record["items"] !== undefined || record["prefixItems"] !== undefined;
151
+ const outcomes = [];
152
+ if (objectLike)
153
+ outcomes.push(stepIntoObject(record, segment));
154
+ if (arrayLike)
155
+ outcomes.push(stepIntoArray(record, segment));
156
+ if (outcomes.length === 0) {
157
+ if (types.size > 0 && [...types].every((name) => SCALAR_TYPES.has(name))) {
158
+ return {
159
+ kind: "invalid",
160
+ reason: "scalar-has-no-members",
161
+ availablePaths: []
162
+ };
163
+ }
164
+ return INDETERMINATE;
165
+ }
166
+ return combine(outcomes);
167
+ }
168
+ function combine(outcomes) {
169
+ const children = outcomes.filter((outcome) => outcome.kind === "child");
170
+ if (children.length > 0)
171
+ return children[0] ?? INDETERMINATE;
172
+ if (outcomes.some((outcome) => outcome.kind === "indeterminate")) {
173
+ return INDETERMINATE;
174
+ }
175
+ const invalid = outcomes.filter((outcome) => outcome.kind === "invalid");
176
+ const first = invalid[0];
177
+ if (!first)
178
+ return INDETERMINATE;
179
+ return {
180
+ kind: "invalid",
181
+ reason: first.reason,
182
+ availablePaths: [
183
+ ...new Set(invalid.flatMap((outcome) => outcome.availablePaths))
184
+ ].sort()
185
+ };
186
+ }
187
+ function enumeratePaths(node, root, maxDepth = 3, limit = 40) {
188
+ const out = [];
189
+ const visit = (current, prefix, depth) => {
190
+ if (out.length >= limit || depth > maxDepth)
191
+ return;
192
+ for (const branch of flatten(current, root) ?? []) {
193
+ const record = asRecord(branch);
194
+ const properties = record ? asRecord(isSchemaNode(record["properties"]) ? record["properties"] : {}) : null;
195
+ if (!properties)
196
+ continue;
197
+ for (const [key, child] of Object.entries(properties)) {
198
+ if (out.length >= limit)
199
+ return;
200
+ const path = prefix ? `${prefix}.${key}` : key;
201
+ out.push(path);
202
+ if (isSchemaNode(child))
203
+ visit(child, path, depth + 1);
204
+ }
205
+ }
206
+ };
207
+ visit(node, "", 1);
208
+ return [...new Set(out)].sort();
209
+ }
210
+ function resolveSourcePath(schema, sourcePath) {
211
+ const segments = parseSourcePath(sourcePath);
212
+ if (segments.length === 0)
213
+ return { kind: "resolved" };
214
+ let candidates = [schema];
215
+ let resolvedPrefix = "";
216
+ for (const segment of segments) {
217
+ const expanded = candidates.flatMap((node) => flatten(node, schema) ?? []);
218
+ if (expanded.length === 0)
219
+ return { kind: "indeterminate" };
220
+ const outcome = combine(expanded.map((node) => stepInto(node, segment)));
221
+ if (outcome.kind === "indeterminate")
222
+ return { kind: "indeterminate" };
223
+ if (outcome.kind === "invalid") {
224
+ const availablePaths = [
225
+ ...new Set(expanded.flatMap((node) => enumeratePaths(node, schema)))
226
+ ].sort();
227
+ return {
228
+ kind: "invalid",
229
+ resolvedPrefix,
230
+ segment,
231
+ reason: outcome.reason,
232
+ availablePaths
233
+ };
234
+ }
235
+ candidates = [outcome.node];
236
+ resolvedPrefix = resolvedPrefix ? `${resolvedPrefix}.${segment}` : segment;
237
+ }
238
+ return { kind: "resolved" };
239
+ }
240
+ // src/definitions/validation/validate-definition-specs.ts
241
+ function listPaths(paths, limit = 24) {
242
+ if (paths.length === 0)
243
+ return "";
244
+ if (paths.length <= limit)
245
+ return paths.join(", ");
246
+ return `${paths.slice(0, limit).join(", ")}, \u2026 (${String(paths.length - limit)} more)`;
247
+ }
248
+ function quotedOrRoot(prefix) {
249
+ return prefix ? `"${prefix}"` : "the result root";
250
+ }
251
+ function checkSignalSourcePaths(steps) {
252
+ const issues = [];
253
+ for (const step of steps) {
254
+ const schema = step.resultSchemaJson ?? null;
255
+ if (!schema)
256
+ continue;
257
+ for (const signal of step.signalExtractorDefinitions) {
258
+ const resolution = resolveSourcePath(schema, signal.sourcePath);
259
+ if (resolution.kind !== "invalid")
260
+ continue;
261
+ const { resolvedPrefix, segment, reason, availablePaths } = resolution;
262
+ const cause = reason === "not-an-array-index" ? `${quotedOrRoot(resolvedPrefix)} is an array, so "${segment}" can never index it \u2014 array segments must be numeric (e.g. "${resolvedPrefix}[0]")` : reason === "scalar-has-no-members" ? `${quotedOrRoot(resolvedPrefix)} is a scalar, so it has no property "${segment}"` : `${quotedOrRoot(resolvedPrefix)} has no property "${segment}"`;
263
+ const suffix = availablePaths.length > 0 ? ` Valid sourcePaths ${resolvedPrefix ? `under "${resolvedPrefix}"` : "for this step"}: ${listPaths(availablePaths)}.` : "";
264
+ issues.push({
265
+ check: "signal-source-path",
266
+ message: `Step "${step.key}" declares signal "${signal.key}" with sourcePath ` + `"${signal.sourcePath}", which can never resolve against the step's ` + `result schema: ${cause}.${suffix}`
267
+ });
268
+ }
269
+ }
270
+ return issues;
271
+ }
272
+ function routeTargets(policy) {
273
+ const keys = [];
274
+ if (policy.defaultEventType === "route" && typeof policy.defaultEventParamsJson?.["pipelineKey"] === "string") {
275
+ keys.push(policy.defaultEventParamsJson["pipelineKey"]);
276
+ }
277
+ for (const rule of policy.rulesJson.rules) {
278
+ if (rule.event.type === "route" && typeof rule.event.params?.["pipelineKey"] === "string") {
279
+ keys.push(rule.event.params["pipelineKey"]);
280
+ }
281
+ }
282
+ return keys;
283
+ }
284
+ function checkRouteTargets(pipelines, knownPipelineKeys) {
285
+ const issues = [];
286
+ const known = new Set([
287
+ ...pipelines.map((pipeline) => pipeline.key),
288
+ ...knownPipelineKeys
289
+ ]);
290
+ for (const pipeline of pipelines) {
291
+ for (const step of pipeline.steps) {
292
+ for (const target of routeTargets(step.advancementPolicyDefinition)) {
293
+ if (known.has(target))
294
+ continue;
295
+ issues.push({
296
+ check: "route-target",
297
+ message: `Pipeline "${pipeline.key}" step "${step.stepKey}" routes to pipeline ` + `"${target}", but no pipeline with that key was found on the server or ` + `in the current push batch. Push the target pipeline first.`
298
+ });
299
+ }
300
+ }
301
+ }
302
+ return issues;
303
+ }
304
+ function executionRanks(steps) {
305
+ const positions = steps.map((step) => step.position);
306
+ const usable = positions.every((value) => Number.isInteger(value) && value > 0) && new Set(positions).size === positions.length;
307
+ const indexes = steps.map((_, index) => index);
308
+ const ordered = usable ? [...indexes].sort((left, right) => (positions[left] ?? 0) - (positions[right] ?? 0)) : indexes;
309
+ return new Map(ordered.map((index, rank) => [index, rank]));
310
+ }
311
+ function bindingSource(binding) {
312
+ if (binding.source === "step_signal") {
313
+ return { stepKey: binding.stepKey, signalKey: binding.signalKey };
314
+ }
315
+ if (binding.source === "step_output") {
316
+ return { stepKey: binding.stepKey, signalKey: null };
317
+ }
318
+ return null;
319
+ }
320
+ function declaredSignalKeys(stepKey, stepsByKey, pipelineSteps) {
321
+ const specs = stepsByKey.get(stepKey);
322
+ if (!specs || specs.length === 0)
323
+ return null;
324
+ const keys = new Set;
325
+ for (const spec of specs) {
326
+ for (const signal of spec.signalExtractorDefinitions)
327
+ keys.add(signal.key);
328
+ }
329
+ for (const step of pipelineSteps) {
330
+ if (step.stepKey !== stepKey)
331
+ continue;
332
+ for (const computed of step.computedSignalDefinitions)
333
+ keys.add(computed.key);
334
+ }
335
+ return [...keys];
336
+ }
337
+ function checkSignalBindings(pipelines, stepsByKey) {
338
+ const issues = [];
339
+ for (const pipeline of pipelines) {
340
+ const ranks = executionRanks(pipeline.steps);
341
+ const order = [...pipeline.steps.keys()].sort((left, right) => (ranks.get(left) ?? 0) - (ranks.get(right) ?? 0)).map((index) => pipeline.steps[index]?.stepKey ?? "");
342
+ const orderHint = `Steps in "${pipeline.key}", in order: ${order.join(" \u2192 ")}.`;
343
+ pipeline.steps.forEach((step, index) => {
344
+ const consumerRank = ranks.get(index) ?? index;
345
+ const where = `Pipeline "${pipeline.key}" step "${step.stepKey}"`;
346
+ for (const [field, binding] of Object.entries(step.inputBindingsJson)) {
347
+ const source = bindingSource(binding);
348
+ if (!source)
349
+ continue;
350
+ const what = source.signalKey ? `binds input "${field}" to signal "${source.signalKey}" of step "${source.stepKey}"` : `binds input "${field}" to the output of step "${source.stepKey}"`;
351
+ const producerRanks = pipeline.steps.map((candidate, candidateIndex) => candidate.stepKey === source.stepKey ? ranks.get(candidateIndex) ?? candidateIndex : null).filter((rank) => rank !== null);
352
+ if (producerRanks.length === 0) {
353
+ issues.push({
354
+ check: "signal-binding",
355
+ message: `${where} ${what}, but no step with that key is in the pipeline. ${orderHint}`
356
+ });
357
+ continue;
358
+ }
359
+ if (!producerRanks.some((rank) => rank < consumerRank)) {
360
+ issues.push({
361
+ check: "signal-binding",
362
+ message: `${where} ${what}, but that step does not run before it, so the ` + `value will never exist. ${orderHint}`
363
+ });
364
+ continue;
365
+ }
366
+ if (source.signalKey === null)
367
+ continue;
368
+ const available = declaredSignalKeys(source.stepKey, stepsByKey, pipeline.steps);
369
+ if (available === null || available.includes(source.signalKey))
370
+ continue;
371
+ issues.push({
372
+ check: "signal-binding",
373
+ message: `${where} ${what}, but "${source.stepKey}" declares no such signal. ` + `Signals on "${source.stepKey}": ${available.length > 0 ? listPaths([...available].sort()) : "(none)"}.`
374
+ });
375
+ }
376
+ });
377
+ }
378
+ return issues;
379
+ }
380
+ function validateDefinitionSpecs(specs, options = {}) {
381
+ const stepsByKey = new Map;
382
+ for (const step of specs.steps) {
383
+ const existing = stepsByKey.get(step.key);
384
+ if (existing)
385
+ existing.push(step);
386
+ else
387
+ stepsByKey.set(step.key, [step]);
388
+ }
389
+ return [
390
+ ...checkSignalSourcePaths(specs.steps),
391
+ ...checkRouteTargets(specs.pipelines, options.knownPipelineKeys ?? []),
392
+ ...checkSignalBindings(specs.pipelines, stepsByKey)
393
+ ];
394
+ }
395
+ function assertValidDefinitionSpecs(specs, options = {}) {
396
+ const issues = validateDefinitionSpecs(specs, options);
397
+ if (issues.length === 0)
398
+ return;
399
+ const header = issues.length === 1 ? "Definition validation failed:" : `Definition validation failed with ${String(issues.length)} problems:`;
400
+ throw new Error([header, ...issues.map((issue) => ` \u2022 ${issue.message}`)].join(`
401
+ `));
402
+ }
403
+ export {
404
+ validateDefinitionSpecs,
405
+ resolveSourcePath,
406
+ parseSourcePath,
407
+ enumeratePaths,
408
+ assertValidDefinitionSpecs
409
+ };
@@ -0,0 +1,45 @@
1
+ /** A JSON Schema node. `true` / `false` are the boolean schema forms. */
2
+ export type JsonSchemaNode = boolean | {
3
+ readonly [key: string]: unknown;
4
+ };
5
+ export type PathFailureReason =
6
+ /** The parent is a closed object with no such property. */
7
+ "unknown-property"
8
+ /** The parent is an array and the segment is not a numeric index. */
9
+ | "not-an-array-index"
10
+ /** The parent is a scalar, so it has no members at all. */
11
+ | "scalar-has-no-members";
12
+ export type PathResolution =
13
+ /** The whole path resolves to a node in the schema. */
14
+ {
15
+ readonly kind: "resolved";
16
+ }
17
+ /** Neither provably valid nor provably invalid — the schema is too loose. */
18
+ | {
19
+ readonly kind: "indeterminate";
20
+ } | {
21
+ readonly kind: "invalid";
22
+ /** Dot path of the prefix that did resolve; `""` at the root. */
23
+ readonly resolvedPrefix: string;
24
+ /** The first segment that could not resolve. */
25
+ readonly segment: string;
26
+ readonly reason: PathFailureReason;
27
+ /** Paths that DO resolve below `resolvedPrefix`, relative to it. */
28
+ readonly availablePaths: readonly string[];
29
+ };
30
+ /** Splits a `sourcePath` exactly as the runtime signal extractor does. */
31
+ export declare function parseSourcePath(sourcePath: string): string[];
32
+ /**
33
+ * Enumerates dot paths that resolve below `node`, for error messages.
34
+ *
35
+ * Depth-limited and count-limited: this is a hint for a human reading a push
36
+ * failure, not an exhaustive schema dump. Arrays are treated as leaves.
37
+ */
38
+ export declare function enumeratePaths(node: JsonSchemaNode, root: JsonSchemaNode, maxDepth?: number, limit?: number): string[];
39
+ /**
40
+ * Whether `sourcePath` can resolve against `schema`.
41
+ *
42
+ * Pass the step's `resultSchemaJson` as both the node and the document root;
43
+ * `$ref`s are resolved against the root.
44
+ */
45
+ export declare function resolveSourcePath(schema: JsonSchemaNode, sourcePath: string): PathResolution;
@@ -0,0 +1,25 @@
1
+ import type { PipelineDefinitionSpec } from "../pipelines/define-pipeline";
2
+ import type { StepDefinitionSpec } from "../steps/define-step";
3
+ export type DefinitionSpecSet = {
4
+ readonly pipelines: readonly PipelineDefinitionSpec[];
5
+ readonly steps: readonly StepDefinitionSpec[];
6
+ };
7
+ export type ValidateDefinitionSpecsOptions = {
8
+ /**
9
+ * Pipeline keys that exist outside this batch — in practice, the keys already
10
+ * on the server. Route targets may name these as well as keys in
11
+ * `specs.pipelines`. Omit it and only the batch counts.
12
+ */
13
+ readonly knownPipelineKeys?: readonly string[];
14
+ };
15
+ export type DefinitionValidationIssue = {
16
+ readonly check: "signal-source-path" | "route-target" | "signal-binding";
17
+ readonly message: string;
18
+ };
19
+ /**
20
+ * Runs every offline check over a batch of definitions and returns the issues
21
+ * found, in check order. An empty array means the batch is clean.
22
+ */
23
+ export declare function validateDefinitionSpecs(specs: DefinitionSpecSet, options?: ValidateDefinitionSpecsOptions): DefinitionValidationIssue[];
24
+ /** `validateDefinitionSpecs`, but throws a single aggregated error. */
25
+ export declare function assertValidDefinitionSpecs(specs: DefinitionSpecSet, options?: ValidateDefinitionSpecsOptions): void;
@@ -0,0 +1,21 @@
1
+ import type { PipelineDefinitionSpec } from "../definitions/pipelines";
2
+ import { type DefaultPipelineAssignmentSpec } from "../definitions/pipelines/define-default-pipeline-assignment";
3
+ import type { StepDefinitionSpec } from "../definitions/steps";
4
+ export type CollectedDefinitions = {
5
+ readonly pipelines: readonly PipelineDefinitionSpec[];
6
+ /** Deduped by `key@vN`; named exports take precedence over embedded steps. */
7
+ readonly steps: readonly StepDefinitionSpec[];
8
+ /** Present only when `default-pipeline-assignment.ts` exists in the directory. */
9
+ readonly defaultPipelineAssignment: DefaultPipelineAssignmentSpec | null;
10
+ };
11
+ /**
12
+ * Imports every `.ts`/`.js` file in `dir` (except the push script itself and
13
+ * `default-pipeline-assignment.ts`) and collects the pipeline and step
14
+ * definitions they export. The assignment file, when present, is imported and
15
+ * validated too but returned separately — syncing it needs the server.
16
+ *
17
+ * Designed to run on the user's native runtime (bun, node-with-tsx, deno), NOT
18
+ * inside a `bun --compile`'d binary — that runtime can't resolve scoped package
19
+ * `exports` field remappings from external user files.
20
+ */
21
+ export declare function collectDefinitionsFromDirectory(dir: string): Promise<CollectedDefinitions>;
@@ -1,2 +1,4 @@
1
+ export { collectDefinitionsFromDirectory } from "./collect-definitions";
2
+ export type { CollectedDefinitions } from "./collect-definitions";
1
3
  export { pushFromDirectory } from "./push-from-directory";
2
4
  export type { PushFromDirectoryOptions, PushFromDirectoryResult, } from "./push-from-directory";
@@ -16784,10 +16784,397 @@ function isDefaultPipelineAssignmentSpec(value) {
16784
16784
  return false;
16785
16785
  return value["_tag"] === "default_pipeline_assignment";
16786
16786
  }
16787
- // src/push/push-from-directory.ts
16787
+ // src/definitions/validation/json-schema-paths.ts
16788
+ var SEGMENT_PATTERN = /([^.[\]]+)|(\[(\d+)\])/g;
16789
+ var NUMERIC_SEGMENT = /^\d+$/;
16790
+ var SCALAR_TYPES = new Set([
16791
+ "string",
16792
+ "number",
16793
+ "integer",
16794
+ "boolean",
16795
+ "null"
16796
+ ]);
16797
+ var MAX_REF_HOPS = 16;
16798
+ var MAX_CANDIDATES = 64;
16799
+ function parseSourcePath(sourcePath) {
16800
+ const trimmed = sourcePath.trim();
16801
+ const normalized = trimmed === "$" ? "" : trimmed.startsWith("$.") ? trimmed.slice(2) : trimmed;
16802
+ if (!normalized)
16803
+ return [];
16804
+ return [...normalized.matchAll(SEGMENT_PATTERN)].map((match) => match[1] ?? match[3]).filter((segment) => Boolean(segment));
16805
+ }
16806
+ function isSchemaNode(value) {
16807
+ return typeof value === "boolean" || typeof value === "object" && value !== null && !Array.isArray(value);
16808
+ }
16809
+ function asRecord(node) {
16810
+ return typeof node === "boolean" ? null : node;
16811
+ }
16812
+ function resolveRef2(root, ref) {
16813
+ if (!ref.startsWith("#"))
16814
+ return null;
16815
+ const pointer = ref.slice(1);
16816
+ if (pointer === "" || pointer === "/")
16817
+ return root;
16818
+ if (!pointer.startsWith("/"))
16819
+ return null;
16820
+ let current = root;
16821
+ for (const rawToken of pointer.slice(1).split("/")) {
16822
+ const token = rawToken.replace(/~1/g, "/").replace(/~0/g, "~");
16823
+ if (typeof current !== "object" || current === null)
16824
+ return null;
16825
+ current = current[token];
16826
+ }
16827
+ return isSchemaNode(current) ? current : null;
16828
+ }
16829
+ function flatten(node, root) {
16830
+ const out = [];
16831
+ const queue = [
16832
+ { node, hops: 0 }
16833
+ ];
16834
+ while (queue.length > 0) {
16835
+ const entry = queue.shift();
16836
+ if (!entry)
16837
+ break;
16838
+ if (out.length >= MAX_CANDIDATES)
16839
+ return null;
16840
+ if (entry.hops > MAX_REF_HOPS)
16841
+ return null;
16842
+ const record2 = asRecord(entry.node);
16843
+ if (!record2) {
16844
+ out.push(entry.node);
16845
+ continue;
16846
+ }
16847
+ const ref = record2["$ref"];
16848
+ if (typeof ref === "string") {
16849
+ const target = resolveRef2(root, ref);
16850
+ if (!target)
16851
+ return null;
16852
+ queue.push({ node: target, hops: entry.hops + 1 });
16853
+ continue;
16854
+ }
16855
+ const branches = ["anyOf", "oneOf", "allOf"].flatMap((keyword) => {
16856
+ const value = record2[keyword];
16857
+ return Array.isArray(value) ? value.filter(isSchemaNode) : [];
16858
+ });
16859
+ if (branches.length > 0) {
16860
+ for (const branch of branches) {
16861
+ queue.push({ node: branch, hops: entry.hops + 1 });
16862
+ }
16863
+ continue;
16864
+ }
16865
+ out.push(entry.node);
16866
+ }
16867
+ return out;
16868
+ }
16869
+ function typeNames(record2) {
16870
+ const raw = record2["type"];
16871
+ if (typeof raw === "string")
16872
+ return new Set([raw]);
16873
+ if (Array.isArray(raw)) {
16874
+ return new Set(raw.filter((entry) => typeof entry === "string"));
16875
+ }
16876
+ return new Set;
16877
+ }
16878
+ var INDETERMINATE = { kind: "indeterminate" };
16879
+ function stepIntoObject(record2, segment) {
16880
+ const properties = asRecord(isSchemaNode(record2["properties"]) ? record2["properties"] : {});
16881
+ const declared = properties ?? {};
16882
+ const child = declared[segment];
16883
+ if (isSchemaNode(child))
16884
+ return { kind: "child", node: child };
16885
+ if (record2["patternProperties"] !== undefined)
16886
+ return INDETERMINATE;
16887
+ if (record2["additionalProperties"] !== false)
16888
+ return INDETERMINATE;
16889
+ return {
16890
+ kind: "invalid",
16891
+ reason: "unknown-property",
16892
+ availablePaths: Object.keys(declared).sort()
16893
+ };
16894
+ }
16895
+ function stepIntoArray(record2, segment) {
16896
+ if (!NUMERIC_SEGMENT.test(segment)) {
16897
+ return {
16898
+ kind: "invalid",
16899
+ reason: "not-an-array-index",
16900
+ availablePaths: []
16901
+ };
16902
+ }
16903
+ const prefixItems = record2["prefixItems"];
16904
+ if (Array.isArray(prefixItems)) {
16905
+ const positional = prefixItems[Number(segment)];
16906
+ if (isSchemaNode(positional))
16907
+ return { kind: "child", node: positional };
16908
+ }
16909
+ const items = record2["items"];
16910
+ if (isSchemaNode(items))
16911
+ return { kind: "child", node: items };
16912
+ return INDETERMINATE;
16913
+ }
16914
+ function stepInto(node, segment) {
16915
+ const record2 = asRecord(node);
16916
+ if (!record2 || Object.keys(record2).length === 0)
16917
+ return INDETERMINATE;
16918
+ const types = typeNames(record2);
16919
+ const objectLike = types.has("object") || record2["properties"] !== undefined || record2["patternProperties"] !== undefined || record2["additionalProperties"] !== undefined;
16920
+ const arrayLike = types.has("array") || record2["items"] !== undefined || record2["prefixItems"] !== undefined;
16921
+ const outcomes = [];
16922
+ if (objectLike)
16923
+ outcomes.push(stepIntoObject(record2, segment));
16924
+ if (arrayLike)
16925
+ outcomes.push(stepIntoArray(record2, segment));
16926
+ if (outcomes.length === 0) {
16927
+ if (types.size > 0 && [...types].every((name) => SCALAR_TYPES.has(name))) {
16928
+ return {
16929
+ kind: "invalid",
16930
+ reason: "scalar-has-no-members",
16931
+ availablePaths: []
16932
+ };
16933
+ }
16934
+ return INDETERMINATE;
16935
+ }
16936
+ return combine(outcomes);
16937
+ }
16938
+ function combine(outcomes) {
16939
+ const children = outcomes.filter((outcome) => outcome.kind === "child");
16940
+ if (children.length > 0)
16941
+ return children[0] ?? INDETERMINATE;
16942
+ if (outcomes.some((outcome) => outcome.kind === "indeterminate")) {
16943
+ return INDETERMINATE;
16944
+ }
16945
+ const invalid = outcomes.filter((outcome) => outcome.kind === "invalid");
16946
+ const first = invalid[0];
16947
+ if (!first)
16948
+ return INDETERMINATE;
16949
+ return {
16950
+ kind: "invalid",
16951
+ reason: first.reason,
16952
+ availablePaths: [
16953
+ ...new Set(invalid.flatMap((outcome) => outcome.availablePaths))
16954
+ ].sort()
16955
+ };
16956
+ }
16957
+ function enumeratePaths(node, root, maxDepth = 3, limit = 40) {
16958
+ const out = [];
16959
+ const visit = (current, prefix, depth) => {
16960
+ if (out.length >= limit || depth > maxDepth)
16961
+ return;
16962
+ for (const branch of flatten(current, root) ?? []) {
16963
+ const record2 = asRecord(branch);
16964
+ const properties = record2 ? asRecord(isSchemaNode(record2["properties"]) ? record2["properties"] : {}) : null;
16965
+ if (!properties)
16966
+ continue;
16967
+ for (const [key, child] of Object.entries(properties)) {
16968
+ if (out.length >= limit)
16969
+ return;
16970
+ const path = prefix ? `${prefix}.${key}` : key;
16971
+ out.push(path);
16972
+ if (isSchemaNode(child))
16973
+ visit(child, path, depth + 1);
16974
+ }
16975
+ }
16976
+ };
16977
+ visit(node, "", 1);
16978
+ return [...new Set(out)].sort();
16979
+ }
16980
+ function resolveSourcePath(schema, sourcePath) {
16981
+ const segments = parseSourcePath(sourcePath);
16982
+ if (segments.length === 0)
16983
+ return { kind: "resolved" };
16984
+ let candidates = [schema];
16985
+ let resolvedPrefix = "";
16986
+ for (const segment of segments) {
16987
+ const expanded = candidates.flatMap((node) => flatten(node, schema) ?? []);
16988
+ if (expanded.length === 0)
16989
+ return { kind: "indeterminate" };
16990
+ const outcome = combine(expanded.map((node) => stepInto(node, segment)));
16991
+ if (outcome.kind === "indeterminate")
16992
+ return { kind: "indeterminate" };
16993
+ if (outcome.kind === "invalid") {
16994
+ const availablePaths = [
16995
+ ...new Set(expanded.flatMap((node) => enumeratePaths(node, schema)))
16996
+ ].sort();
16997
+ return {
16998
+ kind: "invalid",
16999
+ resolvedPrefix,
17000
+ segment,
17001
+ reason: outcome.reason,
17002
+ availablePaths
17003
+ };
17004
+ }
17005
+ candidates = [outcome.node];
17006
+ resolvedPrefix = resolvedPrefix ? `${resolvedPrefix}.${segment}` : segment;
17007
+ }
17008
+ return { kind: "resolved" };
17009
+ }
17010
+ // src/definitions/validation/validate-definition-specs.ts
17011
+ function listPaths(paths, limit = 24) {
17012
+ if (paths.length === 0)
17013
+ return "";
17014
+ if (paths.length <= limit)
17015
+ return paths.join(", ");
17016
+ return `${paths.slice(0, limit).join(", ")}, \u2026 (${String(paths.length - limit)} more)`;
17017
+ }
17018
+ function quotedOrRoot(prefix) {
17019
+ return prefix ? `"${prefix}"` : "the result root";
17020
+ }
17021
+ function checkSignalSourcePaths(steps) {
17022
+ const issues = [];
17023
+ for (const step of steps) {
17024
+ const schema = step.resultSchemaJson ?? null;
17025
+ if (!schema)
17026
+ continue;
17027
+ for (const signal2 of step.signalExtractorDefinitions) {
17028
+ const resolution = resolveSourcePath(schema, signal2.sourcePath);
17029
+ if (resolution.kind !== "invalid")
17030
+ continue;
17031
+ const { resolvedPrefix, segment, reason, availablePaths } = resolution;
17032
+ const cause = reason === "not-an-array-index" ? `${quotedOrRoot(resolvedPrefix)} is an array, so "${segment}" can never index it \u2014 array segments must be numeric (e.g. "${resolvedPrefix}[0]")` : reason === "scalar-has-no-members" ? `${quotedOrRoot(resolvedPrefix)} is a scalar, so it has no property "${segment}"` : `${quotedOrRoot(resolvedPrefix)} has no property "${segment}"`;
17033
+ const suffix = availablePaths.length > 0 ? ` Valid sourcePaths ${resolvedPrefix ? `under "${resolvedPrefix}"` : "for this step"}: ${listPaths(availablePaths)}.` : "";
17034
+ issues.push({
17035
+ check: "signal-source-path",
17036
+ message: `Step "${step.key}" declares signal "${signal2.key}" with sourcePath ` + `"${signal2.sourcePath}", which can never resolve against the step's ` + `result schema: ${cause}.${suffix}`
17037
+ });
17038
+ }
17039
+ }
17040
+ return issues;
17041
+ }
17042
+ function routeTargets(policy) {
17043
+ const keys = [];
17044
+ if (policy.defaultEventType === "route" && typeof policy.defaultEventParamsJson?.["pipelineKey"] === "string") {
17045
+ keys.push(policy.defaultEventParamsJson["pipelineKey"]);
17046
+ }
17047
+ for (const rule of policy.rulesJson.rules) {
17048
+ if (rule.event.type === "route" && typeof rule.event.params?.["pipelineKey"] === "string") {
17049
+ keys.push(rule.event.params["pipelineKey"]);
17050
+ }
17051
+ }
17052
+ return keys;
17053
+ }
17054
+ function checkRouteTargets(pipelines, knownPipelineKeys) {
17055
+ const issues = [];
17056
+ const known = new Set([
17057
+ ...pipelines.map((pipeline2) => pipeline2.key),
17058
+ ...knownPipelineKeys
17059
+ ]);
17060
+ for (const pipeline2 of pipelines) {
17061
+ for (const step of pipeline2.steps) {
17062
+ for (const target of routeTargets(step.advancementPolicyDefinition)) {
17063
+ if (known.has(target))
17064
+ continue;
17065
+ issues.push({
17066
+ check: "route-target",
17067
+ message: `Pipeline "${pipeline2.key}" step "${step.stepKey}" routes to pipeline ` + `"${target}", but no pipeline with that key was found on the server or ` + `in the current push batch. Push the target pipeline first.`
17068
+ });
17069
+ }
17070
+ }
17071
+ }
17072
+ return issues;
17073
+ }
17074
+ function executionRanks(steps) {
17075
+ const positions = steps.map((step) => step.position);
17076
+ const usable = positions.every((value) => Number.isInteger(value) && value > 0) && new Set(positions).size === positions.length;
17077
+ const indexes = steps.map((_, index) => index);
17078
+ const ordered = usable ? [...indexes].sort((left, right) => (positions[left] ?? 0) - (positions[right] ?? 0)) : indexes;
17079
+ return new Map(ordered.map((index, rank) => [index, rank]));
17080
+ }
17081
+ function bindingSource(binding) {
17082
+ if (binding.source === "step_signal") {
17083
+ return { stepKey: binding.stepKey, signalKey: binding.signalKey };
17084
+ }
17085
+ if (binding.source === "step_output") {
17086
+ return { stepKey: binding.stepKey, signalKey: null };
17087
+ }
17088
+ return null;
17089
+ }
17090
+ function declaredSignalKeys(stepKey, stepsByKey, pipelineSteps) {
17091
+ const specs = stepsByKey.get(stepKey);
17092
+ if (!specs || specs.length === 0)
17093
+ return null;
17094
+ const keys = new Set;
17095
+ for (const spec of specs) {
17096
+ for (const signal2 of spec.signalExtractorDefinitions)
17097
+ keys.add(signal2.key);
17098
+ }
17099
+ for (const step of pipelineSteps) {
17100
+ if (step.stepKey !== stepKey)
17101
+ continue;
17102
+ for (const computed of step.computedSignalDefinitions)
17103
+ keys.add(computed.key);
17104
+ }
17105
+ return [...keys];
17106
+ }
17107
+ function checkSignalBindings(pipelines, stepsByKey) {
17108
+ const issues = [];
17109
+ for (const pipeline2 of pipelines) {
17110
+ const ranks = executionRanks(pipeline2.steps);
17111
+ const order = [...pipeline2.steps.keys()].sort((left, right) => (ranks.get(left) ?? 0) - (ranks.get(right) ?? 0)).map((index) => pipeline2.steps[index]?.stepKey ?? "");
17112
+ const orderHint = `Steps in "${pipeline2.key}", in order: ${order.join(" \u2192 ")}.`;
17113
+ pipeline2.steps.forEach((step, index) => {
17114
+ const consumerRank = ranks.get(index) ?? index;
17115
+ const where = `Pipeline "${pipeline2.key}" step "${step.stepKey}"`;
17116
+ for (const [field, binding] of Object.entries(step.inputBindingsJson)) {
17117
+ const source = bindingSource(binding);
17118
+ if (!source)
17119
+ continue;
17120
+ const what = source.signalKey ? `binds input "${field}" to signal "${source.signalKey}" of step "${source.stepKey}"` : `binds input "${field}" to the output of step "${source.stepKey}"`;
17121
+ const producerRanks = pipeline2.steps.map((candidate, candidateIndex) => candidate.stepKey === source.stepKey ? ranks.get(candidateIndex) ?? candidateIndex : null).filter((rank) => rank !== null);
17122
+ if (producerRanks.length === 0) {
17123
+ issues.push({
17124
+ check: "signal-binding",
17125
+ message: `${where} ${what}, but no step with that key is in the pipeline. ${orderHint}`
17126
+ });
17127
+ continue;
17128
+ }
17129
+ if (!producerRanks.some((rank) => rank < consumerRank)) {
17130
+ issues.push({
17131
+ check: "signal-binding",
17132
+ message: `${where} ${what}, but that step does not run before it, so the ` + `value will never exist. ${orderHint}`
17133
+ });
17134
+ continue;
17135
+ }
17136
+ if (source.signalKey === null)
17137
+ continue;
17138
+ const available = declaredSignalKeys(source.stepKey, stepsByKey, pipeline2.steps);
17139
+ if (available === null || available.includes(source.signalKey))
17140
+ continue;
17141
+ issues.push({
17142
+ check: "signal-binding",
17143
+ message: `${where} ${what}, but "${source.stepKey}" declares no such signal. ` + `Signals on "${source.stepKey}": ${available.length > 0 ? listPaths([...available].sort()) : "(none)"}.`
17144
+ });
17145
+ }
17146
+ });
17147
+ }
17148
+ return issues;
17149
+ }
17150
+ function validateDefinitionSpecs(specs, options = {}) {
17151
+ const stepsByKey = new Map;
17152
+ for (const step of specs.steps) {
17153
+ const existing = stepsByKey.get(step.key);
17154
+ if (existing)
17155
+ existing.push(step);
17156
+ else
17157
+ stepsByKey.set(step.key, [step]);
17158
+ }
17159
+ return [
17160
+ ...checkSignalSourcePaths(specs.steps),
17161
+ ...checkRouteTargets(specs.pipelines, options.knownPipelineKeys ?? []),
17162
+ ...checkSignalBindings(specs.pipelines, stepsByKey)
17163
+ ];
17164
+ }
17165
+ function assertValidDefinitionSpecs(specs, options = {}) {
17166
+ const issues = validateDefinitionSpecs(specs, options);
17167
+ if (issues.length === 0)
17168
+ return;
17169
+ const header = issues.length === 1 ? "Definition validation failed:" : `Definition validation failed with ${String(issues.length)} problems:`;
17170
+ throw new Error([header, ...issues.map((issue2) => ` \u2022 ${issue2.message}`)].join(`
17171
+ `));
17172
+ }
17173
+ // src/push/collect-definitions.ts
16788
17174
  import { existsSync, readdirSync } from "fs";
16789
17175
  import { join, resolve } from "path";
16790
17176
  import { pathToFileURL } from "url";
17177
+ var PUSH_SCRIPT_NAMES = new Set(["push.ts", "push.mjs", "push.js"]);
16791
17178
  function isStepDefinitionSpec(value) {
16792
17179
  if (typeof value !== "object" || value === null)
16793
17180
  return false;
@@ -16800,38 +17187,21 @@ function isPipelineDefinitionSpec(value) {
16800
17187
  const obj = value;
16801
17188
  return typeof obj["key"] === "string" && typeof obj["name"] === "string" && typeof obj["version"] === "number" && Array.isArray(obj["steps"]);
16802
17189
  }
16803
- function extractRoutePipelineKeys(policy) {
16804
- const keys = [];
16805
- if (policy.defaultEventType === "route" && typeof policy.defaultEventParamsJson?.["pipelineKey"] === "string") {
16806
- keys.push(policy.defaultEventParamsJson["pipelineKey"]);
16807
- }
16808
- for (const rule of policy.rulesJson.rules) {
16809
- if (rule.event.type === "route" && typeof rule.event.params?.["pipelineKey"] === "string") {
16810
- keys.push(rule.event.params["pipelineKey"]);
16811
- }
16812
- }
16813
- return keys;
17190
+ async function importModule(path) {
17191
+ return await import(pathToFileURL(path).href);
16814
17192
  }
16815
- var PUSH_SCRIPT_NAMES = new Set(["push.ts", "push.mjs", "push.js"]);
16816
- async function pushFromDirectory(dir, opts) {
16817
- const log = opts.log ?? ((msg) => {
16818
- console.warn(msg);
16819
- });
16820
- const headers = { Authorization: `Bearer ${opts.accessToken}` };
17193
+ async function collectDefinitionsFromDirectory(dir) {
16821
17194
  const absDir = resolve(dir);
16822
- const allFiles = readdirSync(absDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
16823
- const hasAssignmentFile = existsSync(join(absDir, DEFAULT_PIPELINE_ASSIGNMENT_FILENAME));
16824
- const sourceFiles = allFiles.filter((f) => !PUSH_SCRIPT_NAMES.has(f) && f !== DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
16825
- const pipelineSpecs = [];
17195
+ const allFiles = readdirSync(absDir).filter((file2) => file2.endsWith(".ts") || file2.endsWith(".js"));
17196
+ const sourceFiles = allFiles.filter((file2) => !PUSH_SCRIPT_NAMES.has(file2) && file2 !== DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
17197
+ const pipelines = [];
16826
17198
  const stepMap = new Map;
16827
17199
  for (const file2 of sourceFiles) {
16828
- const absPath = join(absDir, file2);
16829
- const mod = await import(pathToFileURL(absPath).href);
17200
+ const mod = await importModule(join(absDir, file2));
16830
17201
  for (const [exportName, value] of Object.entries(mod)) {
16831
17202
  if (exportName === "default") {
16832
- if (isPipelineDefinitionSpec(value)) {
16833
- pipelineSpecs.push(value);
16834
- }
17203
+ if (isPipelineDefinitionSpec(value))
17204
+ pipelines.push(value);
16835
17205
  continue;
16836
17206
  }
16837
17207
  if (isStepDefinitionSpec(value)) {
@@ -16839,38 +17209,48 @@ async function pushFromDirectory(dir, opts) {
16839
17209
  }
16840
17210
  }
16841
17211
  }
16842
- for (const spec of pipelineSpecs) {
17212
+ for (const spec of pipelines) {
16843
17213
  for (const embedded of spec._stepDefinitions ?? []) {
16844
17214
  const key = `${embedded.key}@v${String(embedded.version)}`;
16845
- if (!stepMap.has(key)) {
17215
+ if (!stepMap.has(key))
16846
17216
  stepMap.set(key, embedded);
16847
- }
16848
17217
  }
16849
17218
  }
16850
- log(`Found ${String(pipelineSpecs.length)} pipeline(s) and ${String(stepMap.size)} step(s).`);
17219
+ return {
17220
+ pipelines,
17221
+ steps: [...stepMap.values()],
17222
+ defaultPipelineAssignment: await collectDefaultPipelineAssignment(absDir)
17223
+ };
17224
+ }
17225
+ async function collectDefaultPipelineAssignment(absDir) {
17226
+ const path = join(absDir, DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
17227
+ if (!existsSync(path))
17228
+ return null;
17229
+ const mod = await importModule(path);
17230
+ const spec = mod["default"];
17231
+ if (!isDefaultPipelineAssignmentSpec(spec)) {
17232
+ throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} must have a default export produced by defaultPipelineAssignment(({ assign, skip, ... }) => ({ default: ..., rules: [...] })). Got: ${typeof spec}`);
17233
+ }
17234
+ return spec;
17235
+ }
17236
+ // src/push/push-from-directory.ts
17237
+ async function pushFromDirectory(dir, opts) {
17238
+ const log = opts.log ?? ((msg) => {
17239
+ console.warn(msg);
17240
+ });
17241
+ const headers = { Authorization: `Bearer ${opts.accessToken}` };
17242
+ const collected = await collectDefinitionsFromDirectory(dir);
17243
+ const { pipelines, steps } = collected;
17244
+ log(`Found ${String(pipelines.length)} pipeline(s) and ${String(steps.length)} step(s).`);
16851
17245
  const stepsClient = createStepDefinitionsClient(opts.baseUrl);
16852
- for (const spec of stepMap.values()) {
17246
+ const pipelinesClient = createPipelineDefinitionsClient(opts.baseUrl);
17247
+ const serverPipelineKeys = pipelines.length > 0 ? (await pipelinesClient.listByProjectId(opts.projectId, { headers })).map((pipeline2) => pipeline2.key) : [];
17248
+ assertValidDefinitionSpecs({ pipelines, steps }, { knownPipelineKeys: serverPipelineKeys });
17249
+ for (const spec of steps) {
16853
17250
  await stepsClient.upsertFromSpec(opts.projectId, spec, { headers });
16854
17251
  log(`\u2713 step ${spec.key} v${String(spec.version)} \u2192 upserted`);
16855
17252
  }
16856
- const pipelinesClient = createPipelineDefinitionsClient(opts.baseUrl);
16857
- let pushedPipelinesCount = 0;
16858
- if (pipelineSpecs.length > 0) {
16859
- const existingPipelines = await pipelinesClient.listByProjectId(opts.projectId, { headers });
16860
- const knownPipelineKeys = new Set([
16861
- ...pipelineSpecs.map((s) => s.key),
16862
- ...existingPipelines.map((p) => p.key)
16863
- ]);
16864
- for (const spec of pipelineSpecs) {
16865
- for (const step of spec.steps) {
16866
- const routeKeys = extractRoutePipelineKeys(step.advancementPolicyDefinition);
16867
- for (const routeKey of routeKeys) {
16868
- if (!knownPipelineKeys.has(routeKey)) {
16869
- throw new Error(`Pipeline "${spec.key}" step "${step.stepKey}" routes to pipeline "${routeKey}", but no pipeline with that key was found on the server or in the current push batch. Push the target pipeline first.`);
16870
- }
16871
- }
16872
- }
16873
- }
17253
+ if (pipelines.length > 0) {
16874
17254
  const serverSteps = await stepsClient.listByProjectId(opts.projectId, {
16875
17255
  headers
16876
17256
  });
@@ -16879,27 +17259,17 @@ async function pushFromDirectory(dir, opts) {
16879
17259
  key: s.key,
16880
17260
  version: s.version
16881
17261
  }));
16882
- for (const spec of pipelineSpecs) {
17262
+ for (const spec of pipelines) {
16883
17263
  await pipelinesClient.upsertFromSpec(opts.projectId, spec, stepDefs, {
16884
17264
  headers
16885
17265
  });
16886
17266
  log(`\u2713 pipeline ${spec.key} v${String(spec.version)} \u2192 upserted`);
16887
17267
  }
16888
- pushedPipelinesCount = pipelineSpecs.length;
16889
- }
16890
- let syncedDefaultPipelineAssignment = false;
16891
- if (hasAssignmentFile) {
16892
- const assignmentFilePath = join(absDir, DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
16893
- const assignmentMod = await import(pathToFileURL(assignmentFilePath).href);
16894
- const assignmentSpec = assignmentMod["default"];
16895
- if (!isDefaultPipelineAssignmentSpec(assignmentSpec)) {
16896
- throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} must have a default export produced by defaultPipelineAssignment(({ assign, skip, ... }) => ({ default: ..., rules: [...] })). Got: ${typeof assignmentSpec}`);
16897
- }
16898
- syncedDefaultPipelineAssignment = await syncDefaultPipelineAssignment(assignmentSpec, opts, headers, pipelinesClient, log);
16899
17268
  }
17269
+ const syncedDefaultPipelineAssignment = collected.defaultPipelineAssignment ? await syncDefaultPipelineAssignment(collected.defaultPipelineAssignment, opts, headers, pipelinesClient, log) : false;
16900
17270
  return {
16901
- pushedSteps: stepMap.size,
16902
- pushedPipelines: pushedPipelinesCount,
17271
+ pushedSteps: steps.length,
17272
+ pushedPipelines: pipelines.length,
16903
17273
  syncedDefaultPipelineAssignment
16904
17274
  };
16905
17275
  }
@@ -16921,7 +17291,7 @@ async function syncDefaultPipelineAssignment(spec, opts, headers, pipelinesClien
16921
17291
  }
16922
17292
  for (const key of referencedKeys) {
16923
17293
  if (!pipelineKeyToId.has(key)) {
16924
- throw new Error(`default-pipeline-assignment.ts references pipeline "${key}", but no pipeline with that key was found on the server. Push the pipeline first with \`boboddy pipelines push\`.`);
17294
+ throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} references pipeline "${key}", ` + `but no pipeline with that key was found on the server. ` + `Push the pipeline first with \`boboddy pipelines push\`.`);
16925
17295
  }
16926
17296
  }
16927
17297
  const linearPipelineDefinitionId = pipelineKeyToId.get(serialized.linearPipelineDefinitionKey);
@@ -16933,7 +17303,7 @@ async function syncDefaultPipelineAssignment(spec, opts, headers, pipelinesClien
16933
17303
  const pKey = rule.event.params["pipelineKey"];
16934
17304
  const pId = pipelineKeyToId.get(pKey);
16935
17305
  if (!pId) {
16936
- throw new Error(`default-pipeline-assignment.ts assign() references pipeline "${pKey}" which was not found on the server.`);
17306
+ throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} assign() references pipeline "${pKey}" ` + `which was not found on the server.`);
16937
17307
  }
16938
17308
  return {
16939
17309
  ...rule,
@@ -16971,5 +17341,6 @@ async function syncDefaultPipelineAssignment(spec, opts, headers, pipelinesClien
16971
17341
  return true;
16972
17342
  }
16973
17343
  export {
16974
- pushFromDirectory
17344
+ pushFromDirectory,
17345
+ collectDefinitionsFromDirectory
16975
17346
  };
@@ -14,12 +14,18 @@ export interface PushFromDirectoryResult {
14
14
  /**
15
15
  * Imports every `.ts`/`.js` file in `dir` (except the push script itself and
16
16
  * `default-pipeline-assignment.ts`), collects all pipeline and step
17
- * definitions, then upserts them via the strongly-typed SDK clients.
17
+ * definitions, validates them, then upserts them via the strongly-typed SDK
18
+ * clients.
18
19
  *
19
20
  * If `default-pipeline-assignment.ts` is present, it is imported separately
20
21
  * after pipelines are pushed, and the project default pipeline assignment is
21
22
  * updated on the server.
22
23
  *
24
+ * Collection is `collectDefinitionsFromDirectory` (offline, no token) and
25
+ * validation is `validateDefinitionSpecs` (pure). Both run before the first
26
+ * mutating request, so a batch with a dead signal `sourcePath`, a dangling
27
+ * route target, or a backwards signal binding fails without half-pushing.
28
+ *
23
29
  * Designed to run on the user's native runtime (bun, node-with-tsx, deno),
24
30
  * NOT inside a `bun --compile`'d binary — that runtime can't resolve scoped
25
31
  * package `exports` field remappings from external user files.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@boboddy/sdk",
4
- "version": "0.2.10-alpha",
4
+ "version": "0.2.12-alpha",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
@@ -28,6 +28,10 @@
28
28
  "import": "./dist/definitions/advancement-policies/index.js",
29
29
  "types": "./dist/definitions/advancement-policies/index.d.ts"
30
30
  },
31
+ "./definitions/validation": {
32
+ "import": "./dist/definitions/validation/index.js",
33
+ "types": "./dist/definitions/validation/index.d.ts"
34
+ },
31
35
  "./defaults": {
32
36
  "import": "./dist/defaults/index.js",
33
37
  "types": "./dist/defaults/index.d.ts"