@kici-dev/sdk 0.1.27 → 0.2.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.
Files changed (52) hide show
  1. package/dist/api-types.d.ts +28 -0
  2. package/dist/api-types.js +3 -1
  3. package/dist/approval.d.ts +3 -8
  4. package/dist/approval.js +9 -0
  5. package/dist/artifacts-types.d.ts +45 -0
  6. package/dist/artifacts-types.js +3 -0
  7. package/dist/context.d.ts +65 -15
  8. package/dist/events/define-event.d.ts +11 -3
  9. package/dist/events/define-event.js +14 -4
  10. package/dist/events/emit-typing.test-d.d.ts +2 -0
  11. package/dist/events/emit-typing.test-d.js +36 -0
  12. package/dist/events/event-payloads.d.ts +4 -0
  13. package/dist/events/index.d.ts +1 -1
  14. package/dist/events/index.js +2 -2
  15. package/dist/fleet/agent-version-converge.d.ts +23 -0
  16. package/dist/fleet/agent-version-converge.js +58 -0
  17. package/dist/index.d.ts +9 -3
  18. package/dist/index.js +6 -2
  19. package/dist/job-outputs.test-d.d.ts +2 -0
  20. package/dist/job-outputs.test-d.js +164 -0
  21. package/dist/job.d.ts +71 -9
  22. package/dist/job.js +14 -0
  23. package/dist/matrix/expand.d.ts +1 -1
  24. package/dist/matrix/expand.js +2 -2
  25. package/dist/needs-context.d.ts +13 -6
  26. package/dist/outputs.d.ts +19 -4
  27. package/dist/outputs.js +21 -7
  28. package/dist/parallel.d.ts +1 -1
  29. package/dist/parallel.js +1 -1
  30. package/dist/provenance-types.d.ts +3 -3
  31. package/dist/rules/context.d.ts +33 -0
  32. package/dist/rules/context.js +51 -0
  33. package/dist/rules/evaluator.d.ts +10 -0
  34. package/dist/rules/evaluator.js +9 -1
  35. package/dist/rules/index.d.ts +2 -0
  36. package/dist/rules/index.js +2 -1
  37. package/dist/rules/types.d.ts +11 -1
  38. package/dist/step.d.ts +6 -6
  39. package/dist/step.js +6 -3
  40. package/dist/testing/index.d.ts +2 -0
  41. package/dist/testing/index.js +3 -0
  42. package/dist/testing/step-context.d.ts +56 -0
  43. package/dist/testing/step-context.js +259 -0
  44. package/dist/triggers/index.d.ts +2 -1
  45. package/dist/triggers/index.js +2 -1
  46. package/dist/triggers/types.d.ts +23 -1
  47. package/dist/triggers/workflows-failed-batch.d.ts +20 -0
  48. package/dist/triggers/workflows-failed-batch.js +25 -0
  49. package/dist/types.d.ts +142 -9
  50. package/dist/validation/dag.js +11 -3
  51. package/package.json +10 -5
  52. package/sbom.spdx.json +192 -192
@@ -0,0 +1,164 @@
1
+ import "./rolldown-runtime-ClRpJifh.js";
2
+ import { isHostJobOutputs, isMatrixJobOutputs } from "./context.js";
3
+ import { dynamicGroup } from "./dynamic-group.js";
4
+ import { step } from "./step.js";
5
+ import { job } from "./job.js";
6
+ import { workflow } from "./workflow.js";
7
+ import { describe, expectTypeOf, it } from "vitest";
8
+ //#region src/job-outputs.test-d.ts
9
+ describe("Job<TOutputs> — merged-steps inference", () => {
10
+ it("infers a nested output map keyed by step name for a multi-step job", () => {
11
+ const j = job("ci", {
12
+ runsOn: "kici:os:linux",
13
+ steps: [step("build", { run: async () => ({ version: "1.0" }) }), step("test", { run: async () => ({ passed: true }) })]
14
+ });
15
+ expectTypeOf(j.result.build.version).toEqualTypeOf();
16
+ expectTypeOf(j.result.test.passed).toEqualTypeOf();
17
+ });
18
+ it("rejects reading an unknown output field of a declared step", () => {
19
+ job("ci", {
20
+ runsOn: "kici:os:linux",
21
+ steps: [step("build", { run: async () => ({ version: "1.0" }) })]
22
+ }).result.build.nope;
23
+ });
24
+ it("rejects reading an undeclared step name", () => {
25
+ job("ci", {
26
+ runsOn: "kici:os:linux",
27
+ steps: [step("build", { run: async () => ({ version: "1.0" }) })]
28
+ }).result.other;
29
+ });
30
+ it("infers a flat output shape for the run: shorthand", () => {
31
+ const flat = job("flat", {
32
+ runsOn: "kici:os:linux",
33
+ run: async () => ({ url: "x" })
34
+ });
35
+ expectTypeOf(flat.result.url).toEqualTypeOf();
36
+ flat.result.nope;
37
+ });
38
+ it("honours an explicit output-type override for a dynamically-shaped job", () => {
39
+ expectTypeOf(job("dyn", {
40
+ runsOn: "kici:os:linux",
41
+ steps: [step(async () => ({}))]
42
+ }).result.n).toEqualTypeOf();
43
+ });
44
+ it("omits id-less steps and falls back to the loose shape", () => {
45
+ expectTypeOf(job("idless", {
46
+ runsOn: "kici:os:linux",
47
+ steps: [step(async () => ({ x: 1 }))]
48
+ }).result.anything).toEqualTypeOf();
49
+ });
50
+ it("keeps a typed job assignable to the bare Job type", () => {
51
+ expectTypeOf(job("ci", {
52
+ runsOn: "kici:os:linux",
53
+ steps: [step("build", { run: async () => ({ version: "1.0" }) })]
54
+ })).toEqualTypeOf();
55
+ });
56
+ });
57
+ describe("Job<void> — void run-shorthand assignability", () => {
58
+ it("makes a void run-shorthand job assignable to JobOrFactory (inline)", () => {
59
+ workflow("wf", { jobs: [job("setup", {
60
+ runsOn: "kici:os:linux",
61
+ run: async ({ $ }) => {
62
+ await $`echo hi`;
63
+ }
64
+ })] });
65
+ expectTypeOf().toMatchTypeOf();
66
+ });
67
+ it("makes a void run-shorthand job assignable to JobOrFactory (const-extracted)", () => {
68
+ const setup = job("setup", {
69
+ runsOn: "kici:os:linux",
70
+ run: async ({ $ }) => {
71
+ await $`echo hi`;
72
+ }
73
+ });
74
+ expectTypeOf(setup).toMatchTypeOf();
75
+ workflow("wf", { jobs: [setup] });
76
+ });
77
+ it("resolves result to never for a void run-shorthand job", () => {
78
+ expectTypeOf(job("setup", {
79
+ runsOn: "kici:os:linux",
80
+ run: async () => {}
81
+ }).result).toEqualTypeOf();
82
+ });
83
+ it("leaves a value-returning run job untouched (regression guard)", () => {
84
+ const flat = job("flat", {
85
+ runsOn: "kici:os:linux",
86
+ run: async () => ({ url: "x" })
87
+ });
88
+ expectTypeOf(flat.result.url).toEqualTypeOf();
89
+ expectTypeOf(flat).toMatchTypeOf();
90
+ });
91
+ });
92
+ describe("typed ctx.needs from job references", () => {
93
+ const ci = job("ci", {
94
+ runsOn: "kici:os:linux",
95
+ steps: [step("build", { run: async () => ({ version: "1.0" }) }), step("test", { run: async () => ({ passed: true }) })]
96
+ });
97
+ it("threads a referenced job’s inferred outputs into ctx.needs", () => {
98
+ job("deploy", {
99
+ runsOn: "kici:os:linux",
100
+ needs: [ci],
101
+ run: async (ctx) => {
102
+ expectTypeOf(ctx.needs.ci.result.build.version).toEqualTypeOf();
103
+ expectTypeOf(ctx.needs.ci.result.test.passed).toEqualTypeOf();
104
+ }
105
+ });
106
+ });
107
+ it("rejects an undeclared need and an unknown field through a need", () => {
108
+ job("deploy2", {
109
+ runsOn: "kici:os:linux",
110
+ needs: [ci],
111
+ run: async (ctx) => {
112
+ ctx.needs.other;
113
+ ctx.needs.ci.result.build.nope;
114
+ }
115
+ });
116
+ });
117
+ it("keeps string-form needs loose", () => {
118
+ job("loose", {
119
+ runsOn: "kici:os:linux",
120
+ needs: ["ci"],
121
+ run: async (ctx) => {
122
+ expectTypeOf(ctx.needs.ci.result).toEqualTypeOf();
123
+ }
124
+ });
125
+ });
126
+ it("falls back to the loose open map when a group need is mixed in (no regression)", () => {
127
+ job("mixed", {
128
+ runsOn: "kici:os:linux",
129
+ needs: [ci, dynamicGroup("shards")],
130
+ run: async (ctx) => {
131
+ expectTypeOf(ctx.needs.ci).not.toBeNever();
132
+ expectTypeOf(ctx.needs.shards).not.toBeNever();
133
+ }
134
+ });
135
+ });
136
+ });
137
+ describe("envelope-generic jobOutputs for typed refs", () => {
138
+ const ci = job("ci", {
139
+ runsOn: "kici:os:linux",
140
+ steps: [step("build", { run: async () => ({ version: "1.0" }) })]
141
+ });
142
+ const ctx = null;
143
+ it("types a job-ref jobOutputs as the plain | matrix | host union of the inferred shape", () => {
144
+ expectTypeOf(ctx.jobOutputs(ci)).toEqualTypeOf();
145
+ });
146
+ it("narrows to the typed matrix envelope via isMatrixJobOutputs", () => {
147
+ const out = ctx.jobOutputs(ci);
148
+ if (isMatrixJobOutputs(out)) {
149
+ expectTypeOf(out.byMatrix).toEqualTypeOf();
150
+ expectTypeOf(out.merged.build.version).toEqualTypeOf();
151
+ }
152
+ });
153
+ it("narrows to the typed host envelope via isHostJobOutputs", () => {
154
+ const out = ctx.jobOutputs(ci);
155
+ if (isHostJobOutputs(out)) expectTypeOf(out.byHost).toEqualTypeOf();
156
+ });
157
+ it("keeps a string-name ref on the loose union", () => {
158
+ expectTypeOf(ctx.jobOutputs({ name: "ci" })).toEqualTypeOf();
159
+ });
160
+ });
161
+ //#endregion
162
+ export {};
163
+
164
+ //# sourceMappingURL=job-outputs.test-d.js.map
package/dist/job.d.ts CHANGED
@@ -1,31 +1,93 @@
1
- import type { Job, JobOptions } from './types.js';
1
+ import type { Job, JobOptions, StepInput, InferJobOutputsFromSteps, StepContextWithNeeds } from './types.js';
2
+ import type { StepContext } from './context.js';
2
3
  /**
3
- * Create a job with an explicit name.
4
+ * Create a job with an explicit name; run shorthand with typed `ctx.needs`.
5
+ *
6
+ * When the job declares `needs: [jobRef, …]`, the `run:` function's `ctx.needs`
7
+ * is typed from the tuple — job references thread their inferred outputs, so
8
+ * `ctx.needs.<job>.result.<field>` is checked (string / `{ name }` entries stay
9
+ * loose). Outputs are inferred flat from the run function's return type.
10
+ */
11
+ export declare function job<TName extends string, const TNeeds extends readonly unknown[], TRun extends (ctx: StepContextWithNeeds<TNeeds>) => Promise<any>>(name: TName, options: Omit<JobOptions, 'needs' | 'run' | 'steps'> & {
12
+ needs: TNeeds;
13
+ run: TRun;
14
+ steps?: undefined;
15
+ }): Job<Awaited<ReturnType<TRun>>, TName>;
16
+ /**
17
+ * Create a job with an explicit name; outputs inferred from the run shorthand.
18
+ *
19
+ * The `run:` shorthand infers a **flat** output shape from the run function's
20
+ * return type (`job.result.<field>`), matching the runtime's single-step
21
+ * flattening.
22
+ */
23
+ export declare function job<TName extends string, TRun extends (ctx: StepContext) => Promise<any>>(name: TName, options: JobOptions & {
24
+ run: TRun;
25
+ steps?: undefined;
26
+ }): Job<Awaited<ReturnType<TRun>>, TName>;
27
+ /**
28
+ * Create a job with an explicit name; outputs inferred from the steps tuple.
29
+ *
30
+ * A `steps:` job infers a **nested** output shape keyed by step name
31
+ * (`job.result.<step>.<field>`) via {@link InferJobOutputsFromSteps} — name
32
+ * your steps to get typed cross-job reads (id-less steps contribute nothing).
4
33
  *
5
34
  * @example
6
35
  * const build = job('build', {
7
- * runsOn: 'linux',
36
+ * runsOn: 'kici:os:linux',
8
37
  * steps: [checkout, install, compile],
9
38
  * });
39
+ */
40
+ export declare function job<TName extends string, const TSteps extends readonly StepInput[]>(name: TName, options: JobOptions & {
41
+ steps: TSteps;
42
+ run?: undefined;
43
+ }): Job<InferJobOutputsFromSteps<TSteps>, TName>;
44
+ /**
45
+ * Create a job with an explicit name and an explicit output-type override.
46
+ *
47
+ * Pass the output shape as a type argument (`job<MyOutputs>('name', {...})`) for
48
+ * dynamically-shaped jobs the inference can't reproduce. With no type argument
49
+ * (and no `run` / `steps` to infer from) it defaults to the loose
50
+ * `Record<string, unknown>`, so a bare job keeps compiling.
10
51
  *
11
52
  * @example
12
- * // With rules and description
13
53
  * const build = job('build', {
14
- * runsOn: 'linux',
54
+ * runsOn: 'kici:os:linux',
15
55
  * steps: [checkout, install, compile],
16
56
  * rules: [rule('env: CI')],
17
57
  * description: 'Build the project',
18
58
  * });
19
59
  */
20
- export declare function job(name: string, options: JobOptions): Job;
60
+ export declare function job<TOutputs = Record<string, unknown>, TName extends string = string>(name: TName, options: JobOptions): Job<TOutputs, TName>;
61
+ /**
62
+ * Create a job with auto-generated ID; run shorthand with typed `ctx.needs`.
63
+ */
64
+ export declare function job<const TNeeds extends readonly unknown[], TRun extends (ctx: StepContextWithNeeds<TNeeds>) => Promise<any>>(options: Omit<JobOptions, 'needs' | 'run' | 'steps'> & {
65
+ needs: TNeeds;
66
+ run: TRun;
67
+ steps?: undefined;
68
+ }): Job<Awaited<ReturnType<TRun>>>;
21
69
  /**
22
- * Create a job with auto-generated ID.
70
+ * Create a job with auto-generated ID; outputs inferred from the run shorthand.
71
+ */
72
+ export declare function job<TRun extends (ctx: StepContext) => Promise<any>>(options: JobOptions & {
73
+ run: TRun;
74
+ steps?: undefined;
75
+ }): Job<Awaited<ReturnType<TRun>>>;
76
+ /**
77
+ * Create a job with auto-generated ID; outputs inferred from the steps tuple.
23
78
  *
24
79
  * @example
25
80
  * const build = job({
26
- * runsOn: 'linux',
81
+ * runsOn: 'kici:os:linux',
27
82
  * steps: [checkout, install],
28
83
  * });
29
84
  */
30
- export declare function job(options: JobOptions): Job;
85
+ export declare function job<const TSteps extends readonly StepInput[]>(options: JobOptions & {
86
+ steps: TSteps;
87
+ run?: undefined;
88
+ }): Job<InferJobOutputsFromSteps<TSteps>>;
89
+ /**
90
+ * Create a job with auto-generated ID and an explicit output-type override.
91
+ */
92
+ export declare function job<TOutputs = Record<string, unknown>>(options: JobOptions): Job<TOutputs>;
31
93
  //# sourceMappingURL=job.d.ts.map
package/dist/job.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import "./rolldown-runtime-ClRpJifh.js";
2
2
  import { createJobOutputProxy } from "./outputs.js";
3
+ import { isKnownCapability } from "@kici-dev/engine";
3
4
  import { randomUUID } from "node:crypto";
4
5
  //#region src/job.ts
5
6
  /** A typed preset is a `'mise'` string or a `{ mise }` object — neither carries `run`. */
@@ -21,6 +22,17 @@ function validateInit(init, jobName) {
21
22
  });
22
23
  }
23
24
  /**
25
+ * Validate a job's `sandbox` escape-hatch request. An unknown capability name
26
+ * is a typo that would never match the operator allow-list, so it is rejected
27
+ * at author time (security is still enforced deny-by-default at dispatch — this
28
+ * is a correctness/UX guard). Capability form is normalized by the dispatch
29
+ * resolver, so both `NET_ADMIN` and `CAP_NET_ADMIN` are accepted here.
30
+ */
31
+ function validateSandbox(sandbox, jobName) {
32
+ if (sandbox === void 0) return;
33
+ for (const cap of sandbox.capabilities ?? []) if (!isKnownCapability(cap)) throw new Error(`job('${jobName}'): unknown Linux capability '${cap}' in sandbox.capabilities`);
34
+ }
35
+ /**
24
36
  * Implementation of job() factory.
25
37
  */
26
38
  function job(nameOrOptions, maybeOptions) {
@@ -32,6 +44,7 @@ function job(nameOrOptions, maybeOptions) {
32
44
  steps = [options.run];
33
45
  }
34
46
  validateInit(options.init, name);
47
+ validateSandbox(options.sandbox, name);
35
48
  const firstContext = options.contexts?.[0];
36
49
  const concurrencyGroup = options.concurrencyGroup ?? (typeof firstContext === "string" ? firstContext : void 0);
37
50
  if (options.context !== void 0 && options.contexts !== void 0) throw new Error(`job('${name}'): context and contexts are mutually exclusive — use one`);
@@ -57,6 +70,7 @@ function job(nameOrOptions, maybeOptions) {
57
70
  exclude: options.exclude,
58
71
  checkout: options.checkout,
59
72
  container: options.container,
73
+ ...options.sandbox !== void 0 && { sandbox: options.sandbox },
60
74
  context: options.context,
61
75
  contexts: options.contexts,
62
76
  env: options.env,
@@ -1,2 +1,2 @@
1
- export { expandSingleDimension, expandMultiDimension, expandMatrix, applyIncludeExclude, } from '@kici-dev/engine';
1
+ export { expandMatrix, applyIncludeExclude } from '@kici-dev/engine';
2
2
  //# sourceMappingURL=expand.d.ts.map
@@ -1,3 +1,3 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
- import { applyIncludeExclude, expandMatrix, expandMultiDimension, expandSingleDimension } from "@kici-dev/engine";
3
- export { applyIncludeExclude, expandMatrix, expandMultiDimension, expandSingleDimension };
2
+ import { applyIncludeExclude, expandMatrix } from "@kici-dev/engine";
3
+ export { applyIncludeExclude, expandMatrix };
@@ -15,17 +15,24 @@ export interface UpstreamSnapshot {
15
15
  groups: Record<string, string[]>;
16
16
  statuses?: Record<string, ExecutionJobStatus>;
17
17
  }
18
+ /**
19
+ * A single-job need entry: the upstream's outputs proxy plus its terminal
20
+ * status. `T` is the upstream's inferred output shape (defaults to the loose
21
+ * `Record<string, unknown>`); a typed `needs: [jobRef]` threads the reference's
22
+ * outputs here so `ctx.needs.<job>.result.<field>` is checked.
23
+ */
24
+ export interface SingleNeedEntry<T = Record<string, unknown>> {
25
+ result: OutputProxy<T>;
26
+ status: ExecutionJobStatus;
27
+ }
18
28
  /** One entry in the array exposed for a `dynamicGroup(...)` need. */
19
- export interface GroupNeedEntry {
29
+ export interface GroupNeedEntry<T = Record<string, unknown>> {
20
30
  name: string;
21
- result: OutputProxy<any>;
31
+ result: OutputProxy<T>;
22
32
  status: ExecutionJobStatus;
23
33
  }
24
34
  /** A single-job need exposes `{ result, status }`; a group need exposes an ordered array. */
25
- export type NeedEntry = {
26
- result: OutputProxy<any>;
27
- status: ExecutionJobStatus;
28
- } | GroupNeedEntry[];
35
+ export type NeedEntry<T = Record<string, unknown>> = SingleNeedEntry<T> | GroupNeedEntry[];
29
36
  /** The resolved `ctx.needs` map keyed by job name or group name. */
30
37
  export type NeedsContext = Record<string, NeedEntry>;
31
38
  /**
package/dist/outputs.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OutputProxy } from './types.js';
1
+ import type { OutputProxy, SourceLocation } from './types.js';
2
2
  /**
3
3
  * Shared mutable map that steps write their outputs to and proxies read from.
4
4
  * Keys are step/job names, values are the outputs records.
@@ -42,14 +42,29 @@ export declare function getJobOutputsMap(): OutputsMap;
42
42
  * Get the current step ref map.
43
43
  */
44
44
  export declare function getStepRefMap(): StepRefMap;
45
+ /**
46
+ * Minimal shape the step output proxy needs from a step: a name that may be
47
+ * assigned after the proxy is created (id-less steps are named `step-N` when the
48
+ * job's steps are enumerated at execution start), plus the optional source
49
+ * location for a helpful error when the name is still unresolved.
50
+ */
51
+ export interface StepNameRef {
52
+ readonly name: string;
53
+ readonly _sourceLocation?: SourceLocation;
54
+ }
45
55
  /**
46
56
  * Create a Proxy over step outputs that resolves property access lazily
47
57
  * against the module-global step outputs map.
48
58
  *
49
- * @param stepName - The name of the step whose outputs to proxy
59
+ * The proxy reads `stepRef.name` at ACCESS time rather than capturing the name
60
+ * string at creation. Id-less steps have `name === ''` when the proxy is built;
61
+ * the runner assigns `step-N` onto the same step object before any run function
62
+ * executes, so `.result` resolves under the final name by the time it is used.
63
+ *
64
+ * @param stepRef - The step whose (possibly later-assigned) name identifies its outputs
50
65
  * @returns A Proxy that resolves property access at runtime
51
66
  */
52
- export declare function createStepOutputProxy<T>(stepName: string): OutputProxy<T>;
67
+ export declare function createStepOutputProxy<T>(stepRef: StepNameRef): OutputProxy<T>;
53
68
  /**
54
69
  * Create a Proxy over job outputs that resolves property access lazily
55
70
  * against the module-global job outputs map.
@@ -60,7 +75,7 @@ export declare function createStepOutputProxy<T>(stepName: string): OutputProxy<
60
75
  * @param jobName - The name of the job whose outputs to proxy
61
76
  * @returns A Proxy that resolves property access at runtime
62
77
  */
63
- export declare function createJobOutputProxy(jobName: string): OutputProxy<any>;
78
+ export declare function createJobOutputProxy<TOutputs = any>(jobName: string): OutputProxy<TOutputs>;
64
79
  /**
65
80
  * Output proxy bound to a specific outputs object (not the module-global map).
66
81
  *
package/dist/outputs.js CHANGED
@@ -66,30 +66,44 @@ function getStepRefMap() {
66
66
  * Create a Proxy over step outputs that resolves property access lazily
67
67
  * against the module-global step outputs map.
68
68
  *
69
- * @param stepName - The name of the step whose outputs to proxy
69
+ * The proxy reads `stepRef.name` at ACCESS time rather than capturing the name
70
+ * string at creation. Id-less steps have `name === ''` when the proxy is built;
71
+ * the runner assigns `step-N` onto the same step object before any run function
72
+ * executes, so `.result` resolves under the final name by the time it is used.
73
+ *
74
+ * @param stepRef - The step whose (possibly later-assigned) name identifies its outputs
70
75
  * @returns A Proxy that resolves property access at runtime
71
76
  */
72
- function createStepOutputProxy(stepName) {
77
+ function createStepOutputProxy(stepRef) {
78
+ const unresolvedError = () => {
79
+ const loc = stepRef._sourceLocation;
80
+ const at = loc ? ` (step defined at ${loc.file}:${loc.line})` : "";
81
+ return /* @__PURE__ */ new Error(`This step has no name yet; .result is only available inside another step's run function${at}. Name the step to reference its outputs before execution.`);
82
+ };
73
83
  return new Proxy({}, {
74
84
  get(_target, prop, receiver) {
75
85
  if (typeof prop === "symbol") return Reflect.get(_target, prop, receiver);
76
86
  if (WELL_KNOWN_STRING_PROPS.has(prop)) return Reflect.get(_target, prop, receiver);
77
- const outputs = _stepOutputsMap.get(stepName);
78
- if (!outputs) throw new Error(`Step '${stepName}' has not produced outputs yet`);
87
+ if (!stepRef.name) throw unresolvedError();
88
+ const outputs = _stepOutputsMap.get(stepRef.name);
89
+ if (!outputs) throw new Error(`Step '${stepRef.name}' has not produced outputs yet`);
79
90
  return outputs[prop];
80
91
  },
81
92
  ownKeys() {
82
- const outputs = _stepOutputsMap.get(stepName);
93
+ if (!stepRef.name) return [];
94
+ const outputs = _stepOutputsMap.get(stepRef.name);
83
95
  if (!outputs) return [];
84
96
  return Reflect.ownKeys(outputs);
85
97
  },
86
98
  has(_target, prop) {
87
- const outputs = _stepOutputsMap.get(stepName);
99
+ if (!stepRef.name) return false;
100
+ const outputs = _stepOutputsMap.get(stepRef.name);
88
101
  if (!outputs) return false;
89
102
  return prop in outputs;
90
103
  },
91
104
  getOwnPropertyDescriptor(_target, prop) {
92
- const outputs = _stepOutputsMap.get(stepName);
105
+ if (!stepRef.name) return void 0;
106
+ const outputs = _stepOutputsMap.get(stepRef.name);
93
107
  if (!outputs) return void 0;
94
108
  if (prop in outputs) return {
95
109
  configurable: true,
@@ -32,7 +32,7 @@ export declare function isParallelGroup(x: unknown): x is ParallelGroup;
32
32
  /**
33
33
  * Flatten a job's `steps` into a flat list where each parallel group's children
34
34
  * are inlined in array order (the group wrapper is dropped). Used by single-
35
- * process consumers (the local executor and dry-run preview) that surface each
35
+ * process consumers (the test runner and dry-run preview) that surface each
36
36
  * child as its own step but do not run a concurrent scheduler; the concurrent
37
37
  * agent path expands groups into observable concurrent tasks instead.
38
38
  */
package/dist/parallel.js CHANGED
@@ -21,7 +21,7 @@ function isParallelGroup(x) {
21
21
  /**
22
22
  * Flatten a job's `steps` into a flat list where each parallel group's children
23
23
  * are inlined in array order (the group wrapper is dropped). Used by single-
24
- * process consumers (the local executor and dry-run preview) that surface each
24
+ * process consumers (the test runner and dry-run preview) that surface each
25
25
  * child as its own step but do not run a concurrent scheduler; the concurrent
26
26
  * agent path expands groups into observable concurrent tasks instead.
27
27
  */
@@ -33,9 +33,9 @@ export type AttestProvenanceResult = {
33
33
  bundleMediaType: string;
34
34
  } | {
35
35
  /**
36
- * The Platform mint failed transiently, so only the identity token is
37
- * deferred: the statement was frozen + signed at build time and is minted
38
- * later (automatically on Platform recovery, or via
36
+ * The identity-token mint failed transiently, so only the identity token
37
+ * is deferred: the statement was frozen + signed at build time and the
38
+ * token is minted later (automatically when the mint recovers, or via
39
39
  * `kici-admin attestations retry`). The job still completes successfully.
40
40
  */
41
41
  deferred: true;
@@ -0,0 +1,33 @@
1
+ import type { ChangedFilesStatus } from '@kici-dev/engine';
2
+ import type { EventPayload } from '../events/event-payloads.js';
3
+ import type { FanoutPosition } from '../fanout-context.js';
4
+ import type { RuleContext } from './types.js';
5
+ /**
6
+ * Thrown when a rule reads `ctx.changedFiles` but the diff is not available
7
+ * (`changedFilesStatus !== 'fetched'`). `evaluateRules` re-throws this rather
8
+ * than folding it into a `passed=false` skip, so the job fails loudly instead
9
+ * of silently mis-evaluating a path-based gate.
10
+ */
11
+ export declare class ChangedFilesUnavailableError extends Error {
12
+ readonly changedFilesStatus: ChangedFilesStatus;
13
+ readonly eventType?: string;
14
+ constructor(status: ChangedFilesStatus, eventType?: string);
15
+ }
16
+ /** Input for {@link createRuleContext}. */
17
+ export interface CreateRuleContextInput {
18
+ event: EventPayload | Record<string, unknown>;
19
+ changedFiles?: string[];
20
+ /** Defaults to `'fetched'` — a caller that passes a real list needs no status. */
21
+ changedFilesStatus?: ChangedFilesStatus;
22
+ env?: Record<string, string | undefined>;
23
+ dispatchInputs?: Readonly<Record<string, string | number | boolean | null>>;
24
+ fanout?: FanoutPosition;
25
+ }
26
+ /**
27
+ * Build a RuleContext. `changedFiles` is exposed as a getter: it returns the
28
+ * list when `changedFilesStatus === 'fetched'`, otherwise it throws
29
+ * `ChangedFilesUnavailableError`. This is the single construction site for a
30
+ * rule context across the agent, the compiler test-runner, and tests.
31
+ */
32
+ export declare function createRuleContext(input: CreateRuleContextInput): RuleContext;
33
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1,51 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { $ } from "zx";
3
+ //#region src/rules/context.ts
4
+ /**
5
+ * Thrown when a rule reads `ctx.changedFiles` but the diff is not available
6
+ * (`changedFilesStatus !== 'fetched'`). `evaluateRules` re-throws this rather
7
+ * than folding it into a `passed=false` skip, so the job fails loudly instead
8
+ * of silently mis-evaluating a path-based gate.
9
+ */
10
+ var ChangedFilesUnavailableError = class extends Error {
11
+ changedFilesStatus;
12
+ eventType;
13
+ constructor(status, eventType) {
14
+ super(`ctx.changedFiles is not available (status: ${status}` + (eventType ? `, event: ${eventType}` : "") + "). Changed files are only defined for push / pull_request events with a computable diff. Guard with ctx.changedFilesStatus before accessing, e.g. `if (ctx.changedFilesStatus !== 'fetched') return true`.");
15
+ this.name = "ChangedFilesUnavailableError";
16
+ this.changedFilesStatus = status;
17
+ this.eventType = eventType;
18
+ }
19
+ };
20
+ /**
21
+ * Build a RuleContext. `changedFiles` is exposed as a getter: it returns the
22
+ * list when `changedFilesStatus === 'fetched'`, otherwise it throws
23
+ * `ChangedFilesUnavailableError`. This is the single construction site for a
24
+ * rule context across the agent, the compiler test-runner, and tests.
25
+ */
26
+ function createRuleContext(input) {
27
+ const status = input.changedFilesStatus ?? "fetched";
28
+ const files = input.changedFiles ?? [];
29
+ const eventType = typeof input.event.type === "string" ? input.event.type : void 0;
30
+ const base = {
31
+ event: input.event,
32
+ changedFilesStatus: status,
33
+ env: input.env ?? {},
34
+ dispatchInputs: input.dispatchInputs ?? {},
35
+ ...input.fanout && { fanout: input.fanout },
36
+ $
37
+ };
38
+ Object.defineProperty(base, "changedFiles", {
39
+ enumerable: true,
40
+ configurable: true,
41
+ get() {
42
+ if (status !== "fetched") throw new ChangedFilesUnavailableError(status, eventType);
43
+ return files;
44
+ }
45
+ });
46
+ return base;
47
+ }
48
+ //#endregion
49
+ export { ChangedFilesUnavailableError, createRuleContext };
50
+
51
+ //# sourceMappingURL=context.js.map
@@ -6,6 +6,16 @@ import type { Rule, RuleContext, RuleResult } from './types.js';
6
6
  export interface RuleEvaluationResult {
7
7
  allPassed: boolean;
8
8
  results: RuleResult[];
9
+ /**
10
+ * Set iff a rule's check() threw — an evaluation failure, NOT a clean
11
+ * `return false`. Consumers fail-hard on this (surface the error + fail the
12
+ * job/step) instead of treating a crashed gate as a silent skip, which would
13
+ * be a false green. A clean false leaves this undefined.
14
+ */
15
+ evaluationError?: {
16
+ label: string;
17
+ message: string;
18
+ };
9
19
  }
10
20
  /**
11
21
  * Evaluate rules sequentially with fail-fast behavior.
@@ -1,4 +1,5 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
+ import { ChangedFilesUnavailableError } from "./context.js";
2
3
  import { toErrorMessage } from "@kici-dev/core";
3
4
  //#region src/rules/evaluator.ts
4
5
  /**
@@ -17,6 +18,7 @@ import { toErrorMessage } from "@kici-dev/core";
17
18
  async function evaluateRules(rules, context, _label, onRuleResult) {
18
19
  const results = [];
19
20
  let allPassed = true;
21
+ let evaluationError;
20
22
  for (const rule of rules) {
21
23
  const startTime = Date.now();
22
24
  let passed = false;
@@ -24,8 +26,13 @@ async function evaluateRules(rules, context, _label, onRuleResult) {
24
26
  try {
25
27
  passed = await rule.check(context);
26
28
  } catch (e) {
29
+ if (e instanceof ChangedFilesUnavailableError) throw e;
27
30
  passed = false;
28
31
  error = toErrorMessage(e);
32
+ evaluationError = {
33
+ label: rule.label,
34
+ message: error
35
+ };
29
36
  }
30
37
  const durationMs = Date.now() - startTime;
31
38
  const result = {
@@ -43,7 +50,8 @@ async function evaluateRules(rules, context, _label, onRuleResult) {
43
50
  }
44
51
  return {
45
52
  allPassed,
46
- results
53
+ results,
54
+ evaluationError
47
55
  };
48
56
  }
49
57
  //#endregion
@@ -1,5 +1,7 @@
1
1
  export { rule, skip, onlyOnFirstHost, onlyOnLastHost, onlyOnFanoutIndex } from './rule.js';
2
2
  export { evaluateRules, type RuleEvaluationResult } from './evaluator.js';
3
+ export { createRuleContext, ChangedFilesUnavailableError } from './context.js';
4
+ export type { CreateRuleContextInput } from './context.js';
3
5
  export type { Rule, RuleCheckFn, RuleContext, RuleResult, EventPayload } from './types.js';
4
6
  export { isEventType } from '../events/event-payloads.js';
5
7
  export type { EventBase, PullRequestEventPayload, PushEventPayload, TagEventPayload, CommentEventPayload, ReviewEventPayload, ReviewCommentEventPayload, ReleaseEventPayload, DispatchEventPayload, CreateEventPayload, DeleteEventPayload, StatusEventPayload, WorkflowRunEventPayload, ForkEventPayload, StarEventPayload, WatchEventPayload, WebhookEventPayload, KiciEventPayload, WorkflowCompleteEventPayload, JobCompleteEventPayload, GenericWebhookEventPayload, ScheduleEventPayload, LifecycleEventPayload, GitHubRepository, GitHubUser, GitHubPullRequest, GitHubCommit, GitHubComment, GitHubReview, GitHubRelease, } from '../events/event-payloads.js';
@@ -1,5 +1,6 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { onlyOnFanoutIndex, onlyOnFirstHost, onlyOnLastHost, rule, skip } from "./rule.js";
3
+ import { ChangedFilesUnavailableError, createRuleContext } from "./context.js";
3
4
  import { evaluateRules } from "./evaluator.js";
4
5
  import { isEventType } from "../events/event-payloads.js";
5
- export { evaluateRules, isEventType, onlyOnFanoutIndex, onlyOnFirstHost, onlyOnLastHost, rule, skip };
6
+ export { ChangedFilesUnavailableError, createRuleContext, evaluateRules, isEventType, onlyOnFanoutIndex, onlyOnFirstHost, onlyOnLastHost, rule, skip };