@kici-dev/compiler 0.4.0 → 0.6.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 (83) hide show
  1. package/dist/cli.js +37 -7
  2. package/dist/commands/approve.d.ts +12 -0
  3. package/dist/commands/approve.js +5 -2
  4. package/dist/commands/compile.js +5 -15
  5. package/dist/commands/doctor.js +2 -2
  6. package/dist/commands/endpoints.js +4 -6
  7. package/dist/commands/held-run-client.d.ts +21 -1
  8. package/dist/commands/held-run-client.js +34 -15
  9. package/dist/commands/hook.js +22 -20
  10. package/dist/commands/index.d.ts +2 -0
  11. package/dist/commands/index.js +2 -1
  12. package/dist/commands/init.d.ts +9 -2
  13. package/dist/commands/init.js +43 -16
  14. package/dist/commands/login.js +1 -1
  15. package/dist/commands/orchestrators.js +3 -2
  16. package/dist/commands/preview.js +1 -8
  17. package/dist/commands/reject.d.ts +12 -0
  18. package/dist/commands/reject.js +5 -2
  19. package/dist/commands/report/collect.d.ts +82 -0
  20. package/dist/commands/report/collect.js +234 -0
  21. package/dist/commands/report/identity.d.ts +48 -0
  22. package/dist/commands/report/identity.js +49 -0
  23. package/dist/commands/report/index.d.ts +63 -0
  24. package/dist/commands/report/index.js +119 -0
  25. package/dist/commands/report/upload.d.ts +38 -0
  26. package/dist/commands/report/upload.js +64 -0
  27. package/dist/commands/run-hold-watch.js +2 -2
  28. package/dist/commands/run.js +6 -3
  29. package/dist/commands/runs/show.js +80 -1
  30. package/dist/commands/types.js +51 -9
  31. package/dist/errors/formatter.d.ts +2 -4
  32. package/dist/errors/formatter.js +1 -3
  33. package/dist/errors/index.d.ts +1 -1
  34. package/dist/errors/index.js +2 -2
  35. package/dist/execution/sdk-alias.js +4 -2
  36. package/dist/fixtures/compiler.js +2 -1
  37. package/dist/format.js +3 -3
  38. package/dist/generators/secrets-dts.d.ts +8 -3
  39. package/dist/generators/secrets-dts.js +3 -3
  40. package/dist/hooks/installer.js +2 -1
  41. package/dist/llm-context/llms-architecture.txt +59 -23
  42. package/dist/llm-context/llms-cli.txt +168 -34
  43. package/dist/llm-context/llms-features-execution.txt +2017 -0
  44. package/dist/llm-context/llms-features.txt +96 -1100
  45. package/dist/llm-context/llms-full.txt +3131 -1407
  46. package/dist/llm-context/llms-getting-started.txt +164 -31
  47. package/dist/llm-context/llms-patterns.txt +186 -7
  48. package/dist/llm-context/llms-providers.txt +15 -33
  49. package/dist/llm-context/llms-sdk-runtime.txt +62 -40
  50. package/dist/llm-context/llms-sdk.txt +284 -58
  51. package/dist/llm-context/llms.txt +23 -15
  52. package/dist/local-plane/paths.d.ts +15 -0
  53. package/dist/local-plane/paths.js +22 -1
  54. package/dist/local-plane/plane-manager.js +2 -2
  55. package/dist/local-plane/port-holder.js +1 -1
  56. package/dist/local-plane/postgres.d.ts +3 -16
  57. package/dist/local-plane/postgres.js +10 -15
  58. package/dist/lockfile/generator.d.ts +12 -0
  59. package/dist/lockfile/generator.js +182 -54
  60. package/dist/lockfile/index.d.ts +0 -2
  61. package/dist/lockfile/index.js +1 -2
  62. package/dist/postinstall.js +2 -1
  63. package/dist/remote/config.d.ts +2 -15
  64. package/dist/remote/config.js +2 -16
  65. package/dist/remote/dashboard-client.d.ts +39 -0
  66. package/dist/remote/dashboard-client.js +41 -0
  67. package/dist/remote/oauth.js +7 -5
  68. package/dist/remote/uploader.js +2 -2
  69. package/dist/templates/package-json.js +1 -1
  70. package/dist/test-runner/dry-run.d.ts +1 -2
  71. package/dist/test-runner/dry-run.js +5 -20
  72. package/dist/test-runner/git-detector.js +2 -1
  73. package/dist/test-runner/job-executor.js +2 -1
  74. package/dist/test-runner/payload-builder.js +11 -17
  75. package/dist/types.d.ts +64 -10
  76. package/dist/types.js +7 -1
  77. package/dist/validation/validator.js +63 -6
  78. package/package.json +16 -11
  79. package/sbom.spdx.json +957 -900
  80. package/dist/lockfile/purity-analyzer.d.ts +0 -25
  81. package/dist/lockfile/purity-analyzer.js +0 -204
  82. package/dist/lockfile/purity-diagnostics.d.ts +0 -31
  83. package/dist/lockfile/purity-diagnostics.js +0 -52
@@ -1,5 +1,6 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
2
  import path from "node:path";
3
+ import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  //#region src/local-plane/paths.ts
5
6
  /**
@@ -11,10 +12,30 @@ const PLANE_STAMP_VERSION = 3;
11
12
  /**
12
13
  * Root directory of the local dev plane's state, following the same
13
14
  * `KICI_CONFIG_DIR` → `~/.kici` convention the rest of the CLI uses.
15
+ *
16
+ * RESOLVED THROUGH SYMLINKS, deliberately. The plane is a singleton on fixed
17
+ * ports, so a caller may reach it through a config dir that only symlinks
18
+ * `local` at the durable one — `pnpm deploy:stg` does exactly that, to run
19
+ * against a throwaway config dir carrying no credentials while still reusing
20
+ * the warm plane.
21
+ *
22
+ * Without resolving, the plane's Postgres is started with a data directory
23
+ * addressed through that ephemeral path and keeps it open. When the caller
24
+ * removes its temp dir, Postgres PANICs — `could not open file
25
+ * "<tmp>/local/pgdata/global/pg_control"` — and shuts the whole plane down,
26
+ * taking every later phase with it. Resolving first means Postgres only ever
27
+ * sees the durable path, so a caller's temp dir can come and go beneath it.
28
+ *
29
+ * A path that does not exist yet resolves to itself: a fresh plane creates it.
14
30
  */
15
31
  function planeRoot() {
16
32
  const base = process.env.KICI_CONFIG_DIR ?? path.join(os.homedir(), ".kici");
17
- return path.join(base, "local");
33
+ const root = path.join(base, "local");
34
+ try {
35
+ return fs.realpathSync(root);
36
+ } catch {
37
+ return root;
38
+ }
18
39
  }
19
40
  /**
20
41
  * Absolute paths for every artefact the plane persists under its state root.
@@ -19,8 +19,8 @@ import { generateKeyPairSync, randomBytes } from "node:crypto";
19
19
  */
20
20
  function currentBuildIdentity() {
21
21
  return {
22
- version: "0.4.0",
23
- buildCommit: "ffd489464"
22
+ version: "0.6.0",
23
+ buildCommit: "e4a936029"
24
24
  };
25
25
  }
26
26
  /**
@@ -15,7 +15,7 @@ import net from "node:net";
15
15
  const defaultExec = (cmd, args) => new Promise((resolve) => {
16
16
  execFile(cmd, args, {
17
17
  timeout: 5e3,
18
- maxBuffer: 8 * 1024 * 1024
18
+ maxBuffer: 8388608
19
19
  }, (err, stdout) => {
20
20
  const raw = err?.code;
21
21
  resolve({
@@ -3,7 +3,7 @@
3
3
  * `TS_CONST_PINS` in hack/containers-lock.ts (identity
4
4
  * docker.io/library/postgres+alpine), so a lock bump rewrites it in place.
5
5
  */
6
- export declare const PLANE_PG_IMAGE = "docker.io/library/postgres:18.4-alpine";
6
+ export declare const PLANE_PG_IMAGE = "docker.io/library/postgres:18.6-alpine";
7
7
  /** Name of the fallback podman Postgres container. */
8
8
  export declare const PLANE_PG_CONTAINER = "kici-local-postgres";
9
9
  export type PlanePgHandle = {
@@ -11,13 +11,6 @@ export type PlanePgHandle = {
11
11
  kind: 'embedded' | 'podman';
12
12
  stop(): Promise<void>;
13
13
  };
14
- declare let readyPoller: typeof defaultReadyPoller;
15
- /** Test seam: override the podman readiness poller. */
16
- export declare function __setReadyPollerForTest(fn: typeof readyPoller): void;
17
- declare let embeddedDaemon: typeof defaultEmbeddedDaemon;
18
- /** Test seam: override the embedded postmaster daemonizer. */
19
- export declare function __setEmbeddedDaemonForTest(fn: typeof embeddedDaemon): void;
20
- declare function defaultReadyPoller(_port: number): Promise<boolean>;
21
14
  /**
22
15
  * Whether this plane's embedded postmaster is already serving `port`.
23
16
  *
@@ -31,13 +24,6 @@ declare function defaultReadyPoller(_port: number): Promise<boolean>;
31
24
  * one this plane can reuse.
32
25
  */
33
26
  export declare function embeddedClusterIsServing(port: number): Promise<boolean>;
34
- /**
35
- * Start a detached embedded postmaster via `pg_ctl` so it survives the exit of
36
- * this CLI process (embedded-postgres's in-process server is killed by its own
37
- * exit hook, so it cannot back a warm plane). A cluster already serving this
38
- * plane's port is reused as-is.
39
- */
40
- declare function defaultEmbeddedDaemon(port: number): Promise<void>;
41
27
  /**
42
28
  * Stop the plane's Postgres by backend kind. Handle-independent so a separate
43
29
  * CLI invocation (`kici local down`) can tear down what `up` started.
@@ -51,6 +37,7 @@ export declare function stopPlanePostgres(kind: 'embedded' | 'podman'): Promise<
51
37
  */
52
38
  export declare function startPlanePostgres(opts?: {
53
39
  forcePodman?: boolean;
40
+ readyPoller?: (port: number) => Promise<boolean>;
41
+ embeddedDaemon?: (port: number) => Promise<void>;
54
42
  }): Promise<PlanePgHandle>;
55
- export {};
56
43
  //# sourceMappingURL=postgres.d.ts.map
@@ -13,19 +13,9 @@ import EmbeddedPostgres from "embedded-postgres";
13
13
  * `TS_CONST_PINS` in hack/containers-lock.ts (identity
14
14
  * docker.io/library/postgres+alpine), so a lock bump rewrites it in place.
15
15
  */
16
- const PLANE_PG_IMAGE = "docker.io/library/postgres:18.4-alpine";
16
+ const PLANE_PG_IMAGE = "docker.io/library/postgres:18.6-alpine";
17
17
  /** Name of the fallback podman Postgres container. */
18
18
  const PLANE_PG_CONTAINER = "kici-local-postgres";
19
- let readyPoller = defaultReadyPoller;
20
- /** Test seam: override the podman readiness poller. */
21
- function __setReadyPollerForTest(fn) {
22
- readyPoller = fn;
23
- }
24
- let embeddedDaemon = defaultEmbeddedDaemon;
25
- /** Test seam: override the embedded postmaster daemonizer. */
26
- function __setEmbeddedDaemonForTest(fn) {
27
- embeddedDaemon = fn;
28
- }
29
19
  async function defaultReadyPoller(_port) {
30
20
  for (let i = 0; i < 60; i++) try {
31
21
  await $`podman exec ${PLANE_PG_CONTAINER} pg_isready -U kici`.quiet();
@@ -56,7 +46,8 @@ function resolvePgCtl() {
56
46
  "win32:x64": "@embedded-postgres/windows-x64"
57
47
  }[`${platform}:${arch}`];
58
48
  if (!pkg) throw new Error(`unsupported platform for embedded Postgres: ${platform}/${arch}`);
59
- const entry = createRequire(createRequire(import.meta.url).resolve("embedded-postgres")).resolve(pkg);
49
+ const require = createRequire(import.meta.url);
50
+ const entry = createRequire(require.resolve("embedded-postgres")).resolve(pkg);
60
51
  const binName = platform === "win32" ? "pg_ctl.exe" : "pg_ctl";
61
52
  return path.resolve(path.dirname(entry), "..", "native", "bin", binName);
62
53
  }
@@ -119,13 +110,15 @@ async function embeddedClusterIsServing(port) {
119
110
  async function defaultEmbeddedDaemon(port) {
120
111
  const { pgData, logFile } = planePaths();
121
112
  if (await embeddedClusterIsServing(port)) return;
122
- await $`${resolvePgCtl()} -D ${pgData} -o ${`-p ${port}`} -l ${`${logFile}.pg`} -w start`.quiet();
113
+ const pgCtl = resolvePgCtl();
114
+ await $`${pgCtl} -D ${pgData} -o ${`-p ${port}`} -l ${`${logFile}.pg`} -w start`.quiet();
123
115
  }
124
116
  /** Stop the detached embedded postmaster (handle-independent, reads the data dir). */
125
117
  async function stopEmbeddedDaemon() {
126
118
  const { pgData } = planePaths();
127
119
  if (!fs.existsSync(path.join(pgData, "postmaster.pid"))) return;
128
- await $`${resolvePgCtl()} -D ${pgData} stop -m fast`.quiet().catch(() => {});
120
+ const pgCtl = resolvePgCtl();
121
+ await $`${pgCtl} -D ${pgData} stop -m fast`.quiet().catch(() => {});
129
122
  }
130
123
  /**
131
124
  * Stop the plane's Postgres by backend kind. Handle-independent so a separate
@@ -142,6 +135,8 @@ async function stopPlanePostgres(kind) {
142
135
  * (or when forced via `forcePodman` / `KICI_LOCAL_PG_MODE=podman`).
143
136
  */
144
137
  async function startPlanePostgres(opts = {}) {
138
+ const readyPoller = opts.readyPoller ?? defaultReadyPoller;
139
+ const embeddedDaemon = opts.embeddedDaemon ?? defaultEmbeddedDaemon;
145
140
  const { postgres: port } = planePorts();
146
141
  const url = `postgres://kici:kici@127.0.0.1:${port}/kici_local`;
147
142
  if (!(opts.forcePodman || process.env.KICI_LOCAL_PG_MODE === "podman")) try {
@@ -182,6 +177,6 @@ async function startPlanePostgres(opts = {}) {
182
177
  };
183
178
  }
184
179
  //#endregion
185
- export { PLANE_PG_CONTAINER, PLANE_PG_IMAGE, __setEmbeddedDaemonForTest, __setReadyPollerForTest, embeddedClusterIsServing, startPlanePostgres, stopPlanePostgres };
180
+ export { PLANE_PG_CONTAINER, PLANE_PG_IMAGE, embeddedClusterIsServing, startPlanePostgres, stopPlanePostgres };
186
181
 
187
182
  //# sourceMappingURL=postgres.js.map
@@ -36,6 +36,18 @@ export declare function detectGitRoot(): string;
36
36
  * For yarn the prefix also carries the flavor (`yarn-classic` / `yarn-berry`),
37
37
  * so a classic-layout dep-cache tarball is never restored into a berry install.
38
38
  *
39
+ * The detected manager only orders the search; it never restricts it. Detection
40
+ * falls back to the AMBIENT `npm_config_user_agent` when a directory carries no
41
+ * signal of its own, so a repo whose root holds neither a `package.json` nor a
42
+ * lockfile is reported as whatever manager happened to invoke the compiler. A
43
+ * repo compiled under pnpm that keeps an npm `.kici/package-lock.json` was
44
+ * therefore searched for `pnpm-lock.yaml` only, found none, and got no hash at
45
+ * all — which silently disables the orchestrator's dependency cache for it, so
46
+ * every agent installs from the registry and an agent with no registry route
47
+ * cannot run the job at all. Searching every manager's candidates after the
48
+ * detected one's keeps the detected manager authoritative where it has real
49
+ * evidence, and still finds the lockfile that is actually on disk.
50
+ *
39
51
  * @param gitRoot - Absolute path to git repository root
40
52
  * @returns Hex SHA-256 hash string, or null if no lockfile is found
41
53
  */
@@ -5,13 +5,13 @@ import "../errors/index.js";
5
5
  import { BREAKING_FLOOR as BREAKING_FLOOR$1, SCHEMA_VERSION as SCHEMA_VERSION$1 } from "../types.js";
6
6
  import { computeContentHash } from "./hasher.js";
7
7
  import { resolveHashFiles } from "./hash-files.js";
8
- import { analyzePurity } from "./purity-analyzer.js";
9
8
  import path from "node:path";
10
9
  import { readFileSync } from "node:fs";
11
10
  import { getDynamicJobGroup, getDynamicJobNeeds, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isParallelGroup, isStaticArray, isStaticObject, normalizeApproval, normalizeCacheSpecs } from "@kici-dev/sdk";
12
11
  import { sha256 } from "@kici-dev/core";
13
12
  import { PackageManager, detectPackageManagerSync, detectYarnFlavorSync } from "@kici-dev/core/package-manager";
14
- import { assertScheduleInputsSatisfiable, extractInputsDescriptorMap, resolveWhenToRunOn, validateResourceRequest } from "@kici-dev/engine";
13
+ import { assertScheduleInputsSatisfiable, extractInputsDescriptorMap, resolveContentFormat, resolveWhenToRunOn, validateResourceRequest } from "@kici-dev/engine";
14
+ import { assertSafeRegex } from "@kici-dev/engine/safe-regex";
15
15
  import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers, runsOnPickFromInput } from "@kici-dev/engine/labels/compile";
16
16
  import { execSync } from "node:child_process";
17
17
  //#region src/lockfile/generator.ts
@@ -66,16 +66,39 @@ function detectGitRoot() {
66
66
  * For yarn the prefix also carries the flavor (`yarn-classic` / `yarn-berry`),
67
67
  * so a classic-layout dep-cache tarball is never restored into a berry install.
68
68
  *
69
+ * The detected manager only orders the search; it never restricts it. Detection
70
+ * falls back to the AMBIENT `npm_config_user_agent` when a directory carries no
71
+ * signal of its own, so a repo whose root holds neither a `package.json` nor a
72
+ * lockfile is reported as whatever manager happened to invoke the compiler. A
73
+ * repo compiled under pnpm that keeps an npm `.kici/package-lock.json` was
74
+ * therefore searched for `pnpm-lock.yaml` only, found none, and got no hash at
75
+ * all — which silently disables the orchestrator's dependency cache for it, so
76
+ * every agent installs from the registry and an agent with no registry route
77
+ * cannot run the job at all. Searching every manager's candidates after the
78
+ * detected one's keeps the detected manager authoritative where it has real
79
+ * evidence, and still finds the lockfile that is actually on disk.
80
+ *
69
81
  * @param gitRoot - Absolute path to git repository root
70
82
  * @returns Hex SHA-256 hash string, or null if no lockfile is found
71
83
  */
72
84
  function computeLockfileHash(gitRoot) {
73
85
  const pm = detectPackageManagerSync(gitRoot) === PackageManager.Npm ? detectPackageManagerSync(path.join(gitRoot, ".kici")) : detectPackageManagerSync(gitRoot);
74
- const candidates = pm === PackageManager.Pnpm ? [path.join(gitRoot, "pnpm-lock.yaml"), path.join(gitRoot, ".kici", "pnpm-lock.yaml")] : pm === PackageManager.Yarn ? [path.join(gitRoot, "yarn.lock"), path.join(gitRoot, ".kici", "yarn.lock")] : [path.join(gitRoot, ".kici", "package-lock.json")];
75
- const prefix = pm === PackageManager.Yarn ? `${pm}-${detectYarnFlavorSync(detectPackageManagerSync(gitRoot) === PackageManager.Yarn ? gitRoot : path.join(gitRoot, ".kici"))}` : pm;
76
- for (const lockfilePath of candidates) try {
77
- return sha256(`${prefix}\n${readFileSync(lockfilePath, "utf-8")}`);
78
- } catch {}
86
+ const byManager = (m) => m === PackageManager.Pnpm ? [path.join(gitRoot, "pnpm-lock.yaml"), path.join(gitRoot, ".kici", "pnpm-lock.yaml")] : m === PackageManager.Yarn ? [path.join(gitRoot, "yarn.lock"), path.join(gitRoot, ".kici", "yarn.lock")] : [path.join(gitRoot, ".kici", "package-lock.json")];
87
+ const order = [pm, ...[
88
+ PackageManager.Npm,
89
+ PackageManager.Pnpm,
90
+ PackageManager.Yarn
91
+ ].filter((m) => m !== pm)];
92
+ for (const manager of order) for (const lockfilePath of byManager(manager)) {
93
+ let content;
94
+ try {
95
+ content = readFileSync(lockfilePath, "utf-8");
96
+ } catch {
97
+ continue;
98
+ }
99
+ const prefix = manager === PackageManager.Yarn ? `${manager}-${detectYarnFlavorSync(path.dirname(lockfilePath))}` : manager;
100
+ return sha256(`${prefix}\n${content}`);
101
+ }
79
102
  return null;
80
103
  }
81
104
  /**
@@ -112,16 +135,17 @@ function generateLockFile(workflowsWithSource) {
112
135
  const workflows = workflowsWithSource.map(({ workflow, source, bundleSource }) => {
113
136
  return transformWorkflow(workflow, path.relative(gitRoot, source.file).replaceAll("\\", "/"), formatExportRef(source), bundleSource, gitRoot);
114
137
  });
138
+ const contentHash = sha256(JSON.stringify({
139
+ schemaVersion: SCHEMA_VERSION$1,
140
+ minReaderVersion: BREAKING_FLOOR$1,
141
+ source: topLevelSource,
142
+ workflows
143
+ }));
115
144
  return {
116
145
  schemaVersion: SCHEMA_VERSION$1,
117
146
  minReaderVersion: BREAKING_FLOOR$1,
118
147
  source: topLevelSource,
119
- contentHash: sha256(JSON.stringify({
120
- schemaVersion: SCHEMA_VERSION$1,
121
- minReaderVersion: BREAKING_FLOOR$1,
122
- source: topLevelSource,
123
- workflows
124
- })),
148
+ contentHash,
125
149
  ...lockfileHash && { lockfileHash },
126
150
  workflows
127
151
  };
@@ -178,7 +202,8 @@ function transformWorkflow(workflow, sourceFile, exportRef, bundleSource, gitRoo
178
202
  ...workflow.concurrency.max !== void 0 && { max: workflow.concurrency.max }
179
203
  } },
180
204
  ...workflow.timeout !== void 0 && { timeout: workflow.timeout },
181
- ...workflow.approval !== void 0 && { approval: (assertNonStepApprovalScope(workflow.approval, "workflow", locationForWorkflow(sourceFile)), toLockApproval(workflow.approval)) }
205
+ ...workflow.approval !== void 0 && { approval: (assertNonStepApprovalScope(workflow.approval, "workflow", locationForWorkflow(sourceFile)), toLockApproval(workflow.approval)) },
206
+ ...typeof workflow.filter === "function" && { hasFilter: true }
182
207
  };
183
208
  }
184
209
  /**
@@ -189,6 +214,116 @@ function transformWorkflow(workflow, sourceFile, exportRef, bundleSource, gitRoo
189
214
  function reposField(trigger) {
190
215
  return trigger.repos && trigger.repos.length > 0 ? { repos: transformBranchPatterns(trigger.repos) } : {};
191
216
  }
217
+ /** Normalize a scalar-or-array SDK value to an array. */
218
+ function toArray(v) {
219
+ if (v === void 0) return [];
220
+ return Array.isArray(v) ? v : [v];
221
+ }
222
+ /**
223
+ * Normalize one regex entry to the lock's `/pattern/flags` form, rejecting an
224
+ * invalid or ReDoS-prone pattern at compile time so a catastrophic pattern fails
225
+ * `kici compile` with author feedback instead of reaching the orchestrator.
226
+ */
227
+ function serializeRegexEntry(entry, ctx) {
228
+ const source = entry instanceof RegExp ? `/${entry.source}/${entry.flags}` : entry;
229
+ const wrapped = /^\/(.+)\/([gimsuy]*)$/.exec(source);
230
+ const pattern = wrapped ? wrapped[1] : source;
231
+ const flags = wrapped ? wrapped[2] : "";
232
+ let re;
233
+ try {
234
+ re = new RegExp(pattern, flags);
235
+ } catch (err) {
236
+ const reason = err instanceof Error ? err.message : String(err);
237
+ throw compilerError("E123", `${ctx}: invalid regex — ${reason}`);
238
+ }
239
+ try {
240
+ assertSafeRegex(re.source, re.flags, ctx);
241
+ } catch (err) {
242
+ const reason = err instanceof Error ? err.message : String(err);
243
+ throw compilerError("E123", `${ctx}: ${reason}`);
244
+ }
245
+ return `/${re.source}/${re.flags}`;
246
+ }
247
+ /**
248
+ * Serialize a {@link TextMatch} to its lock form: every key normalized to a flat
249
+ * array, every regex to `/pattern/flags`, every degenerate shape rejected.
250
+ *
251
+ * `ctx` is a human label woven into any thrown error (e.g. `push trigger
252
+ * 'commitMessage'`, `requires 'Dockerfile'`).
253
+ */
254
+ function serializeTextMatch(m, ctx) {
255
+ const contains = toArray(m.contains);
256
+ const notContains = toArray(m.notContains);
257
+ const matches = toArray(m.matches);
258
+ const notMatches = toArray(m.notMatches);
259
+ if (contains.length === 0 && notContains.length === 0 && matches.length === 0 && notMatches.length === 0) throw compilerError("E122", `${ctx}: no query key (contains/notContains/matches/notMatches) — nothing to check`);
260
+ for (const needle of [...contains, ...notContains]) if (needle === "") throw compilerError("E122", `${ctx}: an empty needle matches every text — remove it`);
261
+ if (m.ignoreCase !== void 0 && contains.length === 0 && notContains.length === 0) throw compilerError("E122", `${ctx}: 'ignoreCase' affects only contains/notContains — a regex carries its own flags`);
262
+ return {
263
+ ...contains.length > 0 && { contains: [...contains] },
264
+ ...notContains.length > 0 && { notContains: [...notContains] },
265
+ ...matches.length > 0 && { matches: matches.map((e) => serializeRegexEntry(e, ctx)) },
266
+ ...notMatches.length > 0 && { notMatches: notMatches.map((e) => serializeRegexEntry(e, ctx)) },
267
+ ...m.ignoreCase !== void 0 && { ignoreCase: m.ignoreCase }
268
+ };
269
+ }
270
+ /** Emit the optional `commitMessage` field for a git-event lock trigger. */
271
+ function commitMessageField(commitMessage, triggerLabel) {
272
+ if (commitMessage === void 0) return {};
273
+ return { commitMessage: serializeTextMatch(commitMessage, `${triggerLabel} 'commitMessage'`) };
274
+ }
275
+ /**
276
+ * Serialize one SDK content requirement into its lock form, validating and
277
+ * resolving `format: 'auto'` by extension at compile time. A malformed filter
278
+ * throws (fails `kici compile`) rather than reaching the orchestrator.
279
+ */
280
+ function serializeOneRequirement(req) {
281
+ const hasJsonQuery = req.exists !== void 0 && req.exists.length > 0 || req.match !== void 0 && Object.keys(req.match).length > 0 || req.not !== void 0 && Object.keys(req.not).length > 0;
282
+ const textKeys = {
283
+ ...req.contains !== void 0 && { contains: req.contains },
284
+ ...req.notContains !== void 0 && { notContains: req.notContains },
285
+ ...req.matches !== void 0 && { matches: req.matches },
286
+ ...req.notMatches !== void 0 && { notMatches: req.notMatches },
287
+ ...req.ignoreCase !== void 0 && { ignoreCase: req.ignoreCase }
288
+ };
289
+ const hasTextQuery = req.contains !== void 0 || req.notContains !== void 0 || req.matches !== void 0 || req.notMatches !== void 0;
290
+ const hasAnyQuery = hasJsonQuery || hasTextQuery || req.ignoreCase !== void 0;
291
+ if (req.absent) {
292
+ if (hasAnyQuery) throw compilerError("E118", `requires '${req.file}': 'absent' is mutually exclusive with query keys (exists/match/not/contains/notContains/matches/notMatches)`);
293
+ return {
294
+ file: req.file,
295
+ absent: true
296
+ };
297
+ }
298
+ const format = resolveContentFormat(req.file, req.format);
299
+ if (format === "text" && hasJsonQuery) throw compilerError("E119", `requires '${req.file}': text format cannot carry a json/yaml query key (exists/match/not) — name a .json/.yaml file or set format: 'json' | 'yaml'`);
300
+ if (format !== "text" && (hasTextQuery || req.ignoreCase !== void 0)) throw compilerError("E119", `requires '${req.file}': '${format}' format cannot carry a raw-text key (contains/notContains/matches/notMatches/ignoreCase) — use format: 'text'`);
301
+ if (!hasAnyQuery) {
302
+ if (req.format !== void 0) throw compilerError("E122", `requires '${req.file}': format '${req.format}' declared with no query key — nothing to check`);
303
+ return { file: req.file };
304
+ }
305
+ const text = hasTextQuery ? serializeTextMatch(textKeys, `requires '${req.file}'`) : {};
306
+ return {
307
+ file: req.file,
308
+ format,
309
+ ...req.exists !== void 0 && { exists: req.exists },
310
+ ...req.match !== void 0 && { match: req.match },
311
+ ...req.not !== void 0 && { not: req.not },
312
+ ...text
313
+ };
314
+ }
315
+ /**
316
+ * Serialize a trigger's `requires` list, or `undefined` when empty (so the lock
317
+ * field stays absent). Each entry is validated + format-resolved at compile time.
318
+ */
319
+ function serializeRequires(requires) {
320
+ if (!requires || requires.length === 0) return void 0;
321
+ return requires.map(serializeOneRequirement);
322
+ }
323
+ function requiresField(requires) {
324
+ const serialized = serializeRequires(requires);
325
+ return serialized ? { requires: serialized } : {};
326
+ }
192
327
  function toLockPr(t) {
193
328
  return {
194
329
  _type: "pr",
@@ -196,19 +331,27 @@ function toLockPr(t) {
196
331
  targetBranches: transformBranchPatterns(t.targetBranches),
197
332
  sourceBranches: transformBranchPatterns(t.sourceBranches),
198
333
  paths: t.paths,
199
- ...reposField(t)
334
+ ...reposField(t),
335
+ ...requiresField(t.requires),
336
+ ...commitMessageField(t.commitMessage, "pr trigger")
200
337
  };
201
338
  }
202
339
  function toLockPushAndTag(t) {
340
+ const requires = requiresField(t.requires);
341
+ const commitMessage = commitMessageField(t.commitMessage, "push trigger");
203
342
  const results = [{
204
343
  _type: "push",
205
344
  branches: transformBranchPatterns(t.branches),
206
345
  paths: t.paths,
207
- ...reposField(t)
346
+ ...reposField(t),
347
+ ...requires,
348
+ ...commitMessage
208
349
  }];
209
350
  if (t.tags.length > 0) results.push({
210
351
  _type: "tag",
211
- patterns: transformBranchPatterns(t.tags)
352
+ patterns: transformBranchPatterns(t.tags),
353
+ ...requires,
354
+ ...commitMessage
212
355
  });
213
356
  return results;
214
357
  }
@@ -216,7 +359,9 @@ function toLockTag(t) {
216
359
  return {
217
360
  _type: "tag",
218
361
  patterns: transformBranchPatterns(t.patterns),
219
- ...reposField(t)
362
+ ...reposField(t),
363
+ ...requiresField(t.requires),
364
+ ...commitMessageField(t.commitMessage, "tag trigger")
220
365
  };
221
366
  }
222
367
  function toLockComment(t) {
@@ -518,25 +663,15 @@ function validateRunsOn(runsOn, jobName, location) {
518
663
  }
519
664
  /**
520
665
  * Transform one context reference (static name or function) into a lock
521
- * `{ value, dynamic }` entry. A function element is analyzed for purity: a pure
522
- * function becomes an inline expression resolvable at two-phase eval; an impure
523
- * one carries only the `dynamic` flag (the agent runs an init job to resolve it).
666
+ * `{ value, dynamic }` entry. A function element carries only the `dynamic`
667
+ * flag (the agent resolves it in the init round); a static name carries its
668
+ * literal value.
524
669
  */
525
670
  function transformContextRef(ref) {
526
- if (typeof ref === "function") {
527
- const fnSource = ref.toString();
528
- if (analyzePurity(fnSource).pure) return {
529
- value: {
530
- _type: "inline",
531
- expression: fnSource
532
- },
533
- dynamic: true
534
- };
535
- return {
536
- value: "",
537
- dynamic: true
538
- };
539
- }
671
+ if (typeof ref === "function") return {
672
+ value: "",
673
+ dynamic: true
674
+ };
540
675
  return {
541
676
  value: ref,
542
677
  dynamic: false
@@ -551,7 +686,7 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
551
686
  location: jobLocation,
552
687
  suggestion: "Set exactly one of runsOn (single agent) or runsOnAll (fan-out to every matching agent)."
553
688
  });
554
- if (job.runsOn === void 0 && job.runsOnAll === void 0) throw compilerError("E109", `job '${job.name}': one of runsOn or runsOnAll is required`, {
689
+ if (job.invoke === void 0 && job.runsOn === void 0 && job.runsOnAll === void 0) throw compilerError("E109", `job '${job.name}': one of runsOn or runsOnAll is required`, {
555
690
  location: jobLocation,
556
691
  suggestion: "Add runsOn: \"kici:os:linux\" (or another agent label) to the job."
557
692
  });
@@ -567,15 +702,8 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
567
702
  if (contextRefs !== void 0 && contextRefs.length > 0) contextFields.contexts = contextRefs.map((ref) => transformContextRef(ref));
568
703
  const envFields = {};
569
704
  if (job.env !== void 0) {
570
- if (typeof job.env === "function") {
571
- const fnSource = job.env.toString();
572
- const purity = analyzePurity(fnSource);
573
- envFields.dynamicEnv = true;
574
- if (purity.pure) envFields.env = {
575
- _type: "inline",
576
- expression: fnSource
577
- };
578
- } else if (typeof job.env === "object") envFields.env = { ...job.env };
705
+ if (typeof job.env === "function") envFields.dynamicEnv = true;
706
+ else if (typeof job.env === "object") envFields.env = { ...job.env };
579
707
  }
580
708
  if (job.resources !== void 0) try {
581
709
  validateResourceRequest(job.resources);
@@ -588,15 +716,8 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
588
716
  }
589
717
  const concurrencyFields = {};
590
718
  if (job.concurrencyGroup !== void 0) {
591
- if (typeof job.concurrencyGroup === "function") {
592
- const fnSource = job.concurrencyGroup.toString();
593
- const purity = analyzePurity(fnSource);
594
- concurrencyFields.dynamicConcurrencyGroup = true;
595
- if (purity.pure) concurrencyFields.concurrencyGroup = {
596
- _type: "inline",
597
- expression: fnSource
598
- };
599
- } else if (typeof job.concurrencyGroup === "string") concurrencyFields.concurrencyGroup = job.concurrencyGroup;
719
+ if (typeof job.concurrencyGroup === "function") concurrencyFields.dynamicConcurrencyGroup = true;
720
+ else if (typeof job.concurrencyGroup === "string") concurrencyFields.concurrencyGroup = job.concurrencyGroup;
600
721
  }
601
722
  return {
602
723
  _type: "static",
@@ -605,8 +726,15 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
605
726
  ...job.runsOnAll !== void 0 && { runsOnAll: normalizeRunsOnAllToMatchers(job.runsOnAll, `job '${job.name}' runsOnAll`) },
606
727
  ...job.onUnreachable !== void 0 && { onUnreachable: job.onUnreachable },
607
728
  ...job.includeUninitialized !== void 0 && { includeUninitialized: job.includeUninitialized },
729
+ ...job.gitCredentials !== void 0 && { gitCredentials: job.gitCredentials },
608
730
  ...job.maxParallel !== void 0 && { maxParallel: job.maxParallel },
609
731
  ...job.failFast !== void 0 && { failFast: job.failFast },
732
+ ...job.invoke !== void 0 && { invoke: {
733
+ event: job.invoke.event,
734
+ scope: job.invoke.scope,
735
+ ...job.invoke.payload !== void 0 && { payload: job.invoke.payload },
736
+ ...job.invoke.optional === true && { optional: true }
737
+ } },
610
738
  ...resolveNeedsForLock(job.needs, uuidToName),
611
739
  steps: transformSteps(job.steps, gitRoot, jobLocation),
612
740
  matrix: job.matrix ? transformMatrix(job.matrix, job.name, configPath) : void 0,
@@ -1,5 +1,3 @@
1
1
  export { generateLockFile, serializeLockFile, detectGitRoot, computeLockfileHash, schemaWindowWarning, } from './generator.js';
2
2
  export { computeContentHash, COMPILE_SCHEMA_VERSION } from './hasher.js';
3
- export { DynamicValueField, analyzeJobPurity, collectWorkflowPurityWarnings, } from './purity-diagnostics.js';
4
- export type { JobPurityWarning } from './purity-diagnostics.js';
5
3
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,4 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { COMPILE_SCHEMA_VERSION, computeContentHash } from "./hasher.js";
3
3
  import { computeLockfileHash, detectGitRoot, generateLockFile, schemaWindowWarning, serializeLockFile } from "./generator.js";
4
- import { DynamicValueField, analyzeJobPurity, collectWorkflowPurityWarnings } from "./purity-diagnostics.js";
5
- export { COMPILE_SCHEMA_VERSION, DynamicValueField, analyzeJobPurity, collectWorkflowPurityWarnings, computeContentHash, computeLockfileHash, detectGitRoot, generateLockFile, schemaWindowWarning, serializeLockFile };
4
+ export { COMPILE_SCHEMA_VERSION, computeContentHash, computeLockfileHash, detectGitRoot, generateLockFile, schemaWindowWarning, serializeLockFile };
@@ -26,7 +26,8 @@ async function detectDevelopmentMode() {
26
26
  if (process.env.KICI_DEV === "true") return true;
27
27
  try {
28
28
  const rootDir = process.env.INIT_CWD || process.cwd();
29
- const content = await readFile(path.join(rootDir, "package.json"), "utf-8");
29
+ const rootPkgPath = path.join(rootDir, "package.json");
30
+ const content = await readFile(rootPkgPath, "utf-8");
30
31
  return JSON.parse(content).kici?.development === true;
31
32
  } catch {
32
33
  return false;
@@ -35,22 +35,9 @@ export interface GlobalConfig {
35
35
  *
36
36
  * Resolution order:
37
37
  * 1. `KICI_CONFIG_DIR`, when set to a non-empty value.
38
- * 2. A hard refusal when `KICI_TEST_ISOLATION` is present. A developer
39
- * machine's `~/.kici/config` names a real endpoint and carries a live
40
- * PAT, so a test process that reads it authenticates against whatever
41
- * that config points at — in practice, production. Every test that needs
42
- * a config must name its own isolated directory.
43
- * 3. `~/.kici`.
38
+ * 2. `~/.kici`.
44
39
  *
45
- * `KICI_TEST_ISOLATION` is set by this repository's own vitest configs, via
46
- * `hack/lib/vitest-isolation.ts`. It is deliberately a KiCI-owned name rather
47
- * than the runner's `VITEST`: `kici` is a compat-protected CLI, and `VITEST`
48
- * is a third-party marker that propagates into spawned children, so keying off
49
- * it would break a customer whose vitest test shells out to `kici`.
50
- *
51
- * `env` is injectable so this module's own tests can exercise every branch.
52
- * It is NOT an opt-out: production call sites pass no argument and inherit the
53
- * guard.
40
+ * `env` is injectable so this module's own tests can exercise both branches.
54
41
  */
55
42
  export declare function getConfigDir(env?: NodeJS.ProcessEnv): string;
56
43
  /**
@@ -33,26 +33,12 @@ function sanitizeConfig(raw) {
33
33
  *
34
34
  * Resolution order:
35
35
  * 1. `KICI_CONFIG_DIR`, when set to a non-empty value.
36
- * 2. A hard refusal when `KICI_TEST_ISOLATION` is present. A developer
37
- * machine's `~/.kici/config` names a real endpoint and carries a live
38
- * PAT, so a test process that reads it authenticates against whatever
39
- * that config points at — in practice, production. Every test that needs
40
- * a config must name its own isolated directory.
41
- * 3. `~/.kici`.
36
+ * 2. `~/.kici`.
42
37
  *
43
- * `KICI_TEST_ISOLATION` is set by this repository's own vitest configs, via
44
- * `hack/lib/vitest-isolation.ts`. It is deliberately a KiCI-owned name rather
45
- * than the runner's `VITEST`: `kici` is a compat-protected CLI, and `VITEST`
46
- * is a third-party marker that propagates into spawned children, so keying off
47
- * it would break a customer whose vitest test shells out to `kici`.
48
- *
49
- * `env` is injectable so this module's own tests can exercise every branch.
50
- * It is NOT an opt-out: production call sites pass no argument and inherit the
51
- * guard.
38
+ * `env` is injectable so this module's own tests can exercise both branches.
52
39
  */
53
40
  function getConfigDir(env = process.env) {
54
41
  if (env.KICI_CONFIG_DIR) return env.KICI_CONFIG_DIR;
55
- if (env.KICI_TEST_ISOLATION) throw new Error("Refusing to read the ambient ~/.kici config from a test process. Set KICI_CONFIG_DIR to an isolated directory for this run — the developer machine config may point at production.");
56
42
  return path.join(os.homedir(), ".kici");
57
43
  }
58
44
  /**