nx 23.0.0-beta.20 → 23.0.0-beta.21

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.
@@ -27,16 +27,8 @@ exports.GENERIC_VALIDATION_FILE_LIST_CAP = 50;
27
27
  function buildGenericValidationUserPrompt(ctx) {
28
28
  const lines = [
29
29
  `You are validating the output of an Nx migration's deterministic generator phase. The generator has already run; inspect what it produced, verify the workspace is in a consistent state for what this migration intended to accomplish, apply any minor in-scope fixes the generator should have produced cleanly, and report findings.`,
30
- ``,
31
- `<migration>`,
32
- `package: ${(0, shared_rendering_1.escapeXmlBody)(ctx.package)}`,
33
- `version: ${(0, shared_rendering_1.escapeXmlBody)(ctx.version)}`,
34
- `name: ${(0, shared_rendering_1.escapeXmlBody)(ctx.name)}`,
30
+ ...(0, shared_rendering_1.renderMigrationBlock)(ctx),
35
31
  ];
36
- if (ctx.description) {
37
- lines.push(...(0, shared_rendering_1.renderKeyMultilineValue)('description', (0, shared_rendering_1.escapeXmlBody)(ctx.description)));
38
- }
39
- lines.push(`</migration>`);
40
32
  const logs = (0, shared_rendering_1.escapeXmlBody)((0, shared_rendering_1.stripAnsi)(ctx.impl.logs ?? '').trim());
41
33
  lines.push(...(0, shared_rendering_1.renderGeneratorOutputBlock)(logs));
42
34
  if (!ctx.impl.hasDiffContext && ctx.impl.changes.length > 0) {
@@ -44,12 +36,12 @@ function buildGenericValidationUserPrompt(ctx) {
44
36
  }
45
37
  const agentContext = (0, shared_rendering_1.filterNonEmptyStrings)(ctx.impl.agentContext ?? []);
46
38
  if (agentContext.length > 0) {
47
- lines.push(``, `<advisory_context note="hints emitted by the generator; treat as supplementary context, not separate tasks">`, ...agentContext.map((entry) => (0, shared_rendering_1.renderListItem)((0, shared_rendering_1.escapeXmlBody)(entry))), `</advisory_context>`);
39
+ lines.push(...(0, shared_rendering_1.renderAdvisoryContext)('hints emitted by the generator; treat as supplementary context, not separate tasks', agentContext));
48
40
  }
49
41
  const firstStep = ctx.impl.hasDiffContext
50
42
  ? `1. Inspect this migration's changes. ${(0, shared_rendering_1.renderGitInspectInstruction)()} Resolve each affected path to its owning Nx project via \`nx show project <name>\` (or by reading the project's \`project.json\` / \`package.json\`) to discover which targets each project actually defines — do not assume \`typecheck\` / \`test\` / \`lint\` exist. If no typecheck-equivalent exists, \`build\` is an acceptable substitute.`
51
43
  : `1. Resolve each path in <files_changed> to its owning Nx project. Use \`nx show project <name>\` (or read the project's \`project.json\` / \`package.json\`) to discover which targets each project actually defines — do not assume \`typecheck\` / \`test\` / \`lint\` exist. If no typecheck-equivalent exists, \`build\` is an acceptable substitute.`;
52
- lines.push(``, `<validation_instructions>`, firstStep, `2. Pick the smallest relevant subset of available targets to verify the change. Prefer \`nx affected -t <target>\` (or \`nx run <project>:<target>\` for a single project). When many small projects are affected, you may use \`nx run-many -t <target> -p <project1>,<project2>\` with the project list derived from the changed files. Unscoped \`nx run-many\` (no \`-p\`) is forbidden.`, `3. If a verification surfaces an issue the migration should have produced cleanly (e.g. a missing import, a type annotation the generator's template missed), you may apply a minor in-scope fix. The boundary is "what this migration intended to accomplish" — do not refactor, do not modify functionality unrelated to the migration, do not extend the migration's scope, do not touch code the migration was not concerned with. If you are unsure whether a fix is in scope, report it in \`summary\` instead of applying.`, `4. Apply every fix you can within scope, then write your handoff. On \`status: "success"\`, summarize what you verified and any fixes you applied. On \`status: "failed"\`, enumerate the unresolved findings in \`summary\` so the user can address them; no commit will be created from a failed run, so the generator's changes and your partial fixes will sit uncommitted in the working tree for the user to review.`, `</validation_instructions>`, ``, `Once you finish, write your handoff JSON to:`, `<handoff_path>`, (0, shared_rendering_1.escapeXmlBody)(ctx.handoffFileAbsolutePath), `</handoff_path>`);
44
+ lines.push(``, `<validation_instructions>`, firstStep, `2. Pick the smallest relevant subset of available targets to verify the change. Prefer \`nx affected -t <target>\` (or \`nx run <project>:<target>\` for a single project). When many small projects are affected, you may use \`nx run-many -t <target> -p <project1>,<project2>\` with the project list derived from the changed files. Unscoped \`nx run-many\` (no \`-p\`) is forbidden.`, `3. If a verification surfaces an issue the migration should have produced cleanly (e.g. a missing import, a type annotation the generator's template missed), you may apply a minor in-scope fix. The boundary is "what this migration intended to accomplish" — do not refactor, do not modify functionality unrelated to the migration, do not extend the migration's scope, do not touch code the migration was not concerned with. If you are unsure whether a fix is in scope, report it in \`summary\` instead of applying.`, `4. Apply every fix you can within scope, then write your handoff. On \`status: "success"\`, summarize what you verified and any fixes you applied. On \`status: "failed"\`, enumerate the unresolved findings in \`summary\` so the user can address them; no commit will be created from a failed run, so the generator's changes and your partial fixes will sit uncommitted in the working tree for the user to review.`, `</validation_instructions>`, ``, `Once you finish, write your handoff JSON to:`, ...(0, shared_rendering_1.renderHandoffPathFooter)(ctx.handoffFileAbsolutePath));
53
45
  return lines.join('\n');
54
46
  }
55
47
  function renderFileListBody(changes) {
@@ -18,16 +18,8 @@ const shared_rendering_1 = require("./shared-rendering");
18
18
  function buildHybridPromptUserPrompt(ctx) {
19
19
  const lines = [
20
20
  `Complete the AI-driven step that follows the generator phase of a two-phase Nx migration. The deterministic generator phase has already run; the sections below summarize what it did. The step may apply additional changes, verify the generator's output, or both — follow the instructions file.`,
21
- ``,
22
- `<migration>`,
23
- `package: ${(0, shared_rendering_1.escapeXmlBody)(ctx.package)}`,
24
- `version: ${(0, shared_rendering_1.escapeXmlBody)(ctx.version)}`,
25
- `name: ${(0, shared_rendering_1.escapeXmlBody)(ctx.name)}`,
21
+ ...(0, shared_rendering_1.renderMigrationBlock)(ctx),
26
22
  ];
27
- if (ctx.description) {
28
- lines.push(...(0, shared_rendering_1.renderKeyMultilineValue)('description', (0, shared_rendering_1.escapeXmlBody)(ctx.description)));
29
- }
30
- lines.push(`</migration>`);
31
23
  const logs = (0, shared_rendering_1.escapeXmlBody)((0, shared_rendering_1.stripAnsi)(ctx.impl?.logs ?? '').trim());
32
24
  const agentContext = (0, shared_rendering_1.filterNonEmptyStrings)(ctx.impl?.agentContext ?? []);
33
25
  const hasDiffContext = !!ctx.impl?.hasDiffContext;
@@ -45,9 +37,9 @@ function buildHybridPromptUserPrompt(ctx) {
45
37
  }
46
38
  }
47
39
  if (agentContext.length > 0) {
48
- lines.push(``, `<advisory_context note="hints from the generator phase; consult while following the instructions, not as separate tasks">`, ...agentContext.map((entry) => (0, shared_rendering_1.renderListItem)((0, shared_rendering_1.escapeXmlBody)(entry))), `</advisory_context>`);
40
+ lines.push(...(0, shared_rendering_1.renderAdvisoryContext)('hints from the generator phase; consult while following the instructions, not as separate tasks', agentContext));
49
41
  }
50
- lines.push(``, `<instructions_file>${(0, shared_rendering_1.escapeXmlBody)(ctx.promptPath)}</instructions_file>`, ``, `<precedence>If anything in the sections above conflicts with the instructions file, the instructions file wins.</precedence>`, ``, `Open the instructions file (path is workspace-relative), follow its instructions step by step using the sections above as context, then write your handoff JSON to:`, `<handoff_path>`, (0, shared_rendering_1.escapeXmlBody)(ctx.handoffFileAbsolutePath), `</handoff_path>`);
42
+ lines.push(``, `<instructions_file>${(0, shared_rendering_1.escapeXmlBody)(ctx.promptPath)}</instructions_file>`, ``, `<precedence>If anything in the sections above conflicts with the instructions file, the instructions file wins.</precedence>`, ``, `Open the instructions file (path is workspace-relative), follow its instructions step by step using the sections above as context, then write your handoff JSON to:`, ...(0, shared_rendering_1.renderHandoffPathFooter)(ctx.handoffFileAbsolutePath));
51
43
  return lines.join('\n');
52
44
  }
53
45
  function renderFileList(changes) {
@@ -15,15 +15,12 @@ const shared_rendering_1 = require("./shared-rendering");
15
15
  function buildPromptMigrationUserPrompt(ctx) {
16
16
  const lines = [
17
17
  `Apply one prompt-based migration to this Nx workspace.`,
18
+ ...(0, shared_rendering_1.renderMigrationBlock)(ctx),
18
19
  ``,
19
- `<migration>`,
20
- `package: ${(0, shared_rendering_1.escapeXmlBody)(ctx.package)}`,
21
- `version: ${(0, shared_rendering_1.escapeXmlBody)(ctx.version)}`,
22
- `name: ${(0, shared_rendering_1.escapeXmlBody)(ctx.name)}`,
20
+ `<instructions_file>${(0, shared_rendering_1.escapeXmlBody)(ctx.promptPath)}</instructions_file>`,
21
+ ``,
22
+ `Open the instructions file (path is workspace-relative), follow its instructions step by step, then write your handoff JSON to:`,
23
+ ...(0, shared_rendering_1.renderHandoffPathFooter)(ctx.handoffFileAbsolutePath),
23
24
  ];
24
- if (ctx.description) {
25
- lines.push(...(0, shared_rendering_1.renderKeyMultilineValue)('description', (0, shared_rendering_1.escapeXmlBody)(ctx.description)));
26
- }
27
- lines.push(`</migration>`, ``, `<instructions_file>${(0, shared_rendering_1.escapeXmlBody)(ctx.promptPath)}</instructions_file>`, ``, `Open the instructions file (path is workspace-relative), follow its instructions step by step, then write your handoff JSON to:`, `<handoff_path>`, (0, shared_rendering_1.escapeXmlBody)(ctx.handoffFileAbsolutePath), `</handoff_path>`);
28
25
  return lines.join('\n');
29
26
  }
@@ -7,3 +7,12 @@ export declare function filterNonEmptyStrings(entries: unknown[]): string[];
7
7
  export declare function escapeXmlBody(value: string): string;
8
8
  export declare function renderGitInspectInstruction(): string;
9
9
  export declare function renderGeneratorOutputBlock(logs: string): string[];
10
+ export interface MigrationBlockContext {
11
+ package: string;
12
+ name: string;
13
+ version: string;
14
+ description?: string;
15
+ }
16
+ export declare function renderMigrationBlock(ctx: MigrationBlockContext): string[];
17
+ export declare function renderHandoffPathFooter(handoffFileAbsolutePath: string): string[];
18
+ export declare function renderAdvisoryContext(note: string, entries: string[]): string[];
@@ -8,6 +8,9 @@ exports.filterNonEmptyStrings = filterNonEmptyStrings;
8
8
  exports.escapeXmlBody = escapeXmlBody;
9
9
  exports.renderGitInspectInstruction = renderGitInspectInstruction;
10
10
  exports.renderGeneratorOutputBlock = renderGeneratorOutputBlock;
11
+ exports.renderMigrationBlock = renderMigrationBlock;
12
+ exports.renderHandoffPathFooter = renderHandoffPathFooter;
13
+ exports.renderAdvisoryContext = renderAdvisoryContext;
11
14
  function renderFileEntry(change) {
12
15
  return `[${change.type}] ${change.path}`;
13
16
  }
@@ -85,3 +88,43 @@ function renderGeneratorOutputBlock(logs) {
85
88
  `</generator_output>`,
86
89
  ];
87
90
  }
91
+ // Identical `<migration>` block used by prompt-migration, hybrid, and
92
+ // generic-validation builders. Leading blank included so callers can spread
93
+ // directly after a lead sentence. Centralized so the schema and the
94
+ // `escapeXmlBody` contract on values stay in lock-step.
95
+ function renderMigrationBlock(ctx) {
96
+ const lines = [
97
+ ``,
98
+ `<migration>`,
99
+ `package: ${escapeXmlBody(ctx.package)}`,
100
+ `version: ${escapeXmlBody(ctx.version)}`,
101
+ `name: ${escapeXmlBody(ctx.name)}`,
102
+ ];
103
+ if (ctx.description) {
104
+ lines.push(...renderKeyMultilineValue('description', escapeXmlBody(ctx.description)));
105
+ }
106
+ lines.push(`</migration>`);
107
+ return lines;
108
+ }
109
+ // `<handoff_path>` footer that follows the "write your handoff JSON to:"
110
+ // sentence. No leading blank — the block is part of that sentence's structure,
111
+ // not a separate section.
112
+ function renderHandoffPathFooter(handoffFileAbsolutePath) {
113
+ return [
114
+ `<handoff_path>`,
115
+ escapeXmlBody(handoffFileAbsolutePath),
116
+ `</handoff_path>`,
117
+ ];
118
+ }
119
+ // Advisory hints from the generator phase. The `note` attribute varies between
120
+ // callers (hybrid vs generic-validation lead-in differs), so it's parameterized
121
+ // rather than hardcoded. Entries are escaped here so callers can pass raw
122
+ // strings.
123
+ function renderAdvisoryContext(note, entries) {
124
+ return [
125
+ ``,
126
+ `<advisory_context note="${note}">`,
127
+ ...entries.map((entry) => renderListItem(escapeXmlBody(entry))),
128
+ `</advisory_context>`,
129
+ ];
130
+ }
@@ -27,6 +27,9 @@ async function runAgentic(args) {
27
27
  windowsHide: true,
28
28
  });
29
29
  let child;
30
+ // Local alias so `@nx/workspace-require-windows-hide` recognizes the
31
+ // options arg as a tracked Identifier rather than giving up on a
32
+ // member-expression skip — keeps the lint rule strict on other call sites.
30
33
  const spawnOptions = adapted.options;
31
34
  try {
32
35
  child = (0, child_process_1.spawn)(adapted.binary, adapted.args, spawnOptions);
@@ -41,19 +41,82 @@ export declare function logAgenticSuccessOutcome(label: string, sha: string | nu
41
41
  * landed multiple migrations' contributions.
42
42
  */
43
43
  export type MigrationOutcomeKind = 'applied' | 'no-changes' | 'deferred';
44
- export interface MigrationOutcome {
44
+ /**
45
+ * The state of a migration's commit attempt. Tagged union so consumers don't
46
+ * have to re-derive legal combinations from nullable fields.
47
+ *
48
+ * - `none` — no commit was attempted (`--no-create-commits` or no diff to
49
+ * commit).
50
+ * - `landed` — a commit was actually created. `sha: null` only when
51
+ * `git rev-parse HEAD` failed transiently right after the
52
+ * commit landed; by contract the diff did clear.
53
+ * - `failed` — commit was attempted and errored (signing, hook rejection,
54
+ * lock, install error mid-attempt, etc.). The diff stays in
55
+ * the working tree until a later migration's commit absorbs
56
+ * it (then transitions to `absorbed`) or until the run ends
57
+ * (then surfaces as retained state).
58
+ * - `absorbed` — own commit failed but a later migration's commit absorbed
59
+ * this diff via `git add -A`. `into.sha: null` means that
60
+ * absorbing commit itself hit a HEAD-resolve race; the recap
61
+ * renders an anchor without a sha.
62
+ */
63
+ export type CommitState = {
64
+ kind: 'none';
65
+ } | {
66
+ kind: 'landed';
67
+ sha: string | null;
68
+ } | {
69
+ kind: 'failed';
70
+ } | {
71
+ kind: 'absorbed';
72
+ into: {
73
+ name: string;
74
+ sha: string | null;
75
+ };
76
+ };
77
+ /**
78
+ * Per-migration record produced by the executor loop. `status: 'completed'`
79
+ * carries the kind (applied / no-changes / deferred); `status: 'aborted'`
80
+ * means the migration threw before completing — the executor's catch block
81
+ * records it so the recap can list it under retained-state alongside any
82
+ * other migrations whose commits never landed.
83
+ */
84
+ export type MigrationOutcome = {
45
85
  migration: {
46
86
  package: string;
47
87
  name: string;
48
88
  };
49
- outcome: MigrationOutcomeKind;
50
- committedSha: string | null;
51
- commitFailed?: boolean;
52
- committedAsPartOf?: {
89
+ status: 'completed';
90
+ kind: MigrationOutcomeKind;
91
+ commit: CommitState;
92
+ } | {
93
+ migration: {
94
+ package: string;
53
95
  name: string;
54
- sha: string | null;
55
96
  };
56
- }
97
+ status: 'aborted';
98
+ commit: CommitState;
99
+ };
100
+ /**
101
+ * Counts the migrations whose own commit actually landed — including the
102
+ * HEAD-resolve-race case (`commit: { kind: 'landed', sha: null }`). Used by
103
+ * the end-of-run "<K> commits created" tally and by the success-path
104
+ * accounting in `executeMigrations`. Counts landed-commit *records* rather
105
+ * than distinct shas; absorbed predecessors (`kind: 'absorbed'`) are not
106
+ * counted because the absorbing commit's record already contributes one.
107
+ */
108
+ export declare function countLandedCommits(outcomes: ReadonlyArray<MigrationOutcome>): number;
109
+ /**
110
+ * Migrations whose own commit attempt failed and whose diff was never
111
+ * absorbed by a later commit. Surfaces what the user has to commit or
112
+ * revert after the run. Filters on `commit.kind === 'failed'` exactly —
113
+ * `'absorbed'` means the diff cleared into a later commit, `'none'` means
114
+ * no commit was attempted (intentional `--no-create-commits` or no-op).
115
+ */
116
+ export declare function retainedMigrations(outcomes: ReadonlyArray<MigrationOutcome>): Array<{
117
+ package: string;
118
+ name: string;
119
+ }>;
57
120
  /**
58
121
  * Logs a structured recap when a migration throws mid-loop. Inserted between
59
122
  * the "Failed to run X" error block and the re-throw so the user (or AI agent
@@ -71,10 +134,6 @@ export declare function logFailureRecap(opts: {
71
134
  migrationIndex: number;
72
135
  totalMigrations: number;
73
136
  outcomes: ReadonlyArray<MigrationOutcome>;
74
- pendingMigrations?: ReadonlyArray<{
75
- package: string;
76
- name: string;
77
- }>;
78
137
  migrationEmittedNextSteps: string[];
79
138
  insideAgent: boolean;
80
139
  }): void;
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resetSgrAfterAgent = resetSgrAfterAgent;
4
4
  exports.logMigrationBoundary = logMigrationBoundary;
5
5
  exports.logAgenticSuccessOutcome = logAgenticSuccessOutcome;
6
+ exports.countLandedCommits = countLandedCommits;
7
+ exports.retainedMigrations = retainedMigrations;
6
8
  exports.logFailureRecap = logFailureRecap;
7
9
  exports.buildTallyBodyLine = buildTallyBodyLine;
8
10
  exports.buildRetainedAtSuccessBody = buildRetainedAtSuccessBody;
@@ -47,6 +49,29 @@ function logAgenticSuccessOutcome(label, sha, summary) {
47
49
  const shaPart = sha ? ` (${sha})` : '';
48
50
  logger_1.logger.info(`${pc.green('✓')} ${label}${shaPart}: ${summary}`);
49
51
  }
52
+ /**
53
+ * Counts the migrations whose own commit actually landed — including the
54
+ * HEAD-resolve-race case (`commit: { kind: 'landed', sha: null }`). Used by
55
+ * the end-of-run "<K> commits created" tally and by the success-path
56
+ * accounting in `executeMigrations`. Counts landed-commit *records* rather
57
+ * than distinct shas; absorbed predecessors (`kind: 'absorbed'`) are not
58
+ * counted because the absorbing commit's record already contributes one.
59
+ */
60
+ function countLandedCommits(outcomes) {
61
+ return outcomes.filter((o) => o.commit.kind === 'landed').length;
62
+ }
63
+ /**
64
+ * Migrations whose own commit attempt failed and whose diff was never
65
+ * absorbed by a later commit. Surfaces what the user has to commit or
66
+ * revert after the run. Filters on `commit.kind === 'failed'` exactly —
67
+ * `'absorbed'` means the diff cleared into a later commit, `'none'` means
68
+ * no commit was attempted (intentional `--no-create-commits` or no-op).
69
+ */
70
+ function retainedMigrations(outcomes) {
71
+ return outcomes
72
+ .filter((o) => o.commit.kind === 'failed')
73
+ .map((o) => ({ package: o.migration.package, name: o.migration.name }));
74
+ }
50
75
  /**
51
76
  * Logs a structured recap when a migration throws mid-loop. Inserted between
52
77
  * the "Failed to run X" error block and the re-throw so the user (or AI agent
@@ -61,62 +86,54 @@ function logAgenticSuccessOutcome(label, sha, summary) {
61
86
  * the earlier sha.
62
87
  */
63
88
  function logFailureRecap(opts) {
64
- const { migrationIndex, totalMigrations, outcomes, pendingMigrations = [], migrationEmittedNextSteps, insideAgent, } = opts;
65
- const appliedCount = outcomes.filter((o) => o.outcome === 'applied' || o.outcome === 'no-changes').length;
66
- const deferredCount = outcomes.filter((o) => o.outcome === 'deferred').length;
89
+ const { migrationIndex, totalMigrations, outcomes, migrationEmittedNextSteps, insideAgent, } = opts;
90
+ const appliedCount = outcomes.filter((o) => o.status === 'completed' &&
91
+ (o.kind === 'applied' || o.kind === 'no-changes')).length;
92
+ const deferredCount = outcomes.filter((o) => o.status === 'completed' && o.kind === 'deferred').length;
67
93
  const notAttempted = totalMigrations - migrationIndex;
68
- // Walk back to the most recent record whose work is anchored to a sha —
69
- // either its own commit, or a later commit that absorbed it. Both
70
- // `applied` AND `deferred` outcomes can carry a successful commit
71
- // (hybrid-without-agentic produces `deferred` with a sha from its
72
- // deterministic half). Skipped and uncommitted records don't anchor.
94
+ // Walk back to the most recent completed record whose work is anchored to
95
+ // a sha — either its own commit (`landed`), or a later commit that
96
+ // absorbed it (`absorbed`). Both `applied` AND `deferred` outcomes can
97
+ // carry a successful commit (hybrid-without-agentic produces `deferred`
98
+ // with a sha from its deterministic half). Skipped and uncommitted
99
+ // records don't anchor.
73
100
  let lastApplied;
74
101
  for (let i = outcomes.length - 1; i >= 0; i--) {
75
102
  const o = outcomes[i];
76
- if ((o.outcome === 'applied' || o.outcome === 'deferred') &&
77
- (o.committedSha || o.committedAsPartOf)) {
103
+ if (o.status !== 'completed')
104
+ continue;
105
+ if ((o.kind === 'applied' || o.kind === 'deferred') &&
106
+ (o.commit.kind === 'landed' || o.commit.kind === 'absorbed')) {
78
107
  lastApplied = o;
79
108
  break;
80
109
  }
81
110
  }
82
111
  // Migrations whose own commit attempt errored AND whose diff was never
83
112
  // absorbed by a later commit. Surfaces what the user has to inspect or
84
- // clean up in the working tree.
85
- //
86
- // Two sources, merged and deduped:
87
- // - Outcomes with `commitFailed: true && !committedAsPartOf`.
88
- // - `pendingMigrations` entries covers the in-flight migration whose
89
- // install threw before its outcome could be pushed, AND any prior
90
- // failed-commit migration that's still pending at recap time.
91
- //
92
- // Filtering on `commitFailed` (rather than on missing sha) avoids
93
- // mislabeling intentional-no-commit cases: `--no-create-commits` runs
94
- // and no-op steps both yield `committedSha: null` without a failure.
95
- const seenKeys = new Set();
113
+ // clean up in the working tree. Single iteration over `outcomes` —
114
+ // `outcomes` is the sole source of truth (including the in-flight
115
+ // migration recorded by the executor's catch block), so no dedupe is
116
+ // needed. `kind: 'failed'` excludes `kind: 'none'` (intentional no-commit:
117
+ // `--no-create-commits` runs and no-op steps).
96
118
  const uncommittedAtFailure = [];
97
119
  for (const o of outcomes) {
98
- if (o.commitFailed !== true || o.committedAsPartOf)
99
- continue;
100
- const key = `${o.migration.package}:${o.migration.name}`;
101
- if (seenKeys.has(key))
120
+ if (o.commit.kind !== 'failed')
102
121
  continue;
103
- seenKeys.add(key);
104
122
  uncommittedAtFailure.push({ migration: o.migration });
105
123
  }
106
- for (const p of pendingMigrations) {
107
- const key = `${p.package}:${p.name}`;
108
- if (seenKeys.has(key))
109
- continue;
110
- seenKeys.add(key);
111
- uncommittedAtFailure.push({ migration: p });
112
- }
113
124
  logger_1.logger.info('');
114
125
  logger_1.logger.info(`Run halted at migration ${migrationIndex} of ${totalMigrations}.`);
115
126
  if (appliedCount === 0 && deferredCount === 0) {
116
127
  logger_1.logger.info(`0 migrations completed. ${notAttempted} not attempted.`);
117
128
  }
118
129
  else {
119
- const anchorSha = lastApplied?.committedSha ?? lastApplied?.committedAsPartOf?.sha;
130
+ const anchorSha = lastApplied === undefined
131
+ ? undefined
132
+ : lastApplied.commit.kind === 'landed'
133
+ ? lastApplied.commit.sha
134
+ : lastApplied.commit.kind === 'absorbed'
135
+ ? lastApplied.commit.into.sha
136
+ : undefined;
120
137
  // When the anchoring commit hit a HEAD-resolve race, the sha is null;
121
138
  // render the migration name alone (without "→ null") so the recap
122
139
  // never displays the literal word "null" to the user.