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.
- package/dist/src/command-line/migrate/agentic/prompts/generic-validation.js +3 -11
- package/dist/src/command-line/migrate/agentic/prompts/hybrid-prompt-migration.js +3 -11
- package/dist/src/command-line/migrate/agentic/prompts/prompt-migration.js +5 -8
- package/dist/src/command-line/migrate/agentic/prompts/shared-rendering.d.ts +9 -0
- package/dist/src/command-line/migrate/agentic/prompts/shared-rendering.js +43 -0
- package/dist/src/command-line/migrate/agentic/runner.js +3 -0
- package/dist/src/command-line/migrate/migrate-output.d.ts +70 -11
- package/dist/src/command-line/migrate/migrate-output.js +52 -35
- package/dist/src/command-line/migrate/migrate.js +83 -80
- package/dist/src/command-line/migrate/run-migration-process.js +9 -1
- package/dist/src/core/graph/main.js +1 -1
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/utils/git-utils.d.ts +1 -0
- package/dist/src/utils/git-utils.js +65 -0
- package/package.json +12 -12
|
@@ -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(
|
|
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:`,
|
|
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(
|
|
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:`,
|
|
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
|
-
`<
|
|
20
|
-
|
|
21
|
-
`
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
65
|
-
const appliedCount = outcomes.filter((o) => o.
|
|
66
|
-
|
|
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
|
|
69
|
-
// either its own commit, or a later commit that
|
|
70
|
-
// `applied` AND `deferred` outcomes can
|
|
71
|
-
// (hybrid-without-agentic produces `deferred`
|
|
72
|
-
// deterministic half). Skipped and uncommitted
|
|
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 (
|
|
77
|
-
|
|
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
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
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.
|
|
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
|
|
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.
|