human-to-code 0.1.23 → 0.1.24

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 (54) hide show
  1. package/Readme.md +29 -20
  2. package/SECURITY.md +18 -14
  3. package/dist/agents/direct/generation-client.d.ts +16 -5
  4. package/dist/agents/direct/generation-client.d.ts.map +1 -1
  5. package/dist/agents/direct/generation-client.js +9 -4
  6. package/dist/agents/direct/generation-client.js.map +1 -1
  7. package/dist/agents/direct/index.d.ts +1 -0
  8. package/dist/agents/direct/index.d.ts.map +1 -1
  9. package/dist/agents/direct/index.js +1 -0
  10. package/dist/agents/direct/index.js.map +1 -1
  11. package/dist/agents/direct/integration-validation.d.ts +38 -15
  12. package/dist/agents/direct/integration-validation.d.ts.map +1 -1
  13. package/dist/agents/direct/integration-validation.js +305 -207
  14. package/dist/agents/direct/integration-validation.js.map +1 -1
  15. package/dist/agents/direct/language-relationships.d.ts +11 -0
  16. package/dist/agents/direct/language-relationships.d.ts.map +1 -0
  17. package/dist/agents/direct/language-relationships.js +55 -0
  18. package/dist/agents/direct/language-relationships.js.map +1 -0
  19. package/dist/agents/direct/presentation.d.ts +2 -1
  20. package/dist/agents/direct/presentation.d.ts.map +1 -1
  21. package/dist/agents/direct/presentation.js +5 -3
  22. package/dist/agents/direct/presentation.js.map +1 -1
  23. package/dist/agents/direct/project-contracts.d.ts.map +1 -1
  24. package/dist/agents/direct/project-contracts.js +44 -0
  25. package/dist/agents/direct/project-contracts.js.map +1 -1
  26. package/dist/agents/direct/project-memory.d.ts +2 -1
  27. package/dist/agents/direct/project-memory.d.ts.map +1 -1
  28. package/dist/agents/direct/project-memory.js +11 -59
  29. package/dist/agents/direct/project-memory.js.map +1 -1
  30. package/dist/agents/direct/staged-validation.d.ts +2 -0
  31. package/dist/agents/direct/staged-validation.d.ts.map +1 -1
  32. package/dist/agents/direct/staged-validation.js +22 -1
  33. package/dist/agents/direct/staged-validation.js.map +1 -1
  34. package/dist/agents/direct/types.d.ts +8 -0
  35. package/dist/agents/direct/types.d.ts.map +1 -1
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +38 -15
  38. package/dist/cli.js.map +1 -1
  39. package/dist/prompts/direct-conversion.d.ts.map +1 -1
  40. package/dist/prompts/direct-conversion.js +4 -3
  41. package/dist/prompts/direct-conversion.js.map +1 -1
  42. package/dist/prompts/direct-integration.d.ts +28 -8
  43. package/dist/prompts/direct-integration.d.ts.map +1 -1
  44. package/dist/prompts/direct-integration.js +51 -17
  45. package/dist/prompts/direct-integration.js.map +1 -1
  46. package/dist/prompts/direct-repair.d.ts +2 -0
  47. package/dist/prompts/direct-repair.d.ts.map +1 -1
  48. package/dist/prompts/direct-repair.js +4 -0
  49. package/dist/prompts/direct-repair.js.map +1 -1
  50. package/docs/ARCHITECTURE.md +8 -2
  51. package/docs/MODULES.md +4 -3
  52. package/docs/SCALABILITY.md +9 -7
  53. package/docs/roadmap/html-css.md +4 -5
  54. package/package.json +1 -1
@@ -1,225 +1,289 @@
1
1
  /**
2
- * Opt-in post-generation checks for relationships that a compiler cannot
3
- * validate. The first contract covers static-web entry documents: generated
4
- * HTML must reference conventional generated CSS/browser-JavaScript companions
5
- * in the same directory. Consistent groups issue no request; a detected gap
6
- * permits one bounded whole-file reconciliation and otherwise fails closed.
2
+ * Optional, bounded, cross-language integration auditing. ProjectMemory owns
3
+ * relationship discovery; this module is language-agnostic orchestration over
4
+ * generated paths, compact contracts, a strict JSON audit, target-scoped
5
+ * repairs, and one verification audit. No application/framework scenario is
6
+ * embedded here.
7
7
  */
8
8
  import { posix } from "node:path";
9
9
  import { ContextSecurityError, scanSecrets } from "../../context/context.js";
10
10
  import { validateGeneratedUnit } from "./candidate-validation.js";
11
+ import { compactFileContract } from "./project-contracts.js";
11
12
  const DEFAULT_CONTEXT_CHAR_BUDGET = 48_000;
12
- const HTML_EXTENSIONS = new Set([".html", ".htm"]);
13
- const BROWSER_SCRIPT_EXTENSIONS = new Set([".js", ".mjs"]);
14
- const RAW_OR_INERT_HTML_ELEMENTS = new Set([
15
- "iframe", "noembed", "noframes", "script", "style", "template", "textarea", "title", "xmp",
16
- ]);
17
- const CONVENTIONAL_BROWSER_SCRIPT = /^(?:app|client|frontend|index|main|page|script|ui)(?:[.-].*)?\.(?:js|mjs)$/iu;
18
- const BROWSER_PURPOSE = /\b(?:browser|button|calculator|click|document|dom|event|form|html|page|screen|ui|window)\b/iu;
13
+ const MAX_COMPONENT_FILES = 24;
14
+ const MAX_NEIGHBORHOOD_FILES = 17;
15
+ const MAX_CONTRACT_CHARS = 2_000;
16
+ const MAX_PURPOSE_CHARS = 400;
17
+ const MAX_AUDIT_ISSUES = 64;
18
+ const MAX_ISSUE_MESSAGE_CHARS = 600;
19
19
  function targetPath(unit) {
20
20
  return unit.kind === "file" ? unit.outputPath : unit.sourcePath;
21
21
  }
22
- function extension(path) {
23
- return posix.extname(path).toLowerCase();
22
+ function oneLine(value, limit) {
23
+ const clean = value
24
+ .replace(/[\u0000-\u0008\u000b-\u001f\u007f]/gu, " ")
25
+ .replace(/\s+/gu, " ")
26
+ .trim();
27
+ return clean.length <= limit ? clean : `${clean.slice(0, Math.max(0, limit - 1))}…`;
24
28
  }
25
- function isWholeHtml(unit) {
26
- return unit.kind === "file" && HTML_EXTENSIONS.has(extension(targetPath(unit)));
29
+ function ownKeys(value, expected) {
30
+ const actual = Object.keys(value).sort();
31
+ return actual.length === expected.length && actual.every((key, index) => key === [...expected].sort()[index]);
27
32
  }
28
- function isStylesheetCompanion(unit) {
29
- if (unit.kind !== "file" || extension(targetPath(unit)) !== ".css")
30
- return false;
31
- return !/(?:^|[.-])(?:module|spec|test)(?:[.-]|\.css$)/iu.test(posix.basename(targetPath(unit)));
33
+ function object(value) {
34
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
35
  }
33
- function isBrowserScriptCompanion(unit) {
34
- if (unit.kind !== "file" || !BROWSER_SCRIPT_EXTENSIONS.has(extension(targetPath(unit))))
35
- return false;
36
- const name = posix.basename(targetPath(unit));
37
- if (/(?:^|[.-])(?:config|server|spec|test)(?:[.-]|\.(?:js|mjs)$)/iu.test(name))
38
- return false;
39
- return CONVENTIONAL_BROWSER_SCRIPT.test(name) || BROWSER_PURPOSE.test(unit.prompt);
40
- }
41
- function webIntegrationGroups(units) {
42
- const files = units.filter((unit) => unit.kind === "file");
43
- return files.filter(isWholeHtml).flatMap((html) => {
44
- const directory = posix.dirname(targetPath(html));
45
- const related = files.filter((unit) => unit !== html
46
- && posix.dirname(targetPath(unit)) === directory
47
- && (isStylesheetCompanion(unit) || isBrowserScriptCompanion(unit)));
48
- return related.length > 0 ? [{ html, related }] : [];
49
- });
50
- }
51
- /** Maximum integration requests disclosed before an opt-in run. */
52
- export function potentialIntegrationRequests(units) {
53
- return webIntegrationGroups(units).length;
54
- }
55
- function requiredReference(from, to) {
56
- return posix.relative(posix.dirname(from), to) || posix.basename(to);
57
- }
58
- function attribute(tag, name) {
59
- const pattern = new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'=<>\u0060]+))`, "iu");
60
- const match = pattern.exec(tag);
61
- return match?.[1] ?? match?.[2] ?? match?.[3];
62
- }
63
- function resolveLocalReference(from, reference) {
64
- const trimmed = reference.trim();
65
- if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.startsWith("//"))
66
- return undefined;
67
- if (/^[a-z][a-z0-9+.-]*:/iu.test(trimmed))
68
- return undefined;
69
- const withoutSuffix = trimmed.split(/[?#]/u, 1)[0] ?? "";
70
- let decoded;
36
+ /** Strictly validate the model's audit JSON against the exact generated group. */
37
+ export function parseIntegrationAuditOutput(output, allowedPaths) {
38
+ let parsed;
71
39
  try {
72
- decoded = decodeURIComponent(withoutSuffix);
40
+ parsed = JSON.parse(output);
73
41
  }
74
42
  catch {
75
- decoded = withoutSuffix;
43
+ throw new Error("Integration auditor returned invalid JSON.");
76
44
  }
77
- const resolved = decoded.startsWith("/")
78
- ? posix.normalize(decoded.slice(1))
79
- : posix.normalize(posix.join(posix.dirname(from), decoded));
80
- if (resolved === ".." || resolved.startsWith("../"))
81
- return undefined;
82
- return resolved.replace(/^\.\//u, "");
83
- }
84
- function tagEnd(html, start) {
85
- let quote;
86
- for (let index = start + 1; index < html.length; index += 1) {
87
- const char = html[index];
88
- if (quote !== undefined) {
89
- if (char === quote)
90
- quote = undefined;
45
+ if (!object(parsed) || !ownKeys(parsed, ["issues", "status"])) {
46
+ throw new Error("Integration audit must contain exactly status and issues.");
47
+ }
48
+ if (parsed.status !== "consistent" && parsed.status !== "issues") {
49
+ throw new Error("Integration audit status must be consistent or issues.");
50
+ }
51
+ if (!Array.isArray(parsed.issues) || parsed.issues.length > MAX_AUDIT_ISSUES) {
52
+ throw new Error(`Integration audit issues must be an array with at most ${MAX_AUDIT_ISSUES} entries.`);
53
+ }
54
+ const issues = parsed.issues.map((value, index) => {
55
+ if (!object(value) || !ownKeys(value, ["code", "message", "relatedPaths", "targetPath"])) {
56
+ throw new Error(`Integration audit issue ${index} has unknown or missing fields.`);
57
+ }
58
+ if (typeof value.targetPath !== "string" || !allowedPaths.has(value.targetPath)) {
59
+ throw new Error(`Integration audit issue ${index} names an unknown targetPath.`);
60
+ }
61
+ if (!Array.isArray(value.relatedPaths) || value.relatedPaths.length === 0 || value.relatedPaths.length > 16) {
62
+ throw new Error(`Integration audit issue ${index} must name 1-16 relatedPaths.`);
63
+ }
64
+ const relatedPaths = value.relatedPaths.map((path) => {
65
+ if (typeof path !== "string" || path === value.targetPath || !allowedPaths.has(path)) {
66
+ throw new Error(`Integration audit issue ${index} names an unknown related path.`);
67
+ }
68
+ return path;
69
+ });
70
+ if (new Set(relatedPaths).size !== relatedPaths.length) {
71
+ throw new Error(`Integration audit issue ${index} repeats a related path.`);
91
72
  }
92
- else if (char === "\"" || char === "'") {
93
- quote = char;
73
+ if (typeof value.code !== "string" || !/^[A-Z][A-Z0-9_]{1,63}$/u.test(value.code)) {
74
+ throw new Error(`Integration audit issue ${index} has an invalid code.`);
94
75
  }
95
- else if (char === ">") {
96
- return index;
76
+ if (typeof value.message !== "string") {
77
+ throw new Error(`Integration audit issue ${index} has an invalid message.`);
97
78
  }
79
+ const message = oneLine(value.message, MAX_ISSUE_MESSAGE_CHARS);
80
+ if (message.length === 0)
81
+ throw new Error(`Integration audit issue ${index} has an empty message.`);
82
+ return { targetPath: value.targetPath, relatedPaths, code: value.code, message };
83
+ });
84
+ if (parsed.status === "consistent" && issues.length !== 0 || parsed.status === "issues" && issues.length === 0) {
85
+ throw new Error("Integration audit status contradicts its issues array.");
98
86
  }
99
- return html.length - 1;
87
+ return { status: parsed.status, issues };
100
88
  }
101
- /**
102
- * Extract active start tags without accepting examples inside HTML comments,
103
- * template contents, or JavaScript/CSS raw-text bodies as real references.
104
- */
105
- function htmlStartTags(html) {
106
- const tags = [];
107
- let offset = 0;
108
- while (offset < html.length) {
109
- const start = html.indexOf("<", offset);
110
- if (start === -1)
111
- break;
112
- if (html.startsWith("<!--", start)) {
113
- const end = html.indexOf("-->", start + 4);
114
- offset = end === -1 ? html.length : end + 3;
115
- continue;
89
+ function relationshipGroups(generated, projectMemory) {
90
+ const wholeFiles = generated.filter((item) => item.unit.kind === "file");
91
+ const byPath = new Map(wholeFiles.map((item) => [targetPath(item.unit), item.unit]));
92
+ const adjacency = new Map([...byPath.keys()].map((path) => [path, new Set()]));
93
+ const relationships = new Map();
94
+ for (const { unit } of wholeFiles) {
95
+ const fromPath = targetPath(unit);
96
+ const evidenced = projectMemory?.relationsFor?.(unit) ?? [];
97
+ for (const relationship of evidenced) {
98
+ if (!byPath.has(relationship.path) || relationship.path === fromPath)
99
+ continue;
100
+ adjacency.get(fromPath).add(relationship.path);
101
+ adjacency.get(relationship.path).add(fromPath);
102
+ const entry = {
103
+ fromPath,
104
+ toPath: relationship.path,
105
+ role: oneLine(relationship.role, 240),
106
+ reference: oneLine(relationship.reference, 240),
107
+ };
108
+ relationships.set(`${entry.fromPath}\0${entry.toPath}`, entry);
116
109
  }
117
- const end = tagEnd(html, start);
118
- const source = html.slice(start, end + 1);
119
- const match = /^<\s*([a-z][a-z0-9:-]*)\b/iu.exec(source);
120
- if (!match) {
121
- offset = end + 1;
110
+ }
111
+ const components = [];
112
+ const visited = new Set();
113
+ for (const start of [...byPath.keys()].sort()) {
114
+ if (visited.has(start) || adjacency.get(start).size === 0)
122
115
  continue;
116
+ const queue = [start];
117
+ const component = [];
118
+ visited.add(start);
119
+ while (queue.length > 0) {
120
+ const path = queue.shift();
121
+ component.push(path);
122
+ for (const next of adjacency.get(path) ?? []) {
123
+ if (visited.has(next))
124
+ continue;
125
+ visited.add(next);
126
+ queue.push(next);
127
+ }
123
128
  }
124
- const name = match[1].toLowerCase();
125
- tags.push({ name, source });
126
- if (RAW_OR_INERT_HTML_ELEMENTS.has(name) && !/\/\s*>$/u.test(source)) {
127
- const closing = new RegExp(`<\\/\\s*${name}\\s*>`, "giu");
128
- closing.lastIndex = end + 1;
129
- const close = closing.exec(html);
130
- offset = close === null ? html.length : close.index + close[0].length;
129
+ components.push(component.sort());
130
+ }
131
+ const pathGroups = [];
132
+ for (const component of components) {
133
+ if (component.length <= MAX_COMPONENT_FILES) {
134
+ pathGroups.push(component);
135
+ continue;
131
136
  }
132
- else {
133
- offset = end + 1;
137
+ const seen = new Set();
138
+ for (const focus of component) {
139
+ const neighborhood = [focus, ...[...(adjacency.get(focus) ?? [])].sort()]
140
+ .slice(0, MAX_NEIGHBORHOOD_FILES)
141
+ .sort();
142
+ const key = neighborhood.join("\0");
143
+ if (neighborhood.length < 2 || seen.has(key))
144
+ continue;
145
+ seen.add(key);
146
+ pathGroups.push(neighborhood);
134
147
  }
135
148
  }
136
- return tags;
149
+ return pathGroups.map((paths) => {
150
+ const pathSet = new Set(paths);
151
+ const groupRelationships = [...relationships.values()]
152
+ .filter((entry) => pathSet.has(entry.fromPath) && pathSet.has(entry.toPath))
153
+ .sort((left, right) => left.fromPath.localeCompare(right.fromPath) || left.toPath.localeCompare(right.toPath));
154
+ return { units: paths.map((path) => byPath.get(path)), relationships: groupRelationships };
155
+ });
137
156
  }
138
- function hasStylesheetReference(htmlPath, tags, stylesheetPath) {
139
- for (const tag of tags) {
140
- if (tag.name !== "link")
141
- continue;
142
- const rel = attribute(tag.source, "rel")?.toLowerCase().split(/\s+/u) ?? [];
143
- const href = attribute(tag.source, "href");
144
- if (rel.includes("stylesheet") && href !== undefined && resolveLocalReference(htmlPath, href) === stylesheetPath)
145
- return true;
146
- }
147
- return false;
157
+ /** Conservative pre-run ceiling: two audits and one repair per whole-file unit. */
158
+ export function potentialIntegrationRequests(units) {
159
+ const files = units.filter((unit) => unit.kind === "file").length;
160
+ return files < 2 ? { auditUpTo: 0, repairUpTo: 0 } : { auditUpTo: files * 2, repairUpTo: files };
148
161
  }
149
- function hasScriptReference(htmlPath, tags, scriptPath) {
150
- for (const tag of tags) {
151
- if (tag.name !== "script")
162
+ function groupProjectMemory(group, projectMemory, budget) {
163
+ if (!projectMemory || budget < 160)
164
+ return undefined;
165
+ const perUnit = Math.max(160, Math.floor(budget / Math.max(1, group.units.length)));
166
+ const rendered = [];
167
+ let remaining = budget;
168
+ for (const unit of group.units) {
169
+ if (remaining < 160)
170
+ break;
171
+ const block = projectMemory.renderFor(unit, Math.min(perUnit, remaining));
172
+ if (block.length === 0 || rendered.includes(block))
152
173
  continue;
153
- const source = attribute(tag.source, "src");
154
- if (source !== undefined && resolveLocalReference(htmlPath, source) === scriptPath)
155
- return true;
174
+ rendered.push(block);
175
+ remaining -= block.length;
156
176
  }
157
- return false;
177
+ return rendered.length > 0 ? rendered.join("\n\n") : undefined;
158
178
  }
159
- /** Check one complete HTML candidate against its known generated companions. */
160
- export function findHtmlIntegrationIssues(htmlPath, html, relatedUnits) {
161
- const issues = [];
162
- const tags = htmlStartTags(html);
163
- for (const unit of relatedUnits) {
164
- const relatedPath = targetPath(unit);
165
- const reference = requiredReference(htmlPath, relatedPath);
166
- if (isStylesheetCompanion(unit) && !hasStylesheetReference(htmlPath, tags, relatedPath)) {
167
- issues.push({
168
- code: "MISSING_STYLESHEET_REFERENCE",
169
- message: `${htmlPath} does not load its generated stylesheet companion ${relatedPath}.`,
170
- relatedPath,
171
- requiredReference: reference,
172
- });
173
- }
174
- else if (isBrowserScriptCompanion(unit) && !hasScriptReference(htmlPath, tags, relatedPath)) {
175
- issues.push({
176
- code: "MISSING_BROWSER_SCRIPT_REFERENCE",
177
- message: `${htmlPath} does not load its generated browser-script companion ${relatedPath}.`,
178
- relatedPath,
179
- requiredReference: reference,
180
- });
181
- }
179
+ function buildAuditRequest(group, byUnit, contextCharBudget, projectMemory) {
180
+ const baseFiles = group.units.map((unit) => {
181
+ const item = byUnit.get(unit);
182
+ const path = targetPath(unit);
183
+ const contract = compactFileContract(path, item.code).slice(0, MAX_CONTRACT_CHARS);
184
+ return {
185
+ path,
186
+ language: unit.language ?? (posix.extname(path).replace(/^\./u, "") || "unknown"),
187
+ instruction: oneLine(unit.prompt, MAX_PURPOSE_CHARS),
188
+ contract,
189
+ code: item.code,
190
+ };
191
+ });
192
+ const fixedChars = baseFiles.reduce((total, file) => total + file.path.length + file.language.length + file.instruction.length + file.contract.length + 120, 0)
193
+ + group.relationships.reduce((total, relation) => total + relation.fromPath.length + relation.toPath.length + relation.role.length + relation.reference.length + 40, 0);
194
+ if (fixedChars > contextCharBudget)
195
+ return undefined;
196
+ let remaining = contextCharBudget - fixedChars;
197
+ const projectBlock = groupProjectMemory(group, projectMemory, Math.floor(remaining / 3));
198
+ remaining -= projectBlock?.length ?? 0;
199
+ const files = [];
200
+ for (const file of baseFiles) {
201
+ const content = file.code.length <= remaining ? file.code : undefined;
202
+ files.push({
203
+ path: file.path,
204
+ language: file.language,
205
+ instruction: file.instruction,
206
+ contract: file.contract,
207
+ ...(content !== undefined ? { content } : {}),
208
+ });
209
+ if (content !== undefined)
210
+ remaining -= content.length;
182
211
  }
183
- return issues;
184
- }
185
- function describeIssues(issues) {
186
- return issues.map((issue) => `${issue.code} (${issue.requiredReference})`).join(", ");
212
+ return {
213
+ unit: group.units[0],
214
+ units: [...group.units],
215
+ files,
216
+ relationships: [...group.relationships],
217
+ ...(projectBlock ? { projectMemory: projectBlock } : {}),
218
+ };
187
219
  }
188
- function buildIntegrationRequest(group, currentCode, issues, byUnit, contextCharBudget, projectMemory) {
189
- const issueChars = issues.reduce((total, issue) => total + issue.message.length + issue.relatedPath.length + issue.requiredReference.length + 48, 0);
190
- let budget = contextCharBudget - currentCode.length - issueChars;
191
- if (budget < 0)
220
+ function buildRepairRequest(unit, issues, byUnit, contextCharBudget, projectMemory) {
221
+ const item = byUnit.get(unit);
222
+ const issueChars = issues.reduce((total, issue) => total + issue.code.length + issue.message.length + issue.relatedPaths.join("").length + 48, 0);
223
+ let remaining = contextCharBudget - item.code.length - issueChars;
224
+ if (remaining < 0)
192
225
  return undefined;
193
- const renderedProjectMemory = projectMemory?.renderFor(group.html, Math.floor(budget / 2));
194
- budget -= renderedProjectMemory?.length ?? 0;
226
+ const projectBlock = projectMemory?.renderFor(unit, Math.floor(remaining / 3));
227
+ remaining -= projectBlock?.length ?? 0;
228
+ const relatedPaths = [...new Set(issues.flatMap((issue) => issue.relatedPaths))].sort();
195
229
  const relatedFiles = [];
196
- for (const unit of group.related) {
197
- const item = byUnit.get(unit);
198
- if (!item || item.error !== undefined || item.code.length > budget)
230
+ for (const path of relatedPaths) {
231
+ const related = [...byUnit.values()].find((entry) => targetPath(entry.unit) === path);
232
+ if (!related || related.code.length > remaining)
199
233
  continue;
200
- relatedFiles.push({ path: targetPath(unit), content: item.code });
201
- budget -= item.code.length;
234
+ relatedFiles.push({ path, content: related.code });
235
+ remaining -= related.code.length;
202
236
  }
203
237
  return {
204
- unit: group.html,
205
- targetPath: targetPath(group.html),
206
- instruction: group.html.prompt,
207
- currentCode,
238
+ unit,
239
+ targetPath: targetPath(unit),
240
+ instruction: unit.prompt,
241
+ currentCode: item.code,
208
242
  issues,
209
243
  relatedFiles,
210
- ...(renderedProjectMemory ? { projectMemory: renderedProjectMemory } : {}),
244
+ ...(projectBlock ? { projectMemory: projectBlock } : {}),
211
245
  };
212
246
  }
213
- /** Run opt-in integration checks and bounded repair over generated candidates. */
247
+ function auditOutbound(request) {
248
+ return [
249
+ request.projectMemory ?? "",
250
+ ...request.files.flatMap((file) => [file.path, file.language, file.instruction, file.contract, file.content ?? ""]),
251
+ ...request.relationships.flatMap((relationship) => [
252
+ relationship.fromPath,
253
+ relationship.toPath,
254
+ relationship.role,
255
+ relationship.reference,
256
+ ]),
257
+ ].join("\n");
258
+ }
259
+ function repairOutbound(request) {
260
+ return [
261
+ request.targetPath,
262
+ request.instruction,
263
+ request.currentCode,
264
+ request.projectMemory ?? "",
265
+ ...request.issues.flatMap((issue) => [
266
+ issue.targetPath,
267
+ ...issue.relatedPaths,
268
+ issue.code,
269
+ issue.message,
270
+ ]),
271
+ ...request.relatedFiles.flatMap((file) => [file.path, file.content]),
272
+ ].join("\n");
273
+ }
274
+ /** Audit, repair, and verify generated relationship groups without writing. */
214
275
  export async function reconcileGeneratedIntegrations(generated, options = {}) {
215
276
  const results = generated.map((item) => ({ ...item }));
216
277
  const byUnit = new Map(results.map((item) => [item.unit, item]));
217
- const groups = webIntegrationGroups(results.map((item) => item.unit));
218
- const maxAttempts = Math.max(0, options.maxIntegrationAttemptsPerUnit ?? 1);
278
+ const groups = relationshipGroups(results, options.projectMemory);
279
+ const maxAuditPasses = Math.max(1, Math.min(2, options.maxAuditPassesPerGroup ?? 2));
280
+ const maxRepairs = Math.max(0, options.maxRepairAttemptsPerUnit ?? 1);
219
281
  const contextCharBudget = Math.max(0, options.contextCharBudget ?? DEFAULT_CONTEXT_CHAR_BUDGET);
220
- let integrationRequests = 0;
282
+ const repairedUnits = new Set();
283
+ let auditRequests = 0;
284
+ let repairRequests = 0;
221
285
  const rejectGroup = (group, reason) => {
222
- for (const unit of [group.html, ...group.related]) {
286
+ for (const unit of group.units) {
223
287
  const item = byUnit.get(unit);
224
288
  if (!item || item.error !== undefined)
225
289
  continue;
@@ -229,56 +293,90 @@ export async function reconcileGeneratedIntegrations(generated, options = {}) {
229
293
  }
230
294
  };
231
295
  for (const group of groups) {
232
- const groupItems = [group.html, ...group.related].map((unit) => byUnit.get(unit));
233
- const unavailable = groupItems.filter((item) => item.error !== undefined || item.code.trim().length === 0);
296
+ const unavailable = group.units.map((unit) => byUnit.get(unit))
297
+ .filter((item) => item.error !== undefined || item.code.trim().length === 0);
234
298
  if (unavailable.length > 0) {
235
299
  rejectGroup(group, `cross-file integration group is incomplete because ${unavailable.map((item) => item.unit.sourcePath).join(", ")} did not produce an applicable candidate`);
236
300
  continue;
237
301
  }
238
- const htmlItem = byUnit.get(group.html);
239
- let issues = findHtmlIntegrationIssues(targetPath(group.html), htmlItem.code, group.related);
240
- options.onProgress?.({ kind: "integration-check", unit: group.html, issues: issues.length });
241
- if (issues.length === 0)
302
+ if (!options.audit)
242
303
  continue;
243
- if (!options.integrate || maxAttempts === 0) {
244
- rejectGroup(group, `cross-file integration validation failed: ${describeIssues(issues)}`);
304
+ const runAudit = async (pass) => {
305
+ const request = buildAuditRequest(group, byUnit, contextCharBudget, options.projectMemory);
306
+ if (!request || scanSecrets(auditOutbound(request)).length > 0)
307
+ return undefined;
308
+ options.onProgress?.({ kind: "integration-audit", unit: request.unit, pass, files: request.files.length });
309
+ auditRequests += 1;
310
+ try {
311
+ return parseIntegrationAuditOutput(await options.audit(request), new Set(group.units.map(targetPath)));
312
+ }
313
+ catch (error) {
314
+ if (error instanceof ContextSecurityError)
315
+ throw error;
316
+ return undefined;
317
+ }
318
+ };
319
+ const initial = await runAudit(1);
320
+ if (!initial) {
321
+ rejectGroup(group, "cross-file integration audit failed or exceeded its safe bounded context");
322
+ continue;
323
+ }
324
+ if (initial.status === "consistent")
325
+ continue;
326
+ if (!options.repair || maxRepairs === 0) {
327
+ rejectGroup(group, `cross-file integration audit reported ${initial.issues.length} unresolved issue(s)`);
245
328
  continue;
246
329
  }
247
- let reconciled = false;
248
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
249
- const request = buildIntegrationRequest(group, htmlItem.code, issues, byUnit, contextCharBudget, options.projectMemory);
250
- if (!request)
330
+ let repaired = true;
331
+ const byTarget = new Map();
332
+ for (const issue of initial.issues)
333
+ byTarget.set(issue.targetPath, [...(byTarget.get(issue.targetPath) ?? []), issue]);
334
+ for (const [path, issues] of byTarget) {
335
+ const unit = group.units.find((candidate) => targetPath(candidate) === path);
336
+ if (repairedUnits.has(unit)) {
337
+ repaired = false;
251
338
  break;
252
- const outbound = [request.currentCode, request.projectMemory ?? "", ...request.relatedFiles.map((file) => file.content)].join("\n");
253
- if (scanSecrets(outbound).length > 0)
339
+ }
340
+ const request = buildRepairRequest(unit, issues, byUnit, contextCharBudget, options.projectMemory);
341
+ if (!request || scanSecrets(repairOutbound(request)).length > 0) {
342
+ repaired = false;
254
343
  break;
255
- options.onProgress?.({ kind: "integrate", unit: group.html, attempt, issues: issues.length });
256
- integrationRequests += 1;
344
+ }
345
+ options.onProgress?.({ kind: "integration-repair", unit, attempt: 1, issues: issues.length });
346
+ repairRequests += 1;
347
+ repairedUnits.add(unit);
257
348
  try {
258
- const integratedCode = await options.integrate(request);
259
- if (integratedCode.trim().length === 0 || integratedCode === htmlItem.code)
260
- continue;
261
- await validateGeneratedUnit(group.html, integratedCode);
262
- const remaining = findHtmlIntegrationIssues(targetPath(group.html), integratedCode, group.related);
263
- if (remaining.length > 0) {
264
- issues = remaining;
265
- continue;
349
+ const code = await options.repair(request);
350
+ if (code.trim().length === 0 || code === byUnit.get(unit).code) {
351
+ repaired = false;
352
+ break;
266
353
  }
267
- htmlItem.code = integratedCode;
268
- options.projectMemory?.remember(group.html, integratedCode);
269
- reconciled = true;
270
- break;
354
+ await validateGeneratedUnit(unit, code);
355
+ byUnit.get(unit).code = code;
356
+ options.projectMemory?.remember(unit, code);
271
357
  }
272
358
  catch (error) {
273
359
  if (error instanceof ContextSecurityError)
274
360
  throw error;
275
- // The request budget is consumed. A known-broken connected group is
276
- // rejected below instead of being written partially.
361
+ repaired = false;
362
+ break;
277
363
  }
278
364
  }
279
- if (!reconciled)
280
- rejectGroup(group, `cross-file integration validation failed after bounded reconciliation: ${describeIssues(issues)}`);
365
+ if (!repaired) {
366
+ rejectGroup(group, "cross-file integration repair failed within its bounded target budget");
367
+ continue;
368
+ }
369
+ if (maxAuditPasses < 2)
370
+ continue;
371
+ const verified = await runAudit(2);
372
+ if (!verified || verified.status !== "consistent") {
373
+ rejectGroup(group, "cross-file integration remained inconsistent after bounded repair and verification");
374
+ }
281
375
  }
282
- return { results, integrationRequests, checkedTargets: groups.length };
376
+ return { results, auditRequests, repairRequests, checkedGroups: groups.length };
377
+ }
378
+ /** Helper for tests and alternative ProjectMemory implementations. */
379
+ export function generatedRelationshipsFor(unit, projectMemory) {
380
+ return projectMemory.relationsFor?.(unit) ?? [];
283
381
  }
284
382
  //# sourceMappingURL=integration-validation.js.map