@intentius/chant 0.53.0 → 0.53.1

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.
@@ -22,20 +22,53 @@
22
22
  import type { OpConfig } from "../../op/types.js";
23
23
  import type { StepRecord } from "../../op/local-executor.js";
24
24
  import type { WorkflowHistoryRaw, WorkflowHandleRaw } from "./run-client.js";
25
+ /**
26
+ * A step record paired with the position of the declared step it came from.
27
+ *
28
+ * `index` is an offset into `declaredStepOrder(config)`, which depends only
29
+ * on the Op config — not on the history — so it is the record's identity
30
+ * and is stable across polls. The record *list* is not: a mid-run poll omits
31
+ * every step that hasn't settled yet, and the terminal `final: true` pass
32
+ * inserts a `"skipped"` record at each such step's declared position, which
33
+ * shifts every later record's list offset (chant #2032). Anything tracking
34
+ * what it has already seen must key on `index`, never on how many records
35
+ * the previous call returned.
36
+ */
37
+ export interface IndexedStepRecord {
38
+ index: number;
39
+ record: StepRecord;
40
+ }
25
41
  /**
26
42
  * Reconstruct per-step progress records from a workflow's history, in
27
- * declared order. Only settled steps (matched to a scheduled event that has
28
- * since completed or failed) produce a record — a step that hasn't been
29
- * scheduled yet simply isn't in the result, so a live poll only ever
30
- * appends. Pass `final: true` once the workflow has reached a terminal
43
+ * declared order, each tagged with its declared-step `index`. Only settled
44
+ * steps (matched to a scheduled event that has since completed or failed)
45
+ * produce a record — a step that hasn't been scheduled yet simply isn't in
46
+ * the result. Pass `final: true` once the workflow has reached a terminal
31
47
  * status to also emit a `"skipped"` record for every declared step that
32
48
  * never got a matching scheduled event (an untaken effect branch, or an
33
49
  * `onFailure` phase on a run that succeeded) — never before, or a
34
50
  * still-running Op would show its not-yet-reached steps as skipped.
35
51
  */
52
+ export declare function extractIndexedStepRecords(config: OpConfig, history: WorkflowHistoryRaw, opts?: {
53
+ final?: boolean;
54
+ }): IndexedStepRecord[];
55
+ /** `extractIndexedStepRecords` without the indices — the plain declared-order record list. */
36
56
  export declare function extractStepRecords(config: OpConfig, history: WorkflowHistoryRaw, opts?: {
37
57
  final?: boolean;
38
58
  }): StepRecord[];
59
+ /**
60
+ * A sink that forwards each declared step's record exactly once, however many
61
+ * times the poll loop re-derives the list.
62
+ *
63
+ * `--progress-json` calls this per poll with the whole list so far, plus once
64
+ * more with the terminal `final: true` list. Deduplicating by declared-step
65
+ * `index` is what makes that safe: an emitted-count watermark assumed the list
66
+ * was append-only, and the `final` pass's inserted `"skipped"` records break
67
+ * that assumption — with an unreached phase declared before an `onFailure`
68
+ * phase that ran, the insertion shifted the tail, so the last record shipped
69
+ * twice and the skipped one never shipped at all (chant #2032).
70
+ */
71
+ export declare function stepRecordEmitter(emit: (record: StepRecord) => void): (records: IndexedStepRecord[]) => void;
39
72
  /** `{completed, scheduled}` activity counts for the summary line `chant run status`/`op-status` show alongside the full record list. */
40
73
  export declare function countActivities(history: WorkflowHistoryRaw): {
41
74
  completed: number;
@@ -1 +1 @@
1
- {"version":3,"file":"op-progress.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/op-progress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAgB,iBAAiB,EAAE,MAAM,cAAc,CAAC;AA2FxF;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,UAAU,EAAE,CA+Bd;AAED,wIAAwI;AACxI,wBAAgB,eAAe,CAAC,OAAO,EAAE,kBAAkB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAMrG;AAED,0JAA0J;AAC1J,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC,CAM3G"}
1
+ {"version":3,"file":"op-progress.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/op-progress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAgB,iBAAiB,EAAE,MAAM,cAAc,CAAC;AA2FxF;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,iBAAiB,EAAE,CAsCrB;AAED,8FAA8F;AAC9F,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,UAAU,EAAE,CAEd;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,iBAAiB,EAAE,KAAK,IAAI,CAS5G;AAED,wIAAwI;AACxI,wBAAgB,eAAe,CAAC,OAAO,EAAE,kBAAkB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAMrG;AAED,0JAA0J;AAC1J,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC,CAM3G"}
@@ -1,3 +1,5 @@
1
+ import { type ConvergeTickRecord } from "../../lifecycle/converge-ledger.js";
2
+ import type { GateResolutionRecord } from "../../lifecycle/gate-ledger.js";
1
3
  import type { CommandContext } from "../registry.js";
2
4
  /**
3
5
  * `chant operator [--env <env>] [--interval <duration>] [--lease-ttl
@@ -17,8 +19,66 @@ export declare function runOperator(ctx: CommandContext): Promise<number>;
17
19
  * operator) ran the last tick.
18
20
  */
19
21
  export declare function runOperatorStatus(ctx: CommandContext): Promise<number>;
22
+ /** One entry of the merged tick/gate timeline `chant operator log` prints. */
23
+ export type OperatorLogEntry = {
24
+ kind: "tick";
25
+ timestamp: string;
26
+ record: ConvergeTickRecord;
27
+ } | {
28
+ kind: "gate-resolution";
29
+ timestamp: string;
30
+ record: GateResolutionRecord;
31
+ };
32
+ export interface OperatorLogResult {
33
+ entries: OperatorLogEntry[];
34
+ /**
35
+ * Unreadable lines behind this answer, per ledger. `readConvergeLedger` and
36
+ * `readGateResolutions` both skip a malformed line and count it rather than
37
+ * throwing, so a corrupted ledger renders a shorter timeline; without this a
38
+ * consumer could not tell that from a genuinely quiet environment.
39
+ */
40
+ malformed: {
41
+ converge: number;
42
+ gates: number;
43
+ };
44
+ }
20
45
  /**
21
- * `chant approve <op> <gate> [--actor <name>] [--note <text>]` — record the
46
+ * Gather the converge/gate history a set of ConvergeOps has recorded, merged
47
+ * into one timestamp-ordered timeline, oldest first.
48
+ *
49
+ * `since` (inclusive) and `limit` are applied after the merge — `limit` keeps
50
+ * the *newest* n entries, since that is what a log is asked for, and then
51
+ * prints them oldest-first like every other log.
52
+ */
53
+ export declare function collectOperatorLog(ops: {
54
+ name: string;
55
+ env: string;
56
+ }[], opts?: {
57
+ op?: string;
58
+ since?: string;
59
+ limit?: number;
60
+ cwd?: string;
61
+ }): Promise<OperatorLogResult>;
62
+ /**
63
+ * `chant operator log [--env <env>] [--op <name>] [--since <iso>] [--limit
64
+ * <n>] [--json]` (#2029) — the converge tick history and the gate resolutions
65
+ * against it, read from the `chant/lifecycle` orphan branch alone.
66
+ *
67
+ * `readConvergeLedger` has always returned every tick for an environment;
68
+ * until this its only caller was `operator status`, which threw the history
69
+ * away and kept `.at(-1)`. A consumer wanting more than the newest row had to
70
+ * `git show chant/lifecycle:<env>/converge.jsonl` and parse it — which pins it
71
+ * to the orphan branch's name, the `<env>/converge.jsonl` path convention, the
72
+ * `_gates/<op>.jsonl` convention and the on-disk encoding, none of which are
73
+ * promised contracts. This is the read surface that makes those private again.
74
+ *
75
+ * Read-only: no lease, no dispatch, no daemon. Same `--json` discipline as
76
+ * `operator status` — one JSON document on stdout, diagnostics on stderr.
77
+ */
78
+ export declare function runOperatorLog(ctx: CommandContext): Promise<number>;
79
+ /**
80
+ * `chant approve <op> <gate> [--actor <name>] [--note <text>] [--url <url>]`
81
+ * — record the
22
82
  * out-of-band resolution fact for a gate a converge tick recorded as gated
23
83
  * (issue: "resolution is an out-of-band act that writes the counterpart
24
84
  * fact"). Per the issue's own leaning on open question 3 ("local trust in
@@ -1 +1 @@
1
- {"version":3,"file":"operator.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/operator.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAelD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA0DtE;AAuCD;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAqC5E;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCrE"}
1
+ {"version":3,"file":"operator.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/operator.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAC9F,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAIxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAelD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA0DtE;AA+CD;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsC5E;AAID,8EAA8E;AAC9E,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAEjF,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,EACpC,IAAI,GAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GACvE,OAAO,CAAC,iBAAiB,CAAC,CA4C5B;AAwBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgDzE;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgDrE"}
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAA+C,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAQ7F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA8ClD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM;;;;GAO5F;AA8BD,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAoFD,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CtE;AAwCD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CnE;AA2DD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCtE;AAID,2LAA2L;AAC3L,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtE;AAgFD,wBAAsB,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBhE;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuF1E;AA+OD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAoFrE;AA+MD,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAMjE"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAA+C,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAQ7F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA8ClD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM;;;;GAO5F;AA8BD,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAoFD,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CtE;AAwCD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CnE;AA2DD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCtE;AAID,2LAA2L;AAC3L,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtE;AAgFD,wBAAsB,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBhE;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuF1E;AA+OD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAoFrE;AA6MD,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAMjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAsF9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAmUpD;AAwnBD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAsF9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA6UpD;AAmoBD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
@@ -283,8 +283,16 @@ export interface ParsedArgs {
283
283
  leaseTtl?: string;
284
284
  /** `chant operator --once` (#1485) — run a single round and exit, instead of looping until Ctrl-C. Also the offline test/cron-invoker story. */
285
285
  once?: boolean;
286
- /** `chant approve <op> <gate> --note <text>` (#1485) — optional free-text context recorded on the gate-resolution fact (e.g. a PR URL). */
286
+ /** `chant approve <op> <gate> --note <text>` (#1485) — optional free-text prose recorded on the gate-resolution fact. The PR link belongs in `--url` since #2028; this is for everything that isn't the link. */
287
287
  note?: string;
288
+ /** `chant operator log --op <name>` (#2029) — restrict the tick history to one ConvergeOp by name. Omitted, every discovered ConvergeOp's ticks are merged into one timeline. */
289
+ op?: string;
290
+ /** `chant operator log --since <iso>` (#2029) — only entries at or after this ISO-8601 instant. */
291
+ since?: string;
292
+ /** `chant operator log --limit <n>` (#2029) — keep only the newest n entries (still printed oldest-first). */
293
+ limit?: number;
294
+ /** `chant approve <op> <gate> --url <url>` (#2028) — the address this resolution happened at (the PR/MR that carried the change), recorded typed on the gate-resolution fact so a reader is not sniffing `--note` for something link-shaped. Defaults to the PR/MR the surrounding CI job is for, when there is one. Must be an absolute http/https URL. */
295
+ url?: string;
288
296
  }
289
297
  /**
290
298
  * Declarative command definition for the CLI registry.
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/cli/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/cli/registry.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import type { ComponentStatusRow } from "./status.js";
2
+ export declare function sanitizeLedgerText(raw: string, maxLen?: number): string;
1
3
  /** One rule's outcome within a tick. */
2
4
  export interface ConvergeRuleOutcome {
3
5
  ruleId: string;
@@ -15,13 +17,70 @@ export interface ConvergeRuleOutcome {
15
17
  op?: string;
16
18
  /** The gate's signal name, for `action: "gated"`. */
17
19
  gateName?: string;
20
+ /**
21
+ * Where this gate's approval happens (#2028), for `action: "gated"` — the
22
+ * PR carrying the change in a gate-as-PR flow, or whatever review surface
23
+ * the dispatching environment knows about.
24
+ *
25
+ * The pending fact is the one a human has to act on, and it used to carry
26
+ * no link at all: `chant operator status`'s pending row was
27
+ * `{rule, op, gate}` and the only affordance it could print was a shell
28
+ * command. Absent when there genuinely is no address — a local tick with no
29
+ * PR behind it — never a synthesized one. See
30
+ * `./gate-ledger.ts`'s `resolveApprovalUrl` for where it comes from, and
31
+ * `GateResolutionRecord.url` for the resolved counterpart.
32
+ */
33
+ url?: string;
18
34
  /** The report reason, for `action: "reported"` (including a flap-damped rule's forced report) — and the human-readable explanation for `action: "gated"`. */
19
35
  reason?: string;
20
36
  }
37
+ /**
38
+ * One component's verdict as the tick observed it (#2027) — the per-entity
39
+ * layer behind the tick's single aggregate `status`.
40
+ *
41
+ * A deliberate subset of `ComponentStatusRow`: the five verdict-bearing
42
+ * fields, joined on the same `component` key `chant components status --live
43
+ * --json` emits, and none of the heavy ones. `recorded`, `build` and
44
+ * `componentBom` are dropped on purpose — they are release/build-ledger
45
+ * content, already readable by that same key, and a tick record is one line
46
+ * of JSON appended every tick forever. `detail` goes through
47
+ * {@link sanitizeLedgerText} for the same reason.
48
+ */
49
+ export interface ConvergeComponentVerdict {
50
+ component: string;
51
+ reconciliation: ComponentStatusRow["reconciliation"];
52
+ /** Human-readable detail backing the verdict, capped to one line. */
53
+ detail: string;
54
+ /** "Observed live", when live evidence was gathered. Absent means "did not look" or "could not look" — see `unobserved`. */
55
+ live?: boolean;
56
+ /** Why live state could not be read for this component (#1089). Mutually exclusive with `live`; this is the row that tripped an aggregate `status: "unknown"`. */
57
+ unobserved?: {
58
+ reason: string;
59
+ detail?: string;
60
+ };
61
+ }
62
+ /**
63
+ * Project the tick's `ComponentStatusRow[]` down to what the ledger keeps.
64
+ * Pure; row order is preserved, so a consumer sees components in the same
65
+ * order the status join produced them.
66
+ */
67
+ export declare function componentVerdicts(rows: readonly ComponentStatusRow[]): ConvergeComponentVerdict[];
21
68
  /** One immutable converge-tick record. */
22
69
  export interface ConvergeTickRecord {
23
70
  /** Schema version, so an incompatible future shape is detected before being misread. */
24
71
  version: 1;
72
+ /**
73
+ * Stable tick id (#2027) — the one thing an outcome, a gate fact or a
74
+ * remediation can point at. Before this, a tick's identity was the
75
+ * `(op, env, timestamp)` triple, so two ticks landing in the same ISO
76
+ * second were indistinguishable and nothing could reference one.
77
+ *
78
+ * Minted by {@link appendConvergeRecord} with `randomUUID()` (the same
79
+ * mint `./lease.ts` uses for a lease token) when the caller doesn't supply
80
+ * one. Optional on the type because `version: 1` records written before
81
+ * #2027 have none — a reader must handle its absence, not assume it.
82
+ */
83
+ id?: string;
25
84
  /** The ConvergeOp's name (`OpConfig.name`). */
26
85
  op: string;
27
86
  env: string;
@@ -31,6 +90,18 @@ export interface ConvergeTickRecord {
31
90
  firedRuleIds: string[];
32
91
  /** Per-rule outcome, for every fired rule. */
33
92
  outcomes: ConvergeRuleOutcome[];
93
+ /**
94
+ * The per-component verdicts this tick observed (#2027), behind the
95
+ * aggregate counts in `summary`.
96
+ *
97
+ * The tick derives these to compute `summary.drifted` and the whole-tick
98
+ * `status`, and used to throw them away — so a reader could say "this
99
+ * environment drifted twice this hour" but could not colour the node that
100
+ * drifted, or name the single unobserved component whose `unknown` verdict
101
+ * refused remediation for everything else. Optional because `version: 1`
102
+ * records written before #2027 have none; absent is not "no components".
103
+ */
104
+ components?: ConvergeComponentVerdict[];
34
105
  /** Aggregate counts backing the tick's one log line. */
35
106
  summary: {
36
107
  drifted: number;
@@ -52,6 +123,13 @@ export type ConvergeTickRecordInput = Omit<ConvergeTickRecord, "version">;
52
123
  * `pushLifecycle` (./git.ts) afterward, same two-step shape every other
53
124
  * ledger write here uses.
54
125
  *
126
+ * Mints `record.id` (#2027) when the input doesn't carry one, so every tick
127
+ * written from here on is referenceable. Unlike `timestamp`, which stays
128
+ * caller-supplied because library code never calls `Date.now()` internally,
129
+ * an id has nothing for a caller to decide — the mint lives here so no
130
+ * writer can forget it. An explicit `input.id` still wins, which is what a
131
+ * test asserting an exact record uses.
132
+ *
55
133
  * Retries the whole read-modify-write cycle (#1485) on `RefCASConflictError`
56
134
  * — `writeBlobToPath`'s ref write is CAS-guarded, so a concurrent writer to
57
135
  * a *different* env's file on the same orphan branch (two operators ticking
@@ -71,7 +149,9 @@ export declare function appendConvergeRecord(input: ConvergeTickRecordInput, opt
71
149
  cwd?: string;
72
150
  }): Promise<{
73
151
  commit: string;
74
- record: ConvergeTickRecord;
152
+ record: ConvergeTickRecord & {
153
+ id: string;
154
+ };
75
155
  }>;
76
156
  /** Read every tick record for `environment`, oldest first. Malformed lines are skipped, not thrown on — a corrupted or hand-edited ledger degrades gracefully, the same stance `readReleaseLedger` takes. */
77
157
  export declare function readConvergeLedger(environment: string, opts?: {
@@ -1 +1 @@
1
- {"version":3,"file":"converge-ledger.d.ts","sourceRoot":"","sources":["../../src/lifecycle/converge-ledger.ts"],"names":[],"mappings":"AAmCA,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;OAQG;IACH,MAAM,EAAE,KAAK,GAAG,UAAU,GAAG,gBAAgB,GAAG,cAAc,GAAG,OAAO,CAAC;IACzE,gEAAgE;IAChE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6JAA6J;IAC7J,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,0CAA0C;AAC1C,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,OAAO,EAAE,CAAC,CAAC;IACX,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,6IAA6I;IAC7I,SAAS,EAAE,MAAM,CAAC;IAClB,oHAAoH;IACpH,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,8CAA8C;IAC9C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,wDAAwD;IACxD,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,uNAAuN;QACvN,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,iHAAiH;IACjH,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,uBAAuB,EAC9B,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAqBzD;AAED,6MAA6M;AAC7M,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CA0B/D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAO1F"}
1
+ {"version":3,"file":"converge-ledger.d.ts","sourceRoot":"","sources":["../../src/lifecycle/converge-ledger.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,34 @@
1
+ /**
2
+ * The address of the approval surface for a gate, resolved from the CI
3
+ * environment (#2028).
4
+ *
5
+ * #1485's argument for gate-as-fact was that approval gets an address:
6
+ * "gate-as-PR gives approval a URL, a review surface, and CODEOWNERS as the
7
+ * authorization model." What shipped recorded the gate and not the address,
8
+ * so a pending-approval card had nothing to link to and gate-as-PR stayed a
9
+ * convention. This is the narrow, honest half of that: when a tick (or a
10
+ * `chant approve`) runs inside the PR/MR job that carries the change, the
11
+ * loop genuinely knows where approval happens, and says so. Anywhere else it
12
+ * returns `undefined` and the field is simply absent — never a guess, never a
13
+ * synthesized link.
14
+ *
15
+ * The env-var fallback chain is the same one `--actor` and `--run-id` already
16
+ * use (`../cli/handlers/components.ts`): GitHub Actions first, then GitLab CI.
17
+ *
18
+ * - GitHub Actions on a `pull_request` event: `GITHUB_SERVER_URL` +
19
+ * `GITHUB_REPOSITORY` + the PR number, which `GITHUB_REF_NAME` carries as
20
+ * `<n>/merge`. A push-event run has no PR, so it resolves to nothing.
21
+ * - GitLab CI on a merge-request pipeline: `CI_MERGE_REQUEST_PROJECT_URL` +
22
+ * `CI_MERGE_REQUEST_IID`.
23
+ */
24
+ export declare function resolveApprovalUrl(env?: NodeJS.ProcessEnv): string | undefined;
25
+ /**
26
+ * Whether `raw` is an address worth recording as one: an absolute `http`/
27
+ * `https` URL. A gate's address is a link a reader is expected to follow, so
28
+ * a relative path or a `file:`/`javascript:` scheme is refused at the CLI
29
+ * boundary rather than written into an immutable record.
30
+ */
31
+ export declare function isApprovalUrl(raw: string): boolean;
1
32
  /** One immutable gate-resolution record. */
2
33
  export interface GateResolutionRecord {
3
34
  /** Schema version, so an incompatible future shape is detected before being misread. */
@@ -10,8 +41,19 @@ export interface GateResolutionRecord {
10
41
  resolvedBy: string;
11
42
  /** ISO-8601 timestamp, caller-supplied (library code never calls `Date.now()` internally). */
12
43
  timestamp: string;
13
- /** Optional free-text context (e.g. a PR URL — "or a merged PR" is the issue's other resolution path; recording its link here keeps both paths visible from one ledger). */
44
+ /** Optional free-text context. Before #2028 this was also where a PR link went by convention; put the link in {@link GateResolutionRecord.url} instead and leave this for prose. */
14
45
  note?: string;
46
+ /**
47
+ * The address this resolution happened at (#2028) — the PR that carried the
48
+ * change, the review thread, whatever the approval surface was. Typed, so
49
+ * "resolved by this PR" is machine-readable instead of a reader sniffing
50
+ * `note` for something that looks like a link.
51
+ *
52
+ * `chant approve --url` sets it; absent when the resolver genuinely had no
53
+ * address (a human at a terminal, no PR). Always an absolute `http`/`https`
54
+ * URL — see {@link isApprovalUrl}.
55
+ */
56
+ url?: string;
15
57
  }
16
58
  export type GateResolutionInput = Omit<GateResolutionRecord, "version">;
17
59
  /** Append one immutable gate-resolution record. Does not push to the remote — call `pushLifecycle` (./git.ts) afterward, same two-step shape every other ledger write here uses. Retries on `RefCASConflictError` the same way `appendConvergeRecord` does (./converge-ledger.ts) — a concurrent writer to a different op's/env's file on the same orphan branch is the ordinary case, not an edge case. The baseline read must be `readPathSha` + `readBlobBySha` rather than `readBlobFromPath`, so the exact sha `existing` came from can be passed as `expectPriorPathSha` — see `writeBlobToPath` (./git.ts) for the race that closes. */
@@ -1 +1 @@
1
- {"version":3,"file":"gate-ledger.d.ts","sourceRoot":"","sources":["../../src/lifecycle/gate-ledger.ts"],"names":[],"mappings":"AA4CA,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACnC,wFAAwF;IACxF,OAAO,EAAE,CAAC,CAAC;IACX,6CAA6C;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,yHAAyH;IACzH,UAAU,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,SAAS,EAAE,MAAM,CAAC;IAClB,4KAA4K;IAC5K,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,CAAC;AAMxE,+mBAA+mB;AAC/mB,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,mBAAmB,EAC1B,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,CAAC,CAqB3D;AAED,oPAAoP;AACpP,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,MAAM,EACV,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CA0BjE;AAED,uOAAuO;AACvO,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,EAAE,EAC/B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,oBAAoB,GAAG,SAAS,CASlC"}
1
+ {"version":3,"file":"gate-ledger.d.ts","sourceRoot":"","sources":["../../src/lifecycle/gate-ledger.ts"],"names":[],"mappings":"AA4CA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,SAAS,CAY3F;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOlD;AAED,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACnC,wFAAwF;IACxF,OAAO,EAAE,CAAC,CAAC;IACX,6CAA6C;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,yHAAyH;IACzH,UAAU,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,SAAS,EAAE,MAAM,CAAC;IAClB,oLAAoL;IACpL,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,CAAC;AAMxE,+mBAA+mB;AAC/mB,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,mBAAmB,EAC1B,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,CAAC,CAqB3D;AAED,oPAAoP;AACpP,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,MAAM,EACV,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CA0BjE;AAED,uOAAuO;AACvO,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,EAAE,EAC/B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,oBAAoB,GAAG,SAAS,CASlC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.53.0",
3
+ "version": "0.53.1",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -1,5 +1,6 @@
1
1
  import { describe, test, expect } from "vitest";
2
- import { extractStepRecords, countActivities, queryGateState } from "./op-progress";
2
+ import { extractStepRecords, extractIndexedStepRecords, stepRecordEmitter, countActivities, queryGateState } from "./op-progress";
3
+ import type { StepRecord } from "../../op/local-executor";
3
4
  import type { OpConfig } from "../../op/types";
4
5
  import type { EffectReceiptRef } from "../../op/receipt-store";
5
6
  import type { WorkflowHistoryRaw, HistoryEvent } from "./run-client";
@@ -171,6 +172,71 @@ describe("extractStepRecords", () => {
171
172
  });
172
173
  });
173
174
 
175
+ describe("stepRecordEmitter", () => {
176
+ // chant #2032. Declared order is main phases then `onFailure` reversed, so an
177
+ // unreached main phase sits *before* an `onFailure` phase that ran. The live
178
+ // poll therefore returns [Main(fail), Comp(ok)] and the terminal `final: true`
179
+ // pass returns [Main(fail), Never(skipped), Comp(ok)] — an insertion, not an
180
+ // append. The old emitted-count watermark sliced at 2 and re-shipped Comp
181
+ // while dropping Never entirely. The #1676 coverage above misses this because
182
+ // in those configs the skipped record lands last, where insert and append are
183
+ // the same operation.
184
+ const config: OpConfig = {
185
+ name: "op", overview: "o",
186
+ phases: [
187
+ { name: "Main", steps: [{ kind: "activity", fn: "boom" }] },
188
+ { name: "Never", steps: [{ kind: "activity", fn: "never" }] },
189
+ ],
190
+ onFailure: [{ name: "Comp", steps: [{ kind: "activity", fn: "comp" }] }],
191
+ };
192
+ // Main fails, Never is never reached, compensation runs and succeeds.
193
+ const history: WorkflowHistoryRaw = {
194
+ events: [
195
+ scheduled("1", "boom", sec(0)),
196
+ failed("1", sec(1), "boom"),
197
+ scheduled("2", "comp", sec(2)),
198
+ completed("2", sec(3)),
199
+ ],
200
+ };
201
+
202
+ const MAIN: StepRecord = { phase: "Main", fn: "boom", status: "fail", durationMs: 1000, error: "boom" };
203
+ const NEVER: StepRecord = { phase: "Never", fn: "never", status: "skipped", durationMs: 0 };
204
+ const COMP: StepRecord = { phase: "Comp", fn: "comp", status: "ok", durationMs: 1000 };
205
+
206
+ test("the final pass inserts rather than appends, so list position is not an identity", () => {
207
+ expect(extractIndexedStepRecords(config, history)).toEqual([
208
+ { index: 0, record: MAIN },
209
+ { index: 2, record: COMP },
210
+ ]);
211
+ expect(extractIndexedStepRecords(config, history, { final: true })).toEqual([
212
+ { index: 0, record: MAIN },
213
+ { index: 1, record: NEVER },
214
+ { index: 2, record: COMP },
215
+ ]);
216
+ });
217
+
218
+ test("each record ships exactly once across the poll passes and the final pass", () => {
219
+ const emitted: StepRecord[] = [];
220
+ const emit = stepRecordEmitter((r) => emitted.push(r));
221
+
222
+ emit(extractIndexedStepRecords(config, history)); // a poll while the workflow closes
223
+ emit(extractIndexedStepRecords(config, history)); // an identical re-poll adds nothing
224
+ emit(extractIndexedStepRecords(config, history, { final: true }));
225
+
226
+ expect(emitted).toEqual([MAIN, COMP, NEVER]);
227
+ });
228
+
229
+ test("an empty first poll does not shift what a later pass ships", () => {
230
+ const emitted: StepRecord[] = [];
231
+ const emit = stepRecordEmitter((r) => emitted.push(r));
232
+
233
+ emit(extractIndexedStepRecords(config, { events: [] }));
234
+ expect(emitted).toEqual([]);
235
+ emit(extractIndexedStepRecords(config, history, { final: true }));
236
+ expect(emitted).toEqual([MAIN, NEVER, COMP]);
237
+ });
238
+ });
239
+
174
240
  describe("countActivities", () => {
175
241
  test("counts scheduled and completed events", () => {
176
242
  const history: WorkflowHistoryRaw = {
@@ -113,34 +113,55 @@ function terminalOutcomes(events: HistoryEvent[]): Map<string, TerminalOutcome>
113
113
  return out;
114
114
  }
115
115
 
116
+ /**
117
+ * A step record paired with the position of the declared step it came from.
118
+ *
119
+ * `index` is an offset into `declaredStepOrder(config)`, which depends only
120
+ * on the Op config — not on the history — so it is the record's identity
121
+ * and is stable across polls. The record *list* is not: a mid-run poll omits
122
+ * every step that hasn't settled yet, and the terminal `final: true` pass
123
+ * inserts a `"skipped"` record at each such step's declared position, which
124
+ * shifts every later record's list offset (chant #2032). Anything tracking
125
+ * what it has already seen must key on `index`, never on how many records
126
+ * the previous call returned.
127
+ */
128
+ export interface IndexedStepRecord {
129
+ index: number;
130
+ record: StepRecord;
131
+ }
132
+
116
133
  /**
117
134
  * Reconstruct per-step progress records from a workflow's history, in
118
- * declared order. Only settled steps (matched to a scheduled event that has
119
- * since completed or failed) produce a record — a step that hasn't been
120
- * scheduled yet simply isn't in the result, so a live poll only ever
121
- * appends. Pass `final: true` once the workflow has reached a terminal
135
+ * declared order, each tagged with its declared-step `index`. Only settled
136
+ * steps (matched to a scheduled event that has since completed or failed)
137
+ * produce a record — a step that hasn't been scheduled yet simply isn't in
138
+ * the result. Pass `final: true` once the workflow has reached a terminal
122
139
  * status to also emit a `"skipped"` record for every declared step that
123
140
  * never got a matching scheduled event (an untaken effect branch, or an
124
141
  * `onFailure` phase on a run that succeeded) — never before, or a
125
142
  * still-running Op would show its not-yet-reached steps as skipped.
126
143
  */
127
- export function extractStepRecords(
144
+ export function extractIndexedStepRecords(
128
145
  config: OpConfig,
129
146
  history: WorkflowHistoryRaw,
130
147
  opts: { final?: boolean } = {},
131
- ): StepRecord[] {
148
+ ): IndexedStepRecord[] {
132
149
  const events = history.events ?? [];
133
150
  const queues = scheduledQueues(events);
134
151
  const terminals = terminalOutcomes(events);
135
152
  const nextIndex = new Map<string, number>();
136
153
 
137
- const records: StepRecord[] = [];
138
- for (const step of declaredStepOrder(config)) {
154
+ const records: IndexedStepRecord[] = [];
155
+ const declared = declaredStepOrder(config);
156
+ for (let index = 0; index < declared.length; index++) {
157
+ const step = declared[index];
139
158
  const queue = queues.get(step.fn);
140
159
  const idx = nextIndex.get(step.fn) ?? 0;
141
160
  const entry = queue?.[idx];
142
161
  if (!entry) {
143
- if (opts.final) records.push({ phase: step.phase, fn: step.fn, status: "skipped", durationMs: 0 });
162
+ if (opts.final) {
163
+ records.push({ index, record: { phase: step.phase, fn: step.fn, status: "skipped", durationMs: 0 } });
164
+ }
144
165
  continue;
145
166
  }
146
167
  nextIndex.set(step.fn, idx + 1);
@@ -151,16 +172,51 @@ export function extractStepRecords(
151
172
  const durationMs =
152
173
  entry.scheduledAt && terminal.at ? terminal.at.getTime() - entry.scheduledAt.getTime() : 0;
153
174
  records.push({
154
- phase: step.phase,
155
- fn: step.fn,
156
- status: terminal.status,
157
- durationMs,
158
- ...(terminal.error ? { error: terminal.error } : {}),
175
+ index,
176
+ record: {
177
+ phase: step.phase,
178
+ fn: step.fn,
179
+ status: terminal.status,
180
+ durationMs,
181
+ ...(terminal.error ? { error: terminal.error } : {}),
182
+ },
159
183
  });
160
184
  }
161
185
  return records;
162
186
  }
163
187
 
188
+ /** `extractIndexedStepRecords` without the indices — the plain declared-order record list. */
189
+ export function extractStepRecords(
190
+ config: OpConfig,
191
+ history: WorkflowHistoryRaw,
192
+ opts: { final?: boolean } = {},
193
+ ): StepRecord[] {
194
+ return extractIndexedStepRecords(config, history, opts).map((r) => r.record);
195
+ }
196
+
197
+ /**
198
+ * A sink that forwards each declared step's record exactly once, however many
199
+ * times the poll loop re-derives the list.
200
+ *
201
+ * `--progress-json` calls this per poll with the whole list so far, plus once
202
+ * more with the terminal `final: true` list. Deduplicating by declared-step
203
+ * `index` is what makes that safe: an emitted-count watermark assumed the list
204
+ * was append-only, and the `final` pass's inserted `"skipped"` records break
205
+ * that assumption — with an unreached phase declared before an `onFailure`
206
+ * phase that ran, the insertion shifted the tail, so the last record shipped
207
+ * twice and the skipped one never shipped at all (chant #2032).
208
+ */
209
+ export function stepRecordEmitter(emit: (record: StepRecord) => void): (records: IndexedStepRecord[]) => void {
210
+ const seen = new Set<number>();
211
+ return (records) => {
212
+ for (const { index, record } of records) {
213
+ if (seen.has(index)) continue;
214
+ seen.add(index);
215
+ emit(record);
216
+ }
217
+ };
218
+ }
219
+
164
220
  /** `{completed, scheduled}` activity counts for the summary line `chant run status`/`op-status` show alongside the full record list. */
165
221
  export function countActivities(history: WorkflowHistoryRaw): { completed: number; scheduled: number } {
166
222
  const events = history.events ?? [];