@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.
Files changed (137) hide show
  1. package/LICENSE +21 -676
  2. package/README.md +2 -0
  3. package/dist/adapters/dispatcher-sse.js +1 -0
  4. package/dist/adapters/index.d.ts +24 -5
  5. package/dist/adapters/index.js +28 -0
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/adapters/mcp-catalog.js +1 -0
  8. package/dist/adapters/prisma-rollout.js +1 -0
  9. package/dist/chunk-424PT5DM.js +23 -0
  10. package/dist/chunk-424PT5DM.js.map +1 -0
  11. package/dist/{chunk-NN7DATXA.js → chunk-4Y25ZTKI.js} +3 -3
  12. package/dist/chunk-4Y25ZTKI.js.map +1 -0
  13. package/dist/{chunk-BJBBR3QA.js → chunk-D4YAPLOW.js} +4 -4
  14. package/dist/chunk-D4YAPLOW.js.map +1 -0
  15. package/dist/{chunk-ZMYX5VBU.js → chunk-GQZKYWFT.js} +23 -7
  16. package/dist/chunk-GQZKYWFT.js.map +1 -0
  17. package/dist/chunk-KCNN4QUQ.js +255 -0
  18. package/dist/chunk-KCNN4QUQ.js.map +1 -0
  19. package/dist/{chunk-PGGWSUTM.js → chunk-NI4EDT4T.js} +2 -2
  20. package/dist/chunk-NI4EDT4T.js.map +1 -0
  21. package/dist/chunk-Q62VKHIT.js +178 -0
  22. package/dist/chunk-Q62VKHIT.js.map +1 -0
  23. package/dist/{chunk-MUF7ZZTO.js → chunk-R5HOSQUW.js} +2 -1
  24. package/dist/{chunk-MUF7ZZTO.js.map → chunk-R5HOSQUW.js.map} +1 -1
  25. package/dist/{chunk-5N4644PB.js → chunk-SD2ZJ7XG.js} +4 -4
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +52 -0
  28. package/dist/cli.js.map +1 -0
  29. package/dist/commands.js +1 -0
  30. package/dist/definitions.js +1 -0
  31. package/dist/dispatcher.js +1 -0
  32. package/dist/durable-turn.js +3 -2
  33. package/dist/hook-pipeline.js +1 -0
  34. package/dist/index.d.ts +8 -85
  35. package/dist/index.js +249 -134
  36. package/dist/index.js.map +1 -1
  37. package/dist/loop.d.ts +2 -2
  38. package/dist/loop.js +3 -2
  39. package/dist/mcp-bridge.d.ts +3 -3
  40. package/dist/mcp-bridge.js +3 -2
  41. package/dist/model.js +1 -0
  42. package/dist/orchestration.d.ts +84 -0
  43. package/dist/orchestration.js +16 -0
  44. package/dist/orchestration.js.map +1 -0
  45. package/dist/policy.js +1 -0
  46. package/dist/protocol.js +1 -0
  47. package/dist/pulsar.d.ts +78 -0
  48. package/dist/pulsar.js +17 -0
  49. package/dist/pulsar.js.map +1 -0
  50. package/dist/rollout-store-persistent.js +1 -0
  51. package/dist/rollout.js +1 -0
  52. package/dist/sandbox.js +2 -1
  53. package/dist/skills.js +2 -1
  54. package/dist/subagents.js +1 -0
  55. package/dist/tools.d.ts +4 -3
  56. package/dist/tools.js +5 -3
  57. package/package.json +84 -27
  58. package/.turbo/turbo-build.log +0 -115
  59. package/.turbo/turbo-test.log +0 -44
  60. package/.turbo/turbo-typecheck.log +0 -4
  61. package/CHANGELOG.md +0 -253
  62. package/dist/chunk-BJBBR3QA.js.map +0 -1
  63. package/dist/chunk-NN7DATXA.js.map +0 -1
  64. package/dist/chunk-PGGWSUTM.js.map +0 -1
  65. package/dist/chunk-ZMYX5VBU.js.map +0 -1
  66. package/src/adapters/dispatcher-sse.test.ts +0 -218
  67. package/src/adapters/dispatcher-sse.ts +0 -222
  68. package/src/adapters/index.ts +0 -18
  69. package/src/adapters/mcp-catalog.test.ts +0 -213
  70. package/src/adapters/mcp-catalog.ts +0 -188
  71. package/src/adapters/prisma-rollout.test.ts +0 -153
  72. package/src/adapters/prisma-rollout.ts +0 -104
  73. package/src/agent-runtime.test.ts +0 -176
  74. package/src/artifact-stream.test.ts +0 -330
  75. package/src/artifact-stream.ts +0 -453
  76. package/src/channel-gateway.test.ts +0 -432
  77. package/src/channel-gateway.ts +0 -357
  78. package/src/code-review.test.ts +0 -501
  79. package/src/code-review.ts +0 -495
  80. package/src/command-suggestions.test.ts +0 -251
  81. package/src/command-suggestions.ts +0 -338
  82. package/src/commands.test.ts +0 -184
  83. package/src/commands.ts +0 -140
  84. package/src/commit-message.test.ts +0 -249
  85. package/src/commit-message.ts +0 -180
  86. package/src/context-compaction.test.ts +0 -522
  87. package/src/context-compaction.ts +0 -434
  88. package/src/definitions.test.ts +0 -78
  89. package/src/definitions.ts +0 -190
  90. package/src/deployment-status.test.ts +0 -215
  91. package/src/deployment-status.ts +0 -227
  92. package/src/design-context.test.ts +0 -195
  93. package/src/design-context.ts +0 -198
  94. package/src/dispatcher.test.ts +0 -234
  95. package/src/dispatcher.ts +0 -189
  96. package/src/durable-turn.test.ts +0 -209
  97. package/src/durable-turn.ts +0 -135
  98. package/src/edit-planner.test.ts +0 -204
  99. package/src/edit-planner.ts +0 -325
  100. package/src/fuzzy-match.test.ts +0 -311
  101. package/src/fuzzy-match.ts +0 -444
  102. package/src/hook-pipeline.test.ts +0 -279
  103. package/src/hook-pipeline.ts +0 -373
  104. package/src/inbound-admission.test.ts +0 -394
  105. package/src/inbound-admission.ts +0 -246
  106. package/src/index.ts +0 -47
  107. package/src/loop.test.ts +0 -161
  108. package/src/loop.ts +0 -211
  109. package/src/mcp-bridge.test.ts +0 -165
  110. package/src/mcp-bridge.ts +0 -76
  111. package/src/memory-provider.test.ts +0 -232
  112. package/src/memory-provider.ts +0 -257
  113. package/src/model.ts +0 -168
  114. package/src/permission-ruleset.test.ts +0 -301
  115. package/src/permission-ruleset.ts +0 -200
  116. package/src/policy.ts +0 -151
  117. package/src/project-repo.test.ts +0 -232
  118. package/src/project-repo.ts +0 -311
  119. package/src/protocol.ts +0 -159
  120. package/src/rollout-store-persistent.test.ts +0 -217
  121. package/src/rollout-store-persistent.ts +0 -166
  122. package/src/rollout.ts +0 -150
  123. package/src/sandbox.ts +0 -113
  124. package/src/session-share.test.ts +0 -360
  125. package/src/session-share.ts +0 -310
  126. package/src/skill-distillation.test.ts +0 -177
  127. package/src/skill-distillation.ts +0 -369
  128. package/src/skills.test.ts +0 -277
  129. package/src/skills.ts +0 -255
  130. package/src/subagents.test.ts +0 -290
  131. package/src/subagents.ts +0 -332
  132. package/src/tools.ts +0 -126
  133. package/src/workbench.test.ts +0 -0
  134. package/src/workbench.ts +0 -0
  135. package/tsconfig.json +0 -12
  136. package/tsup.config.ts +0 -33
  137. /package/dist/{chunk-5N4644PB.js.map → chunk-SD2ZJ7XG.js.map} +0 -0
@@ -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
- });
@@ -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
- }