@nebutra/agent-runtime 0.2.0 → 0.2.2
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/LICENSE +21 -676
- package/README.md +2 -0
- package/dist/adapters/dispatcher-sse.js +1 -0
- package/dist/adapters/index.d.ts +24 -5
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/mcp-catalog.js +1 -0
- package/dist/adapters/prisma-rollout.js +1 -0
- package/dist/chunk-424PT5DM.js +23 -0
- package/dist/chunk-424PT5DM.js.map +1 -0
- package/dist/{chunk-NN7DATXA.js → chunk-4Y25ZTKI.js} +3 -3
- package/dist/chunk-4Y25ZTKI.js.map +1 -0
- package/dist/{chunk-BJBBR3QA.js → chunk-D4YAPLOW.js} +4 -4
- package/dist/chunk-D4YAPLOW.js.map +1 -0
- package/dist/{chunk-ZMYX5VBU.js → chunk-GQZKYWFT.js} +23 -7
- package/dist/chunk-GQZKYWFT.js.map +1 -0
- package/dist/chunk-KCNN4QUQ.js +255 -0
- package/dist/chunk-KCNN4QUQ.js.map +1 -0
- package/dist/{chunk-PGGWSUTM.js → chunk-NI4EDT4T.js} +2 -2
- package/dist/chunk-NI4EDT4T.js.map +1 -0
- package/dist/chunk-Q62VKHIT.js +178 -0
- package/dist/chunk-Q62VKHIT.js.map +1 -0
- package/dist/{chunk-MUF7ZZTO.js → chunk-R5HOSQUW.js} +2 -1
- package/dist/{chunk-MUF7ZZTO.js.map → chunk-R5HOSQUW.js.map} +1 -1
- package/dist/{chunk-5N4644PB.js → chunk-SD2ZJ7XG.js} +4 -4
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +52 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.js +1 -0
- package/dist/definitions.js +1 -0
- package/dist/dispatcher.js +1 -0
- package/dist/durable-turn.js +3 -2
- package/dist/hook-pipeline.js +1 -0
- package/dist/index.d.ts +8 -85
- package/dist/index.js +249 -134
- package/dist/index.js.map +1 -1
- package/dist/loop.d.ts +2 -2
- package/dist/loop.js +3 -2
- package/dist/mcp-bridge.d.ts +3 -3
- package/dist/mcp-bridge.js +3 -2
- package/dist/model.js +1 -0
- package/dist/orchestration.d.ts +84 -0
- package/dist/orchestration.js +16 -0
- package/dist/orchestration.js.map +1 -0
- package/dist/policy.js +1 -0
- package/dist/protocol.js +1 -0
- package/dist/pulsar.d.ts +78 -0
- package/dist/pulsar.js +17 -0
- package/dist/pulsar.js.map +1 -0
- package/dist/rollout-store-persistent.js +1 -0
- package/dist/rollout.js +1 -0
- package/dist/sandbox.js +2 -1
- package/dist/skills.js +2 -1
- package/dist/subagents.js +1 -0
- package/dist/tools.d.ts +4 -3
- package/dist/tools.js +5 -3
- package/package.json +84 -27
- package/.turbo/turbo-build.log +0 -115
- package/.turbo/turbo-test.log +0 -44
- package/.turbo/turbo-typecheck.log +0 -4
- package/CHANGELOG.md +0 -253
- package/dist/chunk-BJBBR3QA.js.map +0 -1
- package/dist/chunk-NN7DATXA.js.map +0 -1
- package/dist/chunk-PGGWSUTM.js.map +0 -1
- package/dist/chunk-ZMYX5VBU.js.map +0 -1
- package/src/adapters/dispatcher-sse.test.ts +0 -218
- package/src/adapters/dispatcher-sse.ts +0 -222
- package/src/adapters/index.ts +0 -18
- package/src/adapters/mcp-catalog.test.ts +0 -213
- package/src/adapters/mcp-catalog.ts +0 -188
- package/src/adapters/prisma-rollout.test.ts +0 -153
- package/src/adapters/prisma-rollout.ts +0 -104
- package/src/agent-runtime.test.ts +0 -176
- package/src/artifact-stream.test.ts +0 -330
- package/src/artifact-stream.ts +0 -453
- package/src/channel-gateway.test.ts +0 -432
- package/src/channel-gateway.ts +0 -357
- package/src/code-review.test.ts +0 -501
- package/src/code-review.ts +0 -495
- package/src/command-suggestions.test.ts +0 -251
- package/src/command-suggestions.ts +0 -338
- package/src/commands.test.ts +0 -184
- package/src/commands.ts +0 -140
- package/src/commit-message.test.ts +0 -249
- package/src/commit-message.ts +0 -180
- package/src/context-compaction.test.ts +0 -522
- package/src/context-compaction.ts +0 -434
- package/src/definitions.test.ts +0 -78
- package/src/definitions.ts +0 -190
- package/src/deployment-status.test.ts +0 -215
- package/src/deployment-status.ts +0 -227
- package/src/design-context.test.ts +0 -195
- package/src/design-context.ts +0 -198
- package/src/dispatcher.test.ts +0 -234
- package/src/dispatcher.ts +0 -189
- package/src/durable-turn.test.ts +0 -209
- package/src/durable-turn.ts +0 -135
- package/src/edit-planner.test.ts +0 -204
- package/src/edit-planner.ts +0 -325
- package/src/fuzzy-match.test.ts +0 -311
- package/src/fuzzy-match.ts +0 -444
- package/src/hook-pipeline.test.ts +0 -279
- package/src/hook-pipeline.ts +0 -373
- package/src/inbound-admission.test.ts +0 -394
- package/src/inbound-admission.ts +0 -246
- package/src/index.ts +0 -47
- package/src/loop.test.ts +0 -161
- package/src/loop.ts +0 -211
- package/src/mcp-bridge.test.ts +0 -165
- package/src/mcp-bridge.ts +0 -76
- package/src/memory-provider.test.ts +0 -232
- package/src/memory-provider.ts +0 -257
- package/src/model.ts +0 -168
- package/src/permission-ruleset.test.ts +0 -301
- package/src/permission-ruleset.ts +0 -200
- package/src/policy.ts +0 -151
- package/src/project-repo.test.ts +0 -232
- package/src/project-repo.ts +0 -311
- package/src/protocol.ts +0 -159
- package/src/rollout-store-persistent.test.ts +0 -217
- package/src/rollout-store-persistent.ts +0 -166
- package/src/rollout.ts +0 -150
- package/src/sandbox.ts +0 -113
- package/src/session-share.test.ts +0 -360
- package/src/session-share.ts +0 -310
- package/src/skill-distillation.test.ts +0 -177
- package/src/skill-distillation.ts +0 -369
- package/src/skills.test.ts +0 -277
- package/src/skills.ts +0 -255
- package/src/subagents.test.ts +0 -290
- package/src/subagents.ts +0 -332
- package/src/tools.ts +0 -126
- package/src/workbench.test.ts +0 -0
- package/src/workbench.ts +0 -0
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -33
- /package/dist/{chunk-5N4644PB.js.map → chunk-SD2ZJ7XG.js.map} +0 -0
package/src/edit-planner.test.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
analyzeEditIntent,
|
|
5
|
-
applyEditBlock,
|
|
6
|
-
buildEditInstructions,
|
|
7
|
-
type EditBlock,
|
|
8
|
-
EditType,
|
|
9
|
-
type FileManifest,
|
|
10
|
-
parseEditBlocks,
|
|
11
|
-
planEdit,
|
|
12
|
-
selectFilesForEdit,
|
|
13
|
-
} from "./edit-planner.js";
|
|
14
|
-
|
|
15
|
-
const TENANT = "org_123";
|
|
16
|
-
|
|
17
|
-
const manifest: FileManifest = {
|
|
18
|
-
files: {
|
|
19
|
-
"src/App.tsx": { path: "src/App.tsx", type: "page" },
|
|
20
|
-
"src/components/Button.tsx": {
|
|
21
|
-
path: "src/components/Button.tsx",
|
|
22
|
-
type: "component",
|
|
23
|
-
},
|
|
24
|
-
"src/components/Header.tsx": {
|
|
25
|
-
path: "src/components/Header.tsx",
|
|
26
|
-
type: "component",
|
|
27
|
-
},
|
|
28
|
-
"src/pages/Home.tsx": { path: "src/pages/Home.tsx", type: "page" },
|
|
29
|
-
"src/styles/theme.css": { path: "src/styles/theme.css", type: "style" },
|
|
30
|
-
"package.json": { path: "package.json", type: "config" },
|
|
31
|
-
"src/utils/format.ts": { path: "src/utils/format.ts", type: "util" },
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
describe("analyzeEditIntent", () => {
|
|
36
|
-
it("matches UPDATE_STYLE for styling prompts", () => {
|
|
37
|
-
const intent = analyzeEditIntent("change the color theme to blue", manifest);
|
|
38
|
-
expect(intent.type).toBe(EditType.UPDATE_STYLE);
|
|
39
|
-
expect(intent.targetFiles).toContain("src/styles/theme.css");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("matches ADD_DEPENDENCY for package install prompts", () => {
|
|
43
|
-
const intent = analyzeEditIntent("install the axios package", manifest);
|
|
44
|
-
expect(intent.type).toBe(EditType.ADD_DEPENDENCY);
|
|
45
|
-
expect(intent.targetFiles).toContain("package.json");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("matches FIX_ISSUE for bug prompts", () => {
|
|
49
|
-
const intent = analyzeEditIntent("fix the bug in the Header", manifest);
|
|
50
|
-
expect(intent.type).toBe(EditType.FIX_ISSUE);
|
|
51
|
-
expect(intent.targetFiles).toContain("src/components/Header.tsx");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("matches FULL_REBUILD and targets all files", () => {
|
|
55
|
-
const intent = analyzeEditIntent("rebuild the entire app from scratch", manifest);
|
|
56
|
-
expect(intent.type).toBe(EditType.FULL_REBUILD);
|
|
57
|
-
expect(intent.targetFiles.sort()).toEqual(Object.keys(manifest.files).sort());
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("matches REFACTOR", () => {
|
|
61
|
-
const intent = analyzeEditIntent("refactor the Button component", manifest);
|
|
62
|
-
expect(intent.type).toBe(EditType.REFACTOR);
|
|
63
|
-
expect(intent.targetFiles).toContain("src/components/Button.tsx");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("matches ADD_FEATURE with page insertion points", () => {
|
|
67
|
-
const intent = analyzeEditIntent("add a new search feature", manifest);
|
|
68
|
-
expect(intent.type).toBe(EditType.ADD_FEATURE);
|
|
69
|
-
expect(intent.targetFiles).toContain("src/App.tsx");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("matches UPDATE_COMPONENT by component name", () => {
|
|
73
|
-
const intent = analyzeEditIntent("update the Button component label", manifest);
|
|
74
|
-
expect(intent.type).toBe(EditType.UPDATE_COMPONENT);
|
|
75
|
-
expect(intent.targetFiles).toContain("src/components/Button.tsx");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("first-match-wins: fix beats update ordering", () => {
|
|
79
|
-
const intent = analyzeEditIntent("fix the broken Button update", manifest);
|
|
80
|
-
expect(intent.type).toBe(EditType.FIX_ISSUE);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("falls back to UPDATE_COMPONENT with empty targets on no match", () => {
|
|
84
|
-
const intent = analyzeEditIntent("zzz qqq", manifest);
|
|
85
|
-
expect(intent.type).toBe(EditType.UPDATE_COMPONENT);
|
|
86
|
-
expect(intent.targetFiles).toEqual([]);
|
|
87
|
-
expect(intent.description).toBe("zzz qqq");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("does not mutate the manifest", () => {
|
|
91
|
-
const snapshot = JSON.stringify(manifest);
|
|
92
|
-
analyzeEditIntent("rebuild everything", manifest);
|
|
93
|
-
expect(JSON.stringify(manifest)).toBe(snapshot);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("buildEditInstructions", () => {
|
|
98
|
-
it("is deterministic per edit type", () => {
|
|
99
|
-
expect(buildEditInstructions(EditType.FIX_ISSUE)).toBe(
|
|
100
|
-
buildEditInstructions(EditType.FIX_ISSUE),
|
|
101
|
-
);
|
|
102
|
-
expect(buildEditInstructions(EditType.FIX_ISSUE)).not.toBe(
|
|
103
|
-
buildEditInstructions(EditType.ADD_FEATURE),
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe("selectFilesForEdit", () => {
|
|
109
|
-
it("splits primary and context, always includes key files", () => {
|
|
110
|
-
const ctx = selectFilesForEdit("update the Button component", manifest);
|
|
111
|
-
expect(ctx.primaryFiles).toContain("src/components/Button.tsx");
|
|
112
|
-
expect(ctx.contextFiles).toContain("src/App.tsx");
|
|
113
|
-
expect(ctx.contextFiles).toContain("package.json");
|
|
114
|
-
expect(ctx.contextFiles).not.toContain("src/components/Button.tsx");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("FULL_REBUILD makes all files primary", () => {
|
|
118
|
-
const ctx = selectFilesForEdit("rebuild the whole app", manifest);
|
|
119
|
-
expect(ctx.primaryFiles.sort()).toEqual(Object.keys(manifest.files).sort());
|
|
120
|
-
expect(ctx.contextFiles).toEqual([]);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("systemPrompt is deterministic and skips structure for FULL_REBUILD", () => {
|
|
124
|
-
const a = selectFilesForEdit("update the Button", manifest);
|
|
125
|
-
const b = selectFilesForEdit("update the Button", manifest);
|
|
126
|
-
expect(a.systemPrompt).toBe(b.systemPrompt);
|
|
127
|
-
expect(a.systemPrompt).toContain("File structure");
|
|
128
|
-
const rebuild = selectFilesForEdit("full rebuild now", manifest);
|
|
129
|
-
expect(rebuild.systemPrompt).not.toContain("File structure");
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe("parseEditBlocks", () => {
|
|
134
|
-
it("parses multiple blocks", () => {
|
|
135
|
-
const text = `
|
|
136
|
-
<<<edit file="a.tsx">>>
|
|
137
|
-
do thing one
|
|
138
|
-
---
|
|
139
|
-
const a = 1;
|
|
140
|
-
<<<end>>>
|
|
141
|
-
noise
|
|
142
|
-
<<<edit file="b.tsx">>>
|
|
143
|
-
do thing two
|
|
144
|
-
---
|
|
145
|
-
const b = 2;
|
|
146
|
-
<<<end>>>`;
|
|
147
|
-
const blocks = parseEditBlocks(text);
|
|
148
|
-
expect(blocks).toHaveLength(2);
|
|
149
|
-
expect(blocks[0]).toEqual<EditBlock>({
|
|
150
|
-
targetFile: "a.tsx",
|
|
151
|
-
instructions: "do thing one",
|
|
152
|
-
update: "const a = 1;",
|
|
153
|
-
});
|
|
154
|
-
expect(blocks[1]?.targetFile).toBe("b.tsx");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("tolerates malformed input", () => {
|
|
158
|
-
expect(parseEditBlocks("no blocks here")).toEqual([]);
|
|
159
|
-
expect(parseEditBlocks('<<<edit file="x">>> incomplete')).toEqual([]);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe("applyEditBlock", () => {
|
|
164
|
-
const block: EditBlock = {
|
|
165
|
-
targetFile: "a.tsx",
|
|
166
|
-
instructions: "append",
|
|
167
|
-
update: "ADDED",
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
it("uses the injected merger", async () => {
|
|
171
|
-
const merge = async (orig: string, upd: string) => `${orig}|${upd}`;
|
|
172
|
-
const res = await applyEditBlock("ORIG", block, merge);
|
|
173
|
-
expect(res.ok).toBe(true);
|
|
174
|
-
expect(res.merged).toBe("ORIG|ADDED");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("returns error path when merger throws", async () => {
|
|
178
|
-
const merge = async () => {
|
|
179
|
-
throw new Error("boom");
|
|
180
|
-
};
|
|
181
|
-
const res = await applyEditBlock("ORIG", block, merge);
|
|
182
|
-
expect(res.ok).toBe(false);
|
|
183
|
-
expect(res.error).toContain("boom");
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe("planEdit (tenant)", () => {
|
|
188
|
-
it("fails closed on empty tenant", async () => {
|
|
189
|
-
await expect(planEdit("", "update the Button", manifest)).rejects.toThrow();
|
|
190
|
-
await expect(planEdit(" ", "update the Button", manifest)).rejects.toThrow();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("returns a FileContext for a valid tenant", async () => {
|
|
194
|
-
const ctx = await planEdit(TENANT, "update the Button component", manifest);
|
|
195
|
-
expect(ctx.editIntent.type).toBe(EditType.UPDATE_COMPONENT);
|
|
196
|
-
expect(ctx.primaryFiles).toContain("src/components/Button.tsx");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("does not mutate the input manifest", async () => {
|
|
200
|
-
const snapshot = JSON.stringify(manifest);
|
|
201
|
-
await planEdit(TENANT, "rebuild everything", manifest);
|
|
202
|
-
expect(JSON.stringify(manifest)).toBe(snapshot);
|
|
203
|
-
});
|
|
204
|
-
});
|
package/src/edit-planner.ts
DELETED
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Edit planner — a faithful re-expression of a targeted-edit model for an
|
|
5
|
-
* app-builder agent. Given a natural-language prompt and a manifest of the
|
|
6
|
-
* current project files, it classifies the edit intent, selects the minimal
|
|
7
|
-
* set of files to send to a codegen turn, and provides a terse "fast-apply"
|
|
8
|
-
* format for re-integrating model output.
|
|
9
|
-
*
|
|
10
|
-
* Everything here is deterministic and pure aside from `applyEditBlock`,
|
|
11
|
-
* whose merge step is provider-specific and injected.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export enum EditType {
|
|
15
|
-
UPDATE_COMPONENT = "UPDATE_COMPONENT",
|
|
16
|
-
ADD_FEATURE = "ADD_FEATURE",
|
|
17
|
-
FIX_ISSUE = "FIX_ISSUE",
|
|
18
|
-
REFACTOR = "REFACTOR",
|
|
19
|
-
FULL_REBUILD = "FULL_REBUILD",
|
|
20
|
-
UPDATE_STYLE = "UPDATE_STYLE",
|
|
21
|
-
ADD_DEPENDENCY = "ADD_DEPENDENCY",
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type FileKind = "component" | "page" | "style" | "config" | "util" | "other";
|
|
25
|
-
|
|
26
|
-
export interface FileInfo {
|
|
27
|
-
path: string;
|
|
28
|
-
type: FileKind;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface FileManifest {
|
|
32
|
-
files: Record<string, FileInfo>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface EditIntent {
|
|
36
|
-
type: EditType;
|
|
37
|
-
targetFiles: string[];
|
|
38
|
-
description: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface FileContext {
|
|
42
|
-
primaryFiles: string[];
|
|
43
|
-
contextFiles: string[];
|
|
44
|
-
systemPrompt: string;
|
|
45
|
-
editIntent: EditIntent;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface EditBlock {
|
|
49
|
-
targetFile: string;
|
|
50
|
-
instructions: string;
|
|
51
|
-
update: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export type EditMerger = (
|
|
55
|
-
originalContent: string,
|
|
56
|
-
update: string,
|
|
57
|
-
instructions: string,
|
|
58
|
-
) => Promise<string>;
|
|
59
|
-
|
|
60
|
-
export interface ApplyEditResult {
|
|
61
|
-
ok: boolean;
|
|
62
|
-
merged?: string;
|
|
63
|
-
error?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const fileManifestSchema = z.object({
|
|
67
|
-
files: z.record(
|
|
68
|
-
z.string(),
|
|
69
|
-
z.object({
|
|
70
|
-
path: z.string(),
|
|
71
|
-
type: z.enum(["component", "page", "style", "config", "util", "other"]),
|
|
72
|
-
}),
|
|
73
|
-
),
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// --- pure file resolvers -------------------------------------------------
|
|
77
|
-
|
|
78
|
-
function allFiles(manifest: FileManifest): string[] {
|
|
79
|
-
return Object.keys(manifest.files);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function filesByType(manifest: FileManifest, type: FileKind): string[] {
|
|
83
|
-
return allFiles(manifest).filter((p) => manifest.files[p]?.type === type);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Match a component file by its base name appearing in the prompt. */
|
|
87
|
-
function resolveByComponentName(prompt: string, manifest: FileManifest): string[] {
|
|
88
|
-
const lower = prompt.toLowerCase();
|
|
89
|
-
return allFiles(manifest).filter((p) => {
|
|
90
|
-
const info = manifest.files[p];
|
|
91
|
-
if (!info || (info.type !== "component" && info.type !== "page")) {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
const base = (p.split("/").pop() ?? "").replace(/\.[^.]+$/, "");
|
|
95
|
-
return base.length > 0 && lower.includes(base.toLowerCase());
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Feature-insertion points: an App/entry file plus pages. */
|
|
100
|
-
function resolveFeatureInsertionPoints(_prompt: string, manifest: FileManifest): string[] {
|
|
101
|
-
const pages = filesByType(manifest, "page");
|
|
102
|
-
const entry = allFiles(manifest).filter((p) => /(^|\/)App\.[tj]sx?$/.test(p));
|
|
103
|
-
return Array.from(new Set([...entry, ...pages]));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function resolveStyleFiles(_prompt: string, manifest: FileManifest): string[] {
|
|
107
|
-
return filesByType(manifest, "style");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function resolveConfigFiles(_prompt: string, manifest: FileManifest): string[] {
|
|
111
|
-
const configs = filesByType(manifest, "config");
|
|
112
|
-
const pkg = allFiles(manifest).filter((p) => /(^|\/)package\.json$/.test(p));
|
|
113
|
-
return Array.from(new Set([...pkg, ...configs]));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** For FIX: prefer files named in the prompt, else components + pages. */
|
|
117
|
-
function resolveProblemFiles(prompt: string, manifest: FileManifest): string[] {
|
|
118
|
-
const named = resolveByComponentName(prompt, manifest);
|
|
119
|
-
if (named.length > 0) return named;
|
|
120
|
-
return [...filesByType(manifest, "component"), ...filesByType(manifest, "page")];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// --- pattern-driven intent analysis -------------------------------------
|
|
124
|
-
|
|
125
|
-
interface IntentPattern {
|
|
126
|
-
patterns: RegExp[];
|
|
127
|
-
type: EditType;
|
|
128
|
-
fileResolver: (prompt: string, manifest: FileManifest) => string[];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Order matters: first matching pattern wins.
|
|
132
|
-
const INTENT_PATTERNS: readonly IntentPattern[] = [
|
|
133
|
-
{
|
|
134
|
-
patterns: [/\b(rebuild|recreate|start over|from scratch)\b/i],
|
|
135
|
-
type: EditType.FULL_REBUILD,
|
|
136
|
-
fileResolver: (_p, m) => allFiles(m),
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
patterns: [/\b(fix|bug|broken|error|issue|not working|crash)\b/i],
|
|
140
|
-
type: EditType.FIX_ISSUE,
|
|
141
|
-
fileResolver: resolveProblemFiles,
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
patterns: [
|
|
145
|
-
/\b(install|add)\b.*\b(package|dependency|dependencies|library|npm|module)\b/i,
|
|
146
|
-
/\b(package|dependency|dependencies)\b.*\b(install|add)\b/i,
|
|
147
|
-
],
|
|
148
|
-
type: EditType.ADD_DEPENDENCY,
|
|
149
|
-
fileResolver: resolveConfigFiles,
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
patterns: [/\b(color|colour|theme|style|styling|css|font|spacing|layout look)\b/i],
|
|
153
|
-
type: EditType.UPDATE_STYLE,
|
|
154
|
-
fileResolver: resolveStyleFiles,
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
patterns: [/\b(refactor|clean ?up|reorganize|restructure|simplify)\b/i],
|
|
158
|
-
type: EditType.REFACTOR,
|
|
159
|
-
fileResolver: resolveByComponentName,
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
patterns: [/\b(add|create|implement|introduce)\b.*\b(feature|page|section|flow)\b/i],
|
|
163
|
-
type: EditType.ADD_FEATURE,
|
|
164
|
-
fileResolver: resolveFeatureInsertionPoints,
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
patterns: [/\b(update|change|modify|edit|adjust|tweak)\b/i],
|
|
168
|
-
type: EditType.UPDATE_COMPONENT,
|
|
169
|
-
fileResolver: resolveByComponentName,
|
|
170
|
-
},
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
export function analyzeEditIntent(prompt: string, manifest: FileManifest): EditIntent {
|
|
174
|
-
const description = prompt.trim();
|
|
175
|
-
for (const pattern of INTENT_PATTERNS) {
|
|
176
|
-
if (pattern.patterns.some((re) => re.test(prompt))) {
|
|
177
|
-
return {
|
|
178
|
-
type: pattern.type,
|
|
179
|
-
targetFiles: [...pattern.fileResolver(prompt, manifest)],
|
|
180
|
-
description,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return {
|
|
185
|
-
type: EditType.UPDATE_COMPONENT,
|
|
186
|
-
targetFiles: [],
|
|
187
|
-
description,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// --- targeted context selection -----------------------------------------
|
|
192
|
-
|
|
193
|
-
const EDIT_INSTRUCTIONS: Readonly<Record<EditType, string>> = {
|
|
194
|
-
[EditType.UPDATE_COMPONENT]:
|
|
195
|
-
"Modify only the targeted component(s). Preserve unrelated logic, props, and exports.",
|
|
196
|
-
[EditType.ADD_FEATURE]:
|
|
197
|
-
"Introduce the new feature with minimal disruption. Wire it into existing entry/page files.",
|
|
198
|
-
[EditType.FIX_ISSUE]:
|
|
199
|
-
"Diagnose and fix the defect. Make the smallest correct change; do not refactor unrelated code.",
|
|
200
|
-
[EditType.REFACTOR]:
|
|
201
|
-
"Improve internal structure without changing observable behavior or public API.",
|
|
202
|
-
[EditType.FULL_REBUILD]:
|
|
203
|
-
"Regenerate the project. Honor the original intent; you may restructure freely.",
|
|
204
|
-
[EditType.UPDATE_STYLE]:
|
|
205
|
-
"Change styling tokens/rules only. Do not alter component logic or markup structure.",
|
|
206
|
-
[EditType.ADD_DEPENDENCY]:
|
|
207
|
-
"Add the dependency to the manifest and the minimal wiring required to use it.",
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
export function buildEditInstructions(type: EditType): string {
|
|
211
|
-
return EDIT_INSTRUCTIONS[type];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function isKeyFile(path: string): boolean {
|
|
215
|
-
return /(^|\/)App\.[tj]sx?$/.test(path) || /(^|\/)package\.json$/.test(path);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function buildFileStructureSection(manifest: FileManifest): string {
|
|
219
|
-
const lines = allFiles(manifest)
|
|
220
|
-
.slice()
|
|
221
|
-
.sort()
|
|
222
|
-
.map((p) => `- ${p} (${manifest.files[p]?.type ?? "other"})`);
|
|
223
|
-
return `File structure:\n${lines.join("\n")}`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export function selectFilesForEdit(prompt: string, manifest: FileManifest): FileContext {
|
|
227
|
-
const editIntent = analyzeEditIntent(prompt, manifest);
|
|
228
|
-
const everything = allFiles(manifest);
|
|
229
|
-
|
|
230
|
-
if (editIntent.type === EditType.FULL_REBUILD) {
|
|
231
|
-
const systemPrompt = buildEditInstructions(EditType.FULL_REBUILD);
|
|
232
|
-
return {
|
|
233
|
-
primaryFiles: [...everything],
|
|
234
|
-
contextFiles: [],
|
|
235
|
-
systemPrompt,
|
|
236
|
-
editIntent,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const primarySet = new Set(editIntent.targetFiles);
|
|
241
|
-
const primaryFiles = editIntent.targetFiles.slice().sort();
|
|
242
|
-
|
|
243
|
-
const contextSet = new Set(everything.filter((p) => !primarySet.has(p)));
|
|
244
|
-
for (const p of everything) {
|
|
245
|
-
if (isKeyFile(p) && !primarySet.has(p)) {
|
|
246
|
-
contextSet.add(p);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
const contextFiles = Array.from(contextSet).sort();
|
|
250
|
-
|
|
251
|
-
const systemPrompt = [
|
|
252
|
-
buildFileStructureSection(manifest),
|
|
253
|
-
"",
|
|
254
|
-
buildEditInstructions(editIntent.type),
|
|
255
|
-
].join("\n");
|
|
256
|
-
|
|
257
|
-
return { primaryFiles, contextFiles, systemPrompt, editIntent };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// --- fast-apply ----------------------------------------------------------
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Terse edit-apply format. A model emits zero or more blocks of the form:
|
|
264
|
-
*
|
|
265
|
-
* <<<edit file="src/App.tsx">>>
|
|
266
|
-
* <natural-language instructions describing the change>
|
|
267
|
-
* ---
|
|
268
|
-
* <the literal updated/replacement snippet>
|
|
269
|
-
* <<<end>>>
|
|
270
|
-
*
|
|
271
|
-
* Text outside well-formed blocks is ignored; malformed blocks are skipped.
|
|
272
|
-
*/
|
|
273
|
-
const EDIT_BLOCK_RE = /<<<edit\s+file="([^"]+)">>>\s*([\s\S]*?)\s*---\s*([\s\S]*?)\s*<<<end>>>/g;
|
|
274
|
-
|
|
275
|
-
export function parseEditBlocks(text: string): EditBlock[] {
|
|
276
|
-
const blocks: EditBlock[] = [];
|
|
277
|
-
for (const match of text.matchAll(EDIT_BLOCK_RE)) {
|
|
278
|
-
const targetFile = match[1]?.trim() ?? "";
|
|
279
|
-
const instructions = match[2]?.trim() ?? "";
|
|
280
|
-
const update = match[3]?.trim() ?? "";
|
|
281
|
-
if (targetFile.length === 0) continue;
|
|
282
|
-
blocks.push({ targetFile, instructions, update });
|
|
283
|
-
}
|
|
284
|
-
return blocks;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* A trivial deterministic fallback merger: replaces the original content
|
|
289
|
-
* wholesale with the update. Real callers inject an LLM-backed merger.
|
|
290
|
-
*/
|
|
291
|
-
export const fallbackMerger: EditMerger = async (_originalContent, update) => update;
|
|
292
|
-
|
|
293
|
-
export async function applyEditBlock(
|
|
294
|
-
originalContent: string,
|
|
295
|
-
block: EditBlock,
|
|
296
|
-
merge: EditMerger = fallbackMerger,
|
|
297
|
-
): Promise<ApplyEditResult> {
|
|
298
|
-
try {
|
|
299
|
-
const merged = await merge(originalContent, block.update, block.instructions);
|
|
300
|
-
return { ok: true, merged };
|
|
301
|
-
} catch (error) {
|
|
302
|
-
return {
|
|
303
|
-
ok: false,
|
|
304
|
-
error: error instanceof Error ? error.message : String(error),
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// --- tenant-scoped entry point ------------------------------------------
|
|
310
|
-
|
|
311
|
-
function assertTenant(tenantId: string): void {
|
|
312
|
-
if (typeof tenantId !== "string" || tenantId.trim().length === 0) {
|
|
313
|
-
throw new Error("edit-planner: tenantId is required (fail-closed)");
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export async function planEdit(
|
|
318
|
-
tenantId: string,
|
|
319
|
-
prompt: string,
|
|
320
|
-
manifest: FileManifest,
|
|
321
|
-
): Promise<FileContext> {
|
|
322
|
-
assertTenant(tenantId);
|
|
323
|
-
const parsed = fileManifestSchema.parse(manifest);
|
|
324
|
-
return selectFilesForEdit(prompt, parsed);
|
|
325
|
-
}
|