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.
- package/Readme.md +29 -20
- package/SECURITY.md +18 -14
- package/dist/agents/direct/generation-client.d.ts +16 -5
- package/dist/agents/direct/generation-client.d.ts.map +1 -1
- package/dist/agents/direct/generation-client.js +9 -4
- package/dist/agents/direct/generation-client.js.map +1 -1
- package/dist/agents/direct/index.d.ts +1 -0
- package/dist/agents/direct/index.d.ts.map +1 -1
- package/dist/agents/direct/index.js +1 -0
- package/dist/agents/direct/index.js.map +1 -1
- package/dist/agents/direct/integration-validation.d.ts +38 -15
- package/dist/agents/direct/integration-validation.d.ts.map +1 -1
- package/dist/agents/direct/integration-validation.js +305 -207
- package/dist/agents/direct/integration-validation.js.map +1 -1
- package/dist/agents/direct/language-relationships.d.ts +11 -0
- package/dist/agents/direct/language-relationships.d.ts.map +1 -0
- package/dist/agents/direct/language-relationships.js +55 -0
- package/dist/agents/direct/language-relationships.js.map +1 -0
- package/dist/agents/direct/presentation.d.ts +2 -1
- package/dist/agents/direct/presentation.d.ts.map +1 -1
- package/dist/agents/direct/presentation.js +5 -3
- package/dist/agents/direct/presentation.js.map +1 -1
- package/dist/agents/direct/project-contracts.d.ts.map +1 -1
- package/dist/agents/direct/project-contracts.js +44 -0
- package/dist/agents/direct/project-contracts.js.map +1 -1
- package/dist/agents/direct/project-memory.d.ts +2 -1
- package/dist/agents/direct/project-memory.d.ts.map +1 -1
- package/dist/agents/direct/project-memory.js +11 -59
- package/dist/agents/direct/project-memory.js.map +1 -1
- package/dist/agents/direct/staged-validation.d.ts +2 -0
- package/dist/agents/direct/staged-validation.d.ts.map +1 -1
- package/dist/agents/direct/staged-validation.js +22 -1
- package/dist/agents/direct/staged-validation.js.map +1 -1
- package/dist/agents/direct/types.d.ts +8 -0
- package/dist/agents/direct/types.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +38 -15
- package/dist/cli.js.map +1 -1
- package/dist/prompts/direct-conversion.d.ts.map +1 -1
- package/dist/prompts/direct-conversion.js +4 -3
- package/dist/prompts/direct-conversion.js.map +1 -1
- package/dist/prompts/direct-integration.d.ts +28 -8
- package/dist/prompts/direct-integration.d.ts.map +1 -1
- package/dist/prompts/direct-integration.js +51 -17
- package/dist/prompts/direct-integration.js.map +1 -1
- package/dist/prompts/direct-repair.d.ts +2 -0
- package/dist/prompts/direct-repair.d.ts.map +1 -1
- package/dist/prompts/direct-repair.js +4 -0
- package/dist/prompts/direct-repair.js.map +1 -1
- package/docs/ARCHITECTURE.md +8 -2
- package/docs/MODULES.md +4 -3
- package/docs/SCALABILITY.md +9 -7
- package/docs/roadmap/html-css.md +4 -5
- package/package.json +1 -1
|
@@ -1,225 +1,289 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
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
|
|
23
|
-
|
|
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
|
|
26
|
-
|
|
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
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
+
parsed = JSON.parse(output);
|
|
73
41
|
}
|
|
74
42
|
catch {
|
|
75
|
-
|
|
43
|
+
throw new Error("Integration auditor returned invalid JSON.");
|
|
76
44
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
87
|
+
return { status: parsed.status, issues };
|
|
100
88
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
return true;
|
|
174
|
+
rendered.push(block);
|
|
175
|
+
remaining -= block.length;
|
|
156
176
|
}
|
|
157
|
-
return
|
|
177
|
+
return rendered.length > 0 ? rendered.join("\n\n") : undefined;
|
|
158
178
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
194
|
-
|
|
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
|
|
197
|
-
const
|
|
198
|
-
if (!
|
|
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
|
|
201
|
-
|
|
234
|
+
relatedFiles.push({ path, content: related.code });
|
|
235
|
+
remaining -= related.code.length;
|
|
202
236
|
}
|
|
203
237
|
return {
|
|
204
|
-
unit
|
|
205
|
-
targetPath: targetPath(
|
|
206
|
-
instruction:
|
|
207
|
-
currentCode,
|
|
238
|
+
unit,
|
|
239
|
+
targetPath: targetPath(unit),
|
|
240
|
+
instruction: unit.prompt,
|
|
241
|
+
currentCode: item.code,
|
|
208
242
|
issues,
|
|
209
243
|
relatedFiles,
|
|
210
|
-
...(
|
|
244
|
+
...(projectBlock ? { projectMemory: projectBlock } : {}),
|
|
211
245
|
};
|
|
212
246
|
}
|
|
213
|
-
|
|
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 =
|
|
218
|
-
const
|
|
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
|
-
|
|
282
|
+
const repairedUnits = new Set();
|
|
283
|
+
let auditRequests = 0;
|
|
284
|
+
let repairRequests = 0;
|
|
221
285
|
const rejectGroup = (group, reason) => {
|
|
222
|
-
for (const unit of
|
|
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
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
276
|
-
|
|
361
|
+
repaired = false;
|
|
362
|
+
break;
|
|
277
363
|
}
|
|
278
364
|
}
|
|
279
|
-
if (!
|
|
280
|
-
rejectGroup(group,
|
|
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,
|
|
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
|