@intentius/chant 0.124.23 → 0.124.24

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 (123) hide show
  1. package/dist/ci-jobs.d.ts +41 -1
  2. package/dist/ci-jobs.d.ts.map +1 -1
  3. package/dist/ci-jobs.js +48 -1
  4. package/dist/cli/commands/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.js +14 -0
  6. package/dist/cli/handlers/approve.d.ts.map +1 -1
  7. package/dist/cli/handlers/approve.js +9 -0
  8. package/dist/cli/handlers/emulator.d.ts +13 -2
  9. package/dist/cli/handlers/emulator.d.ts.map +1 -1
  10. package/dist/cli/handlers/emulator.js +63 -3
  11. package/dist/cli/handlers/promote.d.ts.map +1 -1
  12. package/dist/cli/handlers/promote.js +7 -1
  13. package/dist/cli/handlers/run-wave.d.ts.map +1 -1
  14. package/dist/cli/handlers/run-wave.js +5 -4
  15. package/dist/cli/handlers/run.d.ts +4 -3
  16. package/dist/cli/handlers/run.d.ts.map +1 -1
  17. package/dist/cli/handlers/run.js +73 -9
  18. package/dist/cli/main.d.ts.map +1 -1
  19. package/dist/cli/main.js +17 -4
  20. package/dist/cli/parse-args.d.ts.map +1 -1
  21. package/dist/cli/parse-args.js +31 -0
  22. package/dist/cli/registry.d.ts +12 -0
  23. package/dist/cli/registry.d.ts.map +1 -1
  24. package/dist/components/cli-support.d.ts +45 -6
  25. package/dist/components/cli-support.d.ts.map +1 -1
  26. package/dist/components/cli-support.js +97 -22
  27. package/dist/components/driver.d.ts.map +1 -1
  28. package/dist/components/driver.js +25 -10
  29. package/dist/graph-detail.d.ts.map +1 -1
  30. package/dist/graph-detail.js +24 -2
  31. package/dist/lifecycle/git.d.ts +11 -0
  32. package/dist/lifecycle/git.d.ts.map +1 -1
  33. package/dist/lifecycle/git.js +62 -6
  34. package/dist/lifecycle/no-record.d.ts +59 -0
  35. package/dist/lifecycle/no-record.d.ts.map +1 -0
  36. package/dist/lifecycle/no-record.js +88 -0
  37. package/dist/op/activities/shell.d.ts.map +1 -1
  38. package/dist/op/activities/shell.js +30 -9
  39. package/dist/op/emulator-lifecycle.d.ts +23 -0
  40. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  41. package/dist/op/emulator-lifecycle.js +39 -2
  42. package/dist/op/gate.d.ts.map +1 -1
  43. package/dist/op/gate.js +3 -0
  44. package/dist/op/index.d.ts +1 -1
  45. package/dist/op/index.d.ts.map +1 -1
  46. package/dist/op/index.js +1 -1
  47. package/dist/op/local-executor.d.ts +10 -0
  48. package/dist/op/local-executor.d.ts.map +1 -1
  49. package/dist/op/local-executor.js +7 -2
  50. package/dist/op/op-waves-notify.d.ts +2 -0
  51. package/dist/op/op-waves-notify.d.ts.map +1 -1
  52. package/dist/op/op-waves-notify.js +9 -3
  53. package/dist/op/op-waves-run.d.ts +8 -2
  54. package/dist/op/op-waves-run.d.ts.map +1 -1
  55. package/dist/op/op-waves-run.js +36 -11
  56. package/dist/op/op-waves.d.ts +85 -1
  57. package/dist/op/op-waves.d.ts.map +1 -1
  58. package/dist/op/op-waves.js +153 -1
  59. package/dist/op/runtimes/local.d.ts.map +1 -1
  60. package/dist/op/runtimes/local.js +6 -0
  61. package/dist/op/step-console.d.ts +65 -0
  62. package/dist/op/step-console.d.ts.map +1 -0
  63. package/dist/op/step-console.js +140 -0
  64. package/dist/point-decision.d.ts.map +1 -1
  65. package/dist/point-decision.js +3 -0
  66. package/dist/wave-runtime.mjs +238 -238
  67. package/dist/workspace/compose-graph.d.ts +22 -3
  68. package/dist/workspace/compose-graph.d.ts.map +1 -1
  69. package/dist/workspace/compose-graph.js +18 -5
  70. package/dist/workspace/graph.schema.json +68 -1
  71. package/dist/workspace/nested-graph.d.ts +1 -1
  72. package/dist/workspace/nested-graph.d.ts.map +1 -1
  73. package/dist/workspace/nested-graph.js +5 -1
  74. package/dist/workspace/runs.d.ts.map +1 -1
  75. package/dist/workspace/runs.js +4 -2
  76. package/package.json +1 -1
  77. package/src/ci-jobs.ts +58 -1
  78. package/src/cli/commands/build.ts +18 -0
  79. package/src/cli/handlers/approve.ts +9 -0
  80. package/src/cli/handlers/emulator.test.ts +61 -1
  81. package/src/cli/handlers/emulator.ts +65 -4
  82. package/src/cli/handlers/promote.test.ts +7 -0
  83. package/src/cli/handlers/promote.ts +7 -1
  84. package/src/cli/handlers/run-no-record.e2e.test.ts +87 -0
  85. package/src/cli/handlers/run-wave.ts +5 -4
  86. package/src/cli/handlers/run.test.ts +126 -0
  87. package/src/cli/handlers/run.ts +69 -9
  88. package/src/cli/main.test.ts +24 -0
  89. package/src/cli/main.ts +17 -4
  90. package/src/cli/parse-args.ts +21 -0
  91. package/src/cli/registry.ts +12 -0
  92. package/src/components/cli-support.test.ts +75 -0
  93. package/src/components/cli-support.ts +130 -25
  94. package/src/components/driver.test.ts +28 -0
  95. package/src/components/driver.ts +23 -10
  96. package/src/components/generate-disabled.test.ts +56 -0
  97. package/src/graph-ci-edges.test.ts +50 -0
  98. package/src/graph-detail.ts +21 -2
  99. package/src/lifecycle/git.ts +61 -6
  100. package/src/lifecycle/no-record.test.ts +175 -0
  101. package/src/lifecycle/no-record.ts +108 -0
  102. package/src/op/activities/shell.ts +28 -6
  103. package/src/op/emulator-lifecycle.test.ts +24 -1
  104. package/src/op/emulator-lifecycle.ts +58 -2
  105. package/src/op/gate.ts +3 -0
  106. package/src/op/index.ts +1 -1
  107. package/src/op/local-executor.ts +20 -3
  108. package/src/op/op-waves-jobs.test.ts +213 -0
  109. package/src/op/op-waves-notify.ts +11 -5
  110. package/src/op/op-waves-run.ts +42 -11
  111. package/src/op/op-waves.ts +202 -2
  112. package/src/op/runtimes/local.ts +6 -0
  113. package/src/op/step-console.test.ts +150 -0
  114. package/src/op/step-console.ts +155 -0
  115. package/src/point-decision-bundle.test.ts +1 -0
  116. package/src/point-decision.ts +3 -0
  117. package/src/workspace/compose-graph.test.ts +55 -0
  118. package/src/workspace/compose-graph.ts +28 -6
  119. package/src/workspace/graph-contract.test.ts +26 -1
  120. package/src/workspace/graph.schema.json +68 -1
  121. package/src/workspace/nested-graph.test.ts +14 -0
  122. package/src/workspace/nested-graph.ts +7 -2
  123. package/src/workspace/runs.ts +4 -2
@@ -216,6 +216,8 @@ export function assertOpWavesSpec(spec) {
216
216
  }
217
217
  if (spec.neverOverMcp !== undefined && typeof spec.neverOverMcp !== "boolean")
218
218
  throw new Error(`${where}: neverOverMcp must be true or false.`);
219
+ if (spec.runner !== undefined)
220
+ assertOpWaveRunner(where, "runner", spec.runner);
219
221
  if (spec.notify !== undefined) {
220
222
  const problem = notifyConfigProblem(spec.notify, "notify");
221
223
  if (problem)
@@ -241,6 +243,12 @@ export function assertOpWavesSpec(spec) {
241
243
  if (problems.length > 0)
242
244
  throw new Error(`${where}: wave "${wave.name}": ${problems.join("; ")}.`);
243
245
  }
246
+ if (wave.runner !== undefined)
247
+ assertOpWaveRunner(where, `wave "${wave.name}" runner`, wave.runner);
248
+ if (wave.idTokens !== undefined)
249
+ assertOpWaveIdTokens(where, `wave "${wave.name}" idTokens`, wave.idTokens);
250
+ if (wave.jobs !== undefined)
251
+ assertOpWaveJobGroups(where, wave);
244
252
  if (wave.discover !== undefined && typeof wave.discover !== "boolean")
245
253
  throw new Error(`${where}: wave "${wave.name}" has discover ${JSON.stringify(wave.discover)}; use true or false.`);
246
254
  if (wave.rest !== undefined && typeof wave.rest !== "boolean")
@@ -282,7 +290,127 @@ export function assertOpWavesSpec(spec) {
282
290
  if (shares > wave.runs.length) {
283
291
  throw new Error(`${where}: wave "${wave.name}" has ${shares} shares for ${wave.runs.length} runs, so a share would apply nothing.`);
284
292
  }
293
+ if (wave.jobs !== undefined) {
294
+ const problem = opWaveGroupsProblem(wave, wave.runs.map((r) => r.target));
295
+ if (problem)
296
+ throw new Error(`${where}: ${problem}.`);
297
+ }
298
+ }
299
+ }
300
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
301
+ /** Refuse a `runner` no forge could read (#3914). */
302
+ function assertOpWaveRunner(where, field, runner) {
303
+ const labels = (value) => Array.isArray(value) && value.length > 0 && value.every((l) => typeof l === "string" && l.trim() !== "");
304
+ if (typeof runner === "string" && runner.trim() !== "")
305
+ return;
306
+ if (labels(runner))
307
+ return;
308
+ if (runner !== null && typeof runner === "object" && !Array.isArray(runner)) {
309
+ const { group, labels: l, ...rest } = runner;
310
+ const extra = Object.keys(rest)[0];
311
+ if (extra !== undefined)
312
+ throw new Error(`${where}: ${field}.${extra} is not a setting; a runner group is { "group": "<name>", "labels"?: [...] }.`);
313
+ if (typeof group === "string" && group.trim() !== "" && (l === undefined || labels(l)))
314
+ return;
315
+ }
316
+ throw new Error(`${where}: ${field} must be a runner label, a non-empty list of labels, or { "group": "<name>", "labels"?: [...] } (GitHub only).`);
317
+ }
318
+ /** Refuse `idTokens` that are not `{ <variable>: <audience> }` (#3914). */
319
+ function assertOpWaveIdTokens(where, field, idTokens) {
320
+ if (idTokens === null || typeof idTokens !== "object" || Array.isArray(idTokens) || Object.keys(idTokens).length === 0) {
321
+ throw new Error(`${where}: ${field} must be { "<variable>": "<audience>" }, with at least one token.`);
285
322
  }
323
+ for (const [name, aud] of Object.entries(idTokens)) {
324
+ if (!ENV_NAME.test(name))
325
+ throw new Error(`${where}: ${field} names "${name}", which is not a variable name (letters, digits and "_", not starting with a digit).`);
326
+ if (typeof aud !== "string" || aud.trim() === "")
327
+ throw new Error(`${where}: ${field}.${name} must be an audience, a non-empty string.`);
328
+ }
329
+ }
330
+ /** Refuse a wave's `jobs` no pipeline could render (#3914). Which runs each group holds is {@link opWaveGroupsProblem}'s. */
331
+ function assertOpWaveJobGroups(where, wave) {
332
+ const at = `wave "${wave.name}"`;
333
+ if (!Array.isArray(wave.jobs) || wave.jobs.length === 0)
334
+ throw new Error(`${where}: ${at} jobs must be a non-empty list of job groups.`);
335
+ if ((wave.shares ?? 1) > 1)
336
+ throw new Error(`${where}: ${at} has jobs and shares; split each job group with its own shares.`);
337
+ if (wave.rest)
338
+ throw new Error(`${where}: ${at} has jobs and rest; the waves after it run in its one job, so its apply cannot be split into groups.`);
339
+ const names = new Set();
340
+ wave.jobs.forEach((group, i) => {
341
+ if (group === null || typeof group !== "object")
342
+ throw new Error(`${where}: ${at} jobs[${i}] must be { "name", "runs", ... }.`);
343
+ const field = `${at} jobs[${i}]${typeof group.name === "string" ? ` (${group.name})` : ""}`;
344
+ if (typeof group.name !== "string" || !NAME.test(group.name))
345
+ throw new Error(`${where}: ${field} must be named with lowercase letters, digits, "-" and "_".`);
346
+ if (group.name === "decide" || /^share-\d+$/.test(group.name))
347
+ throw new Error(`${where}: ${field} is named like the wave's own jobs (decide, share-<i>); name it another way.`);
348
+ if (names.has(group.name))
349
+ throw new Error(`${where}: ${at} names job group "${group.name}" twice.`);
350
+ names.add(group.name);
351
+ const known = new Set(["name", "runs", "shares", "runner", "environment", "variables", "setup", "idTokens"]);
352
+ const extra = Object.keys(group).find((key) => !known.has(key));
353
+ if (extra !== undefined)
354
+ throw new Error(`${where}: ${field}.${extra} is not a setting; a job group takes ${[...known].join(", ")}.`);
355
+ if (!Array.isArray(group.runs) || group.runs.length === 0 || !group.runs.every((r) => typeof r === "string" && r.trim() !== "")) {
356
+ throw new Error(`${where}: ${field} runs must be a non-empty list of targets or patterns.`);
357
+ }
358
+ const shares = group.shares ?? 1;
359
+ if (!Number.isInteger(shares) || shares < 1)
360
+ throw new Error(`${where}: ${field} has shares ${group.shares}; use a whole number from 1.`);
361
+ if (group.runner !== undefined)
362
+ assertOpWaveRunner(where, `${field} runner`, group.runner);
363
+ if (group.idTokens !== undefined)
364
+ assertOpWaveIdTokens(where, `${field} idTokens`, group.idTokens);
365
+ if (group.variables !== undefined && (group.variables === null || typeof group.variables !== "object" || Array.isArray(group.variables) || !Object.values(group.variables).every((v) => typeof v === "string"))) {
366
+ throw new Error(`${where}: ${field} variables must be { "<name>": "<value>" }.`);
367
+ }
368
+ if (group.setup !== undefined && !Array.isArray(group.setup))
369
+ throw new Error(`${where}: ${field} setup must be a list of steps.`);
370
+ if (group.environment !== undefined && (group.environment === null || typeof group.environment !== "object" || typeof group.environment.name !== "string")) {
371
+ throw new Error(`${where}: ${field} environment must be { "name", "url"? }.`);
372
+ }
373
+ });
374
+ }
375
+ /**
376
+ * True when `target` matches a job group's `runs` entry (#3914): the target
377
+ * itself, or a pattern where `*` matches within one `/` segment and `**`
378
+ * across segments.
379
+ */
380
+ export function opWaveRunMatches(pattern, target) {
381
+ if (!pattern.includes("*"))
382
+ return pattern === target;
383
+ let re = "";
384
+ for (let i = 0; i < pattern.length; i++) {
385
+ const c = pattern[i];
386
+ if (c === "*" && pattern[i + 1] === "*") {
387
+ re += ".*";
388
+ i++;
389
+ }
390
+ else if (c === "*")
391
+ re += "[^/]*";
392
+ else
393
+ re += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
394
+ }
395
+ return new RegExp(`^${re}$`).test(target);
396
+ }
397
+ /** The runs of `runs` that job group `group` holds (#3914). */
398
+ export function opWaveGroupRuns(group, runs) {
399
+ return runs.filter((run) => group.runs.some((pattern) => opWaveRunMatches(pattern, run.target)));
400
+ }
401
+ /**
402
+ * What is wrong with how a wave's job groups hold `targets` (#3914): a run in
403
+ * two groups, or in none. Undefined when each run is in exactly one.
404
+ */
405
+ export function opWaveGroupsProblem(wave, targets) {
406
+ for (const target of targets) {
407
+ const holders = (wave.jobs ?? []).filter((group) => group.runs.some((pattern) => opWaveRunMatches(pattern, target))).map((g) => `"${g.name}"`);
408
+ if (holders.length > 1)
409
+ return `wave "${wave.name}" jobs: run "${target}" is in job groups ${holders.join(" and ")}; each run is in exactly one`;
410
+ if (holders.length === 0)
411
+ return `wave "${wave.name}" jobs: run "${target}" is in no job group; each run is in exactly one`;
412
+ }
413
+ return undefined;
286
414
  }
287
415
  function assertArgv(where, field, argv) {
288
416
  if (!Array.isArray(argv) || argv.length === 0 || !argv.every((part) => typeof part === "string") || argv[0] === "") {
@@ -381,7 +509,7 @@ export function opWaveJobs(spec, specFile) {
381
509
  const command = ["chant", "run", "wave", "--spec", specFile, "--wave", String(k), ...(spec.base ? ["--base", spec.base] : [])];
382
510
  const prefix = `wave-${k}-${wave.name}`;
383
511
  const shares = wave.shares ?? 1;
384
- if (shares === 1) {
512
+ if (shares === 1 && wave.jobs === undefined) {
385
513
  jobs.push({ jobName: prefix, wave: k, waveName: wave.name, kind: "wave", needs: previous, command });
386
514
  previous = [prefix];
387
515
  return;
@@ -390,6 +518,26 @@ export function opWaveJobs(spec, specFile) {
390
518
  const decide = `${prefix}-decide`;
391
519
  jobs.push({ jobName: decide, wave: k, waveName: wave.name, kind: "decide", needs: previous, command: [...command, "--decide"], record });
392
520
  const shareJobs = [];
521
+ if (wave.jobs !== undefined) {
522
+ // #3914: a job per group, or per share of a group, each applying its runs under the deciding job's decision.
523
+ for (const group of wave.jobs) {
524
+ const groupShares = group.shares ?? 1;
525
+ const groupCommand = [...command, "--group", group.name];
526
+ if (groupShares === 1) {
527
+ const jobName = `${prefix}-${group.name}`;
528
+ shareJobs.push(jobName);
529
+ jobs.push({ jobName, wave: k, waveName: wave.name, kind: "share", group: group.name, needs: [decide], command: groupCommand, record });
530
+ continue;
531
+ }
532
+ for (let share = 1; share <= groupShares; share++) {
533
+ const jobName = `${prefix}-${group.name}-share-${share}`;
534
+ shareJobs.push(jobName);
535
+ jobs.push({ jobName, wave: k, waveName: wave.name, kind: "share", group: group.name, share, needs: [decide], command: [...groupCommand, "--share", String(share)], record });
536
+ }
537
+ }
538
+ previous = shareJobs;
539
+ return;
540
+ }
393
541
  for (let share = 1; share <= shares; share++) {
394
542
  const jobName = `${prefix}-share-${share}`;
395
543
  shareJobs.push(jobName);
@@ -399,6 +547,10 @@ export function opWaveJobs(spec, specFile) {
399
547
  });
400
548
  return jobs;
401
549
  }
550
+ /** The job group a job applies (#3914), or undefined for a wave's own jobs. */
551
+ export function opWaveGroupOf(spec, job) {
552
+ return job.group === undefined ? undefined : spec.waves[job.wave - 1]?.jobs?.find((g) => g.name === job.group);
553
+ }
402
554
  /** The wave a job belongs to. */
403
555
  export function opWaveOf(spec, job) {
404
556
  return spec.waves[job.wave - 1];
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../../src/op/runtimes/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAUH,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAC;AA0CpB;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,iBAAiB,CAsJ3F"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../../src/op/runtimes/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAWH,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAC;AA0CpB;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,iBAAiB,CA2J3F"}
@@ -21,6 +21,7 @@ import { lexiconNames } from "../../lexicon-module.js";
21
21
  import { loadChantConfig } from "../../config.js";
22
22
  import { loadActivities, loadProfiles } from "../activity-registry.js";
23
23
  import { runOpLocally, OpRunFailure } from "../local-executor.js";
24
+ import { stepHeartbeatMs } from "../step-console.js";
24
25
  import { runComponents } from "../../components/cli-support.js";
25
26
  import { discoverOps } from "../discover.js";
26
27
  import { readRunLedger, runEnvOf, DEFAULT_RUN_ENV } from "../../lifecycle/run-ledger.js";
@@ -142,6 +143,11 @@ export function createLocalOpRuntime(opts = {}) {
142
143
  onLedgerError: (err) => process.stderr.write(`warning: run "${runId}" of "${op.name}" finished, but its record could not be ` +
143
144
  `appended to the run ledger: ${err instanceof Error ? err.message : String(err)}\n`),
144
145
  ...(startOpts.progress ? { onRecord: startOpts.progress } : {}),
146
+ // #3956: a running step's output, and a still-running line while
147
+ // it is quiet, on stderr as they happen; stdout stays the
148
+ // `--json` record's and `--progress-json`'s.
149
+ stepOutput: (line) => process.stderr.write(`${line}\n`),
150
+ stepHeartbeatMs: stepHeartbeatMs(process.env, (message) => process.stderr.write(`warning: ${message}\n`)),
145
151
  ...(startOpts.work ? { work: startOpts.work } : {}),
146
152
  onWorkLeaseWarning: (message) => process.stderr.write(`warning: "${op.name}" could not renew its work lease, retrying at the next beat: ${message}\n`),
147
153
  });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * A running step's console (#3956): its output as it arrives, and a
3
+ * "still running" line while it is quiet.
4
+ *
5
+ * The run's own render (`./local-output.ts`) is printed once the run
6
+ * settles, so before this a 15-minute step printed nothing for 15 minutes and
7
+ * a watchdog reading the output killed a healthy run. When the caller hands
8
+ * the executor a writer (`RunOpOptions.stepOutput`, which the local runtime
9
+ * passes, writing to stderr), each activity step runs with a console:
10
+ *
11
+ * - an activity that runs a process (`shellCmd`) passes each line the process
12
+ * writes, stdout and stderr alike, through {@link stepOutputLine}, and it is
13
+ * printed at once as `[<phase>] <step> | <line>`;
14
+ * - a step that prints nothing for `heartbeatMs` (an activity that only
15
+ * returns a value, or a command that is quiet) gets
16
+ * `[<phase>] <step>: still running after <n>s` each time that much passes.
17
+ *
18
+ * `<step>` is the step's id when it has one, else its call as the run's
19
+ * render names it, `fn(arg=value, ...)`, cut to a readable length. Without a
20
+ * writer there is no console, and `shellCmd` prints a command's stderr when it
21
+ * ends, as it did before.
22
+ */
23
+ /** How long a step may be quiet before the runner says it is still running. */
24
+ export declare const DEFAULT_STEP_HEARTBEAT_MS = 60000;
25
+ /** The environment variable that sets the interval: a duration ("30s", "5m"), or `0` / `off`. */
26
+ export declare const STEP_HEARTBEAT_ENV = "CHANT_STEP_HEARTBEAT";
27
+ /** Where a run's step lines go, and how often a quiet step says it is still running. */
28
+ export interface RunConsole {
29
+ write: (line: string) => void;
30
+ heartbeatMs?: number;
31
+ }
32
+ /** Run `fn` as a run whose steps print through `run`. Without one, steps get no console. */
33
+ export declare function withRunConsole<T>(run: RunConsole | undefined, fn: () => Promise<T>): Promise<T>;
34
+ /** The name a step's lines carry: its id, else `fn(args)` on one line, cut to {@link MAX_LABEL}. */
35
+ export declare function stepLabel(fn: string, args: Record<string, unknown>, id?: string): string;
36
+ /**
37
+ * Run `fn` with a console for one step, when the run has one
38
+ * ({@link withRunConsole}). The heartbeat timer is stopped when `fn`
39
+ * settles; it never holds the process open on its own.
40
+ */
41
+ export declare function withStepConsole<T>(opts: {
42
+ phase: string;
43
+ label: string;
44
+ }, fn: () => Promise<T>): Promise<T>;
45
+ /** Whether the calling activity runs under a step console, so its output is printed as it arrives. */
46
+ export declare function hasStepConsole(): boolean;
47
+ /** Print one line of the calling step's output. A no-op outside a step console. */
48
+ export declare function stepOutputLine(text: string): void;
49
+ /**
50
+ * A sink for a stream's chunks that prints whole lines to the calling step's
51
+ * console as they complete; `end()` prints what is left. The console is the
52
+ * one current when the splitter is made, since a stream's `data` events need
53
+ * not run in the step's async context.
54
+ */
55
+ export declare function stepLineSplitter(): {
56
+ push(chunk: string | Buffer): void;
57
+ end(): void;
58
+ };
59
+ /**
60
+ * The heartbeat interval `CHANT_STEP_HEARTBEAT` sets, or the default. `0` and
61
+ * `off` turn it off. A value that is not a duration falls back to the default
62
+ * and says so through `warn`.
63
+ */
64
+ export declare function stepHeartbeatMs(env?: NodeJS.ProcessEnv, warn?: (message: string) => void): number;
65
+ //# sourceMappingURL=step-console.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-console.d.ts","sourceRoot":"","sources":["../../src/op/step-console.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD,iGAAiG;AACjG,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AASzD,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAKD,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAE/F;AAQD,oGAAoG;AACpG,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIxF;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6BjH;AAED,sGAAsG;AACtG,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI;IAAE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,IAAI,IAAI,CAAA;CAAE,CAmBtF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,CAU9G"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * A running step's console (#3956): its output as it arrives, and a
3
+ * "still running" line while it is quiet.
4
+ *
5
+ * The run's own render (`./local-output.ts`) is printed once the run
6
+ * settles, so before this a 15-minute step printed nothing for 15 minutes and
7
+ * a watchdog reading the output killed a healthy run. When the caller hands
8
+ * the executor a writer (`RunOpOptions.stepOutput`, which the local runtime
9
+ * passes, writing to stderr), each activity step runs with a console:
10
+ *
11
+ * - an activity that runs a process (`shellCmd`) passes each line the process
12
+ * writes, stdout and stderr alike, through {@link stepOutputLine}, and it is
13
+ * printed at once as `[<phase>] <step> | <line>`;
14
+ * - a step that prints nothing for `heartbeatMs` (an activity that only
15
+ * returns a value, or a command that is quiet) gets
16
+ * `[<phase>] <step>: still running after <n>s` each time that much passes.
17
+ *
18
+ * `<step>` is the step's id when it has one, else its call as the run's
19
+ * render names it, `fn(arg=value, ...)`, cut to a readable length. Without a
20
+ * writer there is no console, and `shellCmd` prints a command's stderr when it
21
+ * ends, as it did before.
22
+ */
23
+ import { AsyncLocalStorage } from "node:async_hooks";
24
+ import { parseDuration } from "./duration.js";
25
+ /** How long a step may be quiet before the runner says it is still running. */
26
+ export const DEFAULT_STEP_HEARTBEAT_MS = 60000;
27
+ /** The environment variable that sets the interval: a duration ("30s", "5m"), or `0` / `off`. */
28
+ export const STEP_HEARTBEAT_ENV = "CHANT_STEP_HEARTBEAT";
29
+ /** The longest step label a line carries before it is cut. */
30
+ const MAX_LABEL = 60;
31
+ const store = new AsyncLocalStorage();
32
+ const runStore = new AsyncLocalStorage();
33
+ /** Run `fn` as a run whose steps print through `run`. Without one, steps get no console. */
34
+ export function withRunConsole(run, fn) {
35
+ return run ? runStore.run(run, fn) : fn();
36
+ }
37
+ function formatArgs(args) {
38
+ return Object.entries(args)
39
+ .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
40
+ .join(", ");
41
+ }
42
+ /** The name a step's lines carry: its id, else `fn(args)` on one line, cut to {@link MAX_LABEL}. */
43
+ export function stepLabel(fn, args, id) {
44
+ if (id)
45
+ return id;
46
+ const call = `${fn}(${formatArgs(args)})`.replace(/\s+/g, " ");
47
+ return call.length > MAX_LABEL ? `${call.slice(0, MAX_LABEL - 4)}...)` : call;
48
+ }
49
+ /**
50
+ * Run `fn` with a console for one step, when the run has one
51
+ * ({@link withRunConsole}). The heartbeat timer is stopped when `fn`
52
+ * settles; it never holds the process open on its own.
53
+ */
54
+ export async function withStepConsole(opts, fn) {
55
+ const run = runStore.getStore();
56
+ if (!run)
57
+ return fn();
58
+ const { write } = run;
59
+ const prefix = `[${opts.phase}] ${opts.label}`;
60
+ const started = Date.now();
61
+ let spoke = false;
62
+ const sink = {
63
+ line(text) {
64
+ spoke = true;
65
+ write(`${prefix} | ${text}`);
66
+ },
67
+ };
68
+ const every = run.heartbeatMs ?? DEFAULT_STEP_HEARTBEAT_MS;
69
+ const timer = every > 0
70
+ ? setInterval(() => {
71
+ if (spoke) {
72
+ spoke = false;
73
+ return;
74
+ }
75
+ write(`${prefix}: still running after ${Math.round((Date.now() - started) / 1000)}s`);
76
+ }, every)
77
+ : undefined;
78
+ timer?.unref();
79
+ try {
80
+ return await store.run(sink, fn);
81
+ }
82
+ finally {
83
+ if (timer)
84
+ clearInterval(timer);
85
+ }
86
+ }
87
+ /** Whether the calling activity runs under a step console, so its output is printed as it arrives. */
88
+ export function hasStepConsole() {
89
+ return store.getStore() !== undefined;
90
+ }
91
+ /** Print one line of the calling step's output. A no-op outside a step console. */
92
+ export function stepOutputLine(text) {
93
+ store.getStore()?.line(text);
94
+ }
95
+ /**
96
+ * A sink for a stream's chunks that prints whole lines to the calling step's
97
+ * console as they complete; `end()` prints what is left. The console is the
98
+ * one current when the splitter is made, since a stream's `data` events need
99
+ * not run in the step's async context.
100
+ */
101
+ export function stepLineSplitter() {
102
+ const sink = store.getStore();
103
+ let pending = "";
104
+ const emit = (line) => sink?.line(line.endsWith("\r") ? line.slice(0, -1) : line);
105
+ return {
106
+ push(chunk) {
107
+ pending += typeof chunk === "string" ? chunk : chunk.toString("utf8");
108
+ let at = pending.indexOf("\n");
109
+ while (at >= 0) {
110
+ emit(pending.slice(0, at));
111
+ pending = pending.slice(at + 1);
112
+ at = pending.indexOf("\n");
113
+ }
114
+ },
115
+ end() {
116
+ if (pending !== "")
117
+ emit(pending);
118
+ pending = "";
119
+ },
120
+ };
121
+ }
122
+ /**
123
+ * The heartbeat interval `CHANT_STEP_HEARTBEAT` sets, or the default. `0` and
124
+ * `off` turn it off. A value that is not a duration falls back to the default
125
+ * and says so through `warn`.
126
+ */
127
+ export function stepHeartbeatMs(env = process.env, warn) {
128
+ const raw = env[STEP_HEARTBEAT_ENV]?.trim();
129
+ if (!raw)
130
+ return DEFAULT_STEP_HEARTBEAT_MS;
131
+ if (raw === "0" || raw.toLowerCase() === "off")
132
+ return 0;
133
+ try {
134
+ return parseDuration(raw);
135
+ }
136
+ catch {
137
+ warn?.(`${STEP_HEARTBEAT_ENV}="${raw}" is not a duration (such as 30s or 5m) or off; using ${DEFAULT_STEP_HEARTBEAT_MS / 1000}s`);
138
+ return DEFAULT_STEP_HEARTBEAT_MS;
139
+ }
140
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"point-decision.d.ts","sourceRoot":"","sources":["../src/point-decision.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"point-decision.d.ts","sourceRoot":"","sources":["../src/point-decision.ts"],"names":[],"mappings":"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"}
@@ -33,6 +33,7 @@ import { samePlanDigest } from "./lifecycle/plan-digest.js";
33
33
  import { parseDuration } from "./op/duration.js";
34
34
  import { DEFAULT_GATE_POINT_PASS, defaultGatePointStore, gatePointInputs, gateSubject } from "./op/gate-point.js";
35
35
  import { resolveGateRunLocator } from "./op/gate-run-locator.js";
36
+ import { refuseGateWithoutRecording } from "./lifecycle/no-record.js";
36
37
  export { DEFAULT_GATE_POINT_PASS, GATE_POINT_INPUTS, defaultGatePointStore, gatePointInputs, gatePointOf, gatePointProblems, gateSubject, parseGateSubject, } from "./op/gate-point.js";
37
38
  export { isGateRunLocator, resolveGateRunLocator } from "./op/gate-run-locator.js";
38
39
  /** Who a resolution written for an answer is by: the people who answered, or the decider that did. */
@@ -41,6 +42,8 @@ function resolvedByOf(q) {
41
42
  }
42
43
  /** Decide a gate that asks a point: ask it, then record the pending fact or the resolution that cites the answer. */
43
44
  export async function evaluatePointGate(port, asker, input) {
45
+ // #3955: refused before the point is asked, which would itself write to the ledger.
46
+ refuseGateWithoutRecording(input.op, input.gate);
44
47
  const now = input.now ?? new Date().toISOString();
45
48
  const facts = { component: input.op, name: input.gate, env: input.env ?? null, planDigest: input.planDigest ?? null };
46
49
  const q = await asker.ask({ point: input.point.name, inputs: input.inputs ?? {}, facts, subject: gateSubject(input.op, input.gate, input.env), ...(input.point.store ? { store: input.point.store } : {}) });