@tea-agent/loop-agent 0.39.0-beta.6 → 0.39.0-beta.8
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/CHANGELOG.md +14 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +44 -11
- package/dist/executors/pi-executor.js +7 -0
- package/dist/executors/shell-executor.js +1 -1
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/task/config-types.js +15 -0
- package/dist/task/frontend-project-capability.js +19 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CfDSkLtp.js → abnfDiagram-N423BO3Z-bPer1waP.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BlefM6I5.js → arc-BLdkwucW.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4tHfzHn.js → architectureDiagram-T3A2C74G-Bw4wo4I1.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-G-G77Rh1.js → blockDiagram-VBNYF7ZC-DUUkc0gr.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DWgxmY72.js → c4Diagram-5PPSVZJV-BC3O0nob.js} +1 -1
- package/dist/worker/console/static/assets/channel-AbFtVnFm.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CJQcfbCY.js → chunk-2GRJ4B5K-Dd4g2nti.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-vUPviH9y.js → chunk-2Q5K7J3B-s2EDm7-n.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DzRXSTeh.js → chunk-5RXB4S5H-GqtHM2yQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CWfgqm07.js → chunk-5VM5RSS4-BzRFrN8j.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BtTDauXA.js → chunk-6Q2QTUOP-Bw9nFjCJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bc-NNt84.js → chunk-GF5L2VYU-B4n-03Qm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-B_f-emL4.js → chunk-JWPE2WC7-Dt75ZdBD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DV-jgJ66.js → chunk-KBJHAD2P-B_afMhHB.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DMqzn4RZ.js → chunk-RYQCIY6F-Bd8Rqqg9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CVNtlAm4.js → chunk-XXDRQBXY-9boOMji4.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BBZBhVTd.js → cose-bilkent-JH36ORCC-BtCycfCc.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DbMU2PZ2.js → cynefin-VYW2F7L2-eTOts9L1.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dj0v8rjc.js → cynefinDiagram-MW4NZA55-Cibd2mjg.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BW8R4WvJ.js → dagre-VZM6K2ZE-CDjDZtDV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DEgWz3ld.js → diagram-7IWD3JNH-Btexn-JJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BAA01rlR.js → diagram-B4RE2ZJO-Cyl8sdpM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Cau6qqpW.js → diagram-LBJQPF4R-Ev-83E1t.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-8HiPsLos.js → diagram-Q27KOJAE-41LqE0Mc.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CmkXuTYL.js → diagram-UB23O5K3-BC39T5R3.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DCV_0seW.js → ebnfDiagram-BXEA7PRR-DzcNRuri.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-gckfWESj.js → erDiagram-JOGREHBK-B5Frvvnz.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D2mT2BUu.js → flowDiagram-UKHOOZJN-BapxROIM.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-B_HR5qEw.js → ganttDiagram-PKOTCBZU-Cd4d4wQ0.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BUw4fpSa.js → gitGraphDiagram-DS77QQ5N-CrL6buFW.js} +1 -1
- package/dist/worker/console/static/assets/{index-DPbsruV5.js → index-DBh_x_Ek.js} +46 -46
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BSRAqEj0.js → infoDiagram-6WML65LV-BrSiNxyd.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CLxE8hUp.js → ishikawaDiagram-WSZJBQD7-gvlvY-n9.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-juFzVXIj.js → journeyDiagram-NVQOT4AX-D-YaPzRQ.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CwLHVhOU.js → kanban-definition-27J2QSJJ-BsmDv9E_.js} +1 -1
- package/dist/worker/console/static/assets/{linear-9LfUyPAs.js → linear-UWaoDN3h.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-TPPHfQbe.js → mermaid.core-BlvpGVG8.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-Dn_Z9RxF.js → mindmap-definition-FAOFIHXS-CUCIP5CP.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CiEMKTgm.js → pegDiagram-VL7TDLO6-CB1aQUAK.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-JASaGxjZ.js → pieDiagram-7S7Q4E2Y-BwehHxhf.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-ChmH0ply.js → quadrantDiagram-CIZ2JOQS-CFz30APB.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CtyM-HGQ.js → railroadDiagram-AXF67PYL-BwNmw1i6.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BN9y_Bvs.js → requirementDiagram-LRYGKXZP-jg-tPXlI.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-IyS0GQ1-.js → sankeyDiagram-W5VNT64P-BlyReDKl.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DjxGXurN.js → sequenceDiagram-SI44F4Z6-DBUzW1f3.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Cr3Z0S8y.js → sizeCapture-X5ZJPWSS-BYcI2RAr.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BjJFC6pW.js → stateDiagram-OKZ733FA-D7AiyJdo.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DeMUJQiW.js → swimlanes-SLNWSIFB-BHGt7Kkv.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D2F7-U4H.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bc2Rv2Oo.js → timeline-definition-Z64GVDOM-OPjooWcX.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CDhRUeqG.js → vennDiagram-T6HMQDX7-ggsWDvrO.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DaN_z3Xy.js → wardleyDiagram-T6FBY63Y-DjNe-BjZ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-C1vTLBDG.js → xychartDiagram-ELKLHX3M-Byp63Hei.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +87 -4
- package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
- package/dist/workflows/dag/frontend-design-policy.js +19 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +82 -3
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shape.js +5 -3
- package/dist/workflows/dag/init-hybrid.js +94 -1
- package/dist/workflows/dag/node-execution.js +7 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +124 -1
- package/dist/workflows/dag/retry-policy.js +29 -9
- package/dist/workflows/dag/runner.js +89 -7
- package/dist/workflows/dag/types.js +16 -0
- package/dist/workflows/dag/validate.js +9 -3
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-DsdX3ZUX.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CrvCyJ-8.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CrvCyJ-8.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Bq44O9Nt.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BT4tWyW3.js +0 -8
|
@@ -131,6 +131,23 @@ function normalizeHazardOccurrenceIdentity(line) {
|
|
|
131
131
|
// Unclosed quote or no out-of-string `#`: do not invent a comment boundary.
|
|
132
132
|
return trimmed;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Bare `__NNN_*` helper module names imported by a Python source snippet.
|
|
136
|
+
* Only the head token of an `import`/`from` statement is considered a module
|
|
137
|
+
* name; dotted submodules (`__551_manager_helpers.x`) collapse to the head.
|
|
138
|
+
*/
|
|
139
|
+
function importedBareHelperModules(source) {
|
|
140
|
+
const names = [];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
for (const match of normalizeSourceNewlines(source).matchAll(/\b(?:import|from)\s+([A-Za-z0-9_.]+)/g)) {
|
|
143
|
+
const head = match[1].split(".")[0];
|
|
144
|
+
if (/^__\d+[A-Za-z0-9_]*$/.test(head) && !seen.has(head)) {
|
|
145
|
+
seen.add(head);
|
|
146
|
+
names.push(head);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return names;
|
|
150
|
+
}
|
|
134
151
|
function listPatternOccurrences(source, patternSource, flags = "") {
|
|
135
152
|
const normalized = normalizeSourceNewlines(source);
|
|
136
153
|
const pattern = new RegExp(patternSource, flags.includes("g") ? flags : `${flags}g`);
|
|
@@ -221,6 +238,8 @@ export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScri
|
|
|
221
238
|
const inputHashes = {};
|
|
222
239
|
const hazardOccurrenceCounts = {};
|
|
223
240
|
const hazardOccurrences = {};
|
|
241
|
+
const scriptBareImports = {};
|
|
242
|
+
const mappedScriptSet = new Set(normalizedScripts);
|
|
224
243
|
for (const file of assetFiles) {
|
|
225
244
|
const bytes = await readFile(path.resolve(workspaceRoot, file));
|
|
226
245
|
const source = bytes.toString("utf8");
|
|
@@ -228,8 +247,10 @@ export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScri
|
|
|
228
247
|
const lists = scanRepairHazardOccurrenceLists(source);
|
|
229
248
|
hazardOccurrences[file] = lists;
|
|
230
249
|
hazardOccurrenceCounts[file] = Object.fromEntries(Object.entries(lists).map(([label, identities]) => [label, identities.length]));
|
|
250
|
+
if (mappedScriptSet.has(file))
|
|
251
|
+
scriptBareImports[file] = importedBareHelperModules(source);
|
|
231
252
|
}
|
|
232
|
-
return { mappedScripts: normalizedScripts, assetFiles, inputHashes, hazardOccurrenceCounts, hazardOccurrences };
|
|
253
|
+
return { mappedScripts: normalizedScripts, assetFiles, inputHashes, hazardOccurrenceCounts, hazardOccurrences, scriptBareImports };
|
|
233
254
|
}
|
|
234
255
|
function bounded(value) {
|
|
235
256
|
const normalized = value.replaceAll(/(Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+/gi, "$1 [REDACTED]");
|
|
@@ -308,8 +329,68 @@ function classifyFixtureResolutionFailure(output, layout) {
|
|
|
308
329
|
repairPaths: paths,
|
|
309
330
|
};
|
|
310
331
|
}
|
|
311
|
-
|
|
332
|
+
/**
|
|
333
|
+
* Generated-local bare helper module signatures found in the bound pytest
|
|
334
|
+
* asset inventory. A file such as `testcase/__551_manager_helpers.py` in the
|
|
335
|
+
* test root/script dir is generated-local: when collection reports
|
|
336
|
+
* `No module named '__551_manager_helpers'`, the defect can be precisely
|
|
337
|
+
* attributed to the generated mapped script instead of a missing third-party
|
|
338
|
+
* package.
|
|
339
|
+
*/
|
|
340
|
+
function generatedLocalBareHelperNames(inventory) {
|
|
341
|
+
if (!inventory)
|
|
342
|
+
return [];
|
|
343
|
+
const names = [];
|
|
344
|
+
for (const file of inventory.assetFiles) {
|
|
345
|
+
const stem = path.posix.basename(file).replace(/\.py$/, "");
|
|
346
|
+
if (/^__\d+[A-Za-z0-9_]*$/.test(stem))
|
|
347
|
+
names.push(stem);
|
|
348
|
+
}
|
|
349
|
+
return [...new Set(names)];
|
|
350
|
+
}
|
|
351
|
+
function classifyCollectionFailure(output, evidence) {
|
|
312
352
|
const normalized = output.replaceAll("\\", "/");
|
|
353
|
+
const helperNames = generatedLocalBareHelperNames(evidence?.inventory);
|
|
354
|
+
const helperByLower = new Map(helperNames.map((name) => [name.toLowerCase(), name]));
|
|
355
|
+
// A bare `__NNN_...` module import that collection cannot find is only a
|
|
356
|
+
// candidate generated-local helper signature. REPAIRABLE requires two
|
|
357
|
+
// deterministic pieces of evidence: (1) a helper asset of the same name in
|
|
358
|
+
// the bound inventory, and (2) at least one mapped generated script whose
|
|
359
|
+
// source truly imports that bare module. Without both — or when attribution
|
|
360
|
+
// is impossible — fail closed as BLOCKED so an un-attributable dependency
|
|
361
|
+
// error is never widened into a repair. Genuine third-party modules (no
|
|
362
|
+
// `__NNN_` signature) stay BLOCKED below.
|
|
363
|
+
const missingModule = /No module named ['\"]([^'\"]+)['\"]/i.exec(normalized)?.[1];
|
|
364
|
+
if (missingModule && /^__\d+[A-Za-z0-9_]*$/.test(missingModule)) {
|
|
365
|
+
const helperAsset = helperByLower.get(missingModule.toLowerCase());
|
|
366
|
+
const inventory = evidence?.inventory;
|
|
367
|
+
const scriptBareImports = inventory?.scriptBareImports ?? {};
|
|
368
|
+
const missingLower = missingModule.toLowerCase();
|
|
369
|
+
const importedByMappedScript = inventory
|
|
370
|
+
? inventory.mappedScripts.some((script) => (scriptBareImports[script] ?? []).some((name) => name.toLowerCase() === missingLower))
|
|
371
|
+
: false;
|
|
372
|
+
if (helperAsset && importedByMappedScript && inventory) {
|
|
373
|
+
const repairPaths = [...new Set([
|
|
374
|
+
...(helperAsset
|
|
375
|
+
? inventory.assetFiles.filter((file) => path.posix.basename(file).replace(/\.py$/, "") === helperAsset)
|
|
376
|
+
: []),
|
|
377
|
+
...inventory.mappedScripts,
|
|
378
|
+
])].sort();
|
|
379
|
+
return {
|
|
380
|
+
status: "REPAIRABLE",
|
|
381
|
+
kind: "missing-generated-local-helper",
|
|
382
|
+
detail: `generated testcase asset imports missing bare generated-local helper module ${missingModule}; inline the helper or remove the generated shared dependency`,
|
|
383
|
+
repairPaths,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
status: "BLOCKED",
|
|
388
|
+
kind: "missing-generated-local-helper-without-evidence",
|
|
389
|
+
detail: helperAsset
|
|
390
|
+
? `missing bare generated-local helper module ${missingModule} has a name match but no mapped generated script imports it (no precise generated-local attribution)`
|
|
391
|
+
: `missing bare __NNN_* module ${missingModule} has no generated-local helper asset in the bound inventory (cannot be precisely attributed)`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
313
394
|
const blockedPatterns = [
|
|
314
395
|
[/No module named ['\"](?!testcase(?:\.|['\"]))/i, "missing-third-party-module"],
|
|
315
396
|
[/pytest(?:_|-)plugin|pluggy|unknown hook|unrecognized arguments/i, "pytest-plugin-or-config"],
|
|
@@ -401,7 +482,7 @@ export function assessBackendPytestCollection(input) {
|
|
|
401
482
|
fixtureStderrExcerpt: bounded(fixtureResolution.stderr),
|
|
402
483
|
});
|
|
403
484
|
}
|
|
404
|
-
const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}
|
|
485
|
+
const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}`, { layout: input.layout, inventory: input.inventory });
|
|
405
486
|
return backendPytestCollectionFactsSchema.parse({
|
|
406
487
|
schemaId: "backend-test-pytest-collection-v3",
|
|
407
488
|
phase: input.phase,
|
|
@@ -412,7 +493,9 @@ export function assessBackendPytestCollection(input) {
|
|
|
412
493
|
fixtureResolutionAttempted: false,
|
|
413
494
|
fixtureResolutionStatus: "NOT_RUN",
|
|
414
495
|
fixtureResolutionExitCode: null,
|
|
415
|
-
repairPaths:
|
|
496
|
+
repairPaths: classification.repairPaths && classification.repairPaths.length > 0
|
|
497
|
+
? classification.repairPaths
|
|
498
|
+
: testcasePythonPaths(`${input.stdout}\n${input.stderr}`),
|
|
416
499
|
mappedScripts: input.inventory.mappedScripts,
|
|
417
500
|
existingMappedScripts: input.inventory.mappedScripts,
|
|
418
501
|
missingMappedScripts: [],
|
|
@@ -385,12 +385,17 @@ export const FORBIDDEN_GENERATED_IMPORT_NAMESPACES = [
|
|
|
385
385
|
"testcase.helpers",
|
|
386
386
|
"testcase.factories",
|
|
387
387
|
];
|
|
388
|
-
const FORBIDDEN_GENERATED_IMPORT_PATTERN = /^\s*(?:from\s+(testcase_helpers|testcase\.helpers|testcase\.factories)\b|import\s+(testcase_helpers|testcase\.helpers|testcase\.factories)\b)/gm;
|
|
388
|
+
const FORBIDDEN_GENERATED_IMPORT_PATTERN = /^\s*(?:from\s+(testcase_helpers|testcase\.helpers|testcase\.factories|__\d+[A-Za-z0-9_]*)\b|import\s+(testcase_helpers|testcase\.helpers|testcase\.factories|__\d+[A-Za-z0-9_]*)\b)/gm;
|
|
389
389
|
/**
|
|
390
|
-
* Scan Python source for imports from forbidden generated shared namespaces
|
|
391
|
-
* Anchored at line start with a word
|
|
392
|
-
* substring inside a docstring
|
|
393
|
-
*
|
|
390
|
+
* Scan Python source for imports from forbidden generated shared namespaces
|
|
391
|
+
* or bare generated-local helper modules. Anchored at line start with a word
|
|
392
|
+
* boundary so `testcase.helpers_other` or a substring inside a docstring is not
|
|
393
|
+
* falsely flagged. In addition to the `testcase_helpers`/`testcase.helpers`/
|
|
394
|
+
* `testcase.factories` namespaces, bare `__<digits>...` helper modules (for
|
|
395
|
+
* example `from __551_manager_helpers import ...` / `import __551_manager_helpers`)
|
|
396
|
+
* are generated-local shared dependencies: a self-contained pytest child must
|
|
397
|
+
* inline or remove them rather than depend on a generated shared module.
|
|
398
|
+
* Returns the list of offending module names (deduplicated, order-preserving).
|
|
394
399
|
*/
|
|
395
400
|
export function findForbiddenGeneratedImports(source) {
|
|
396
401
|
const hits = [];
|
|
@@ -2,6 +2,10 @@ import { deterministicSha256, } from "./frontend-implementation-contract.js";
|
|
|
2
2
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
3
3
|
const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
4
4
|
const NPM_PACKAGE_NAME = /^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
5
|
+
/** Max concrete implementation files a single writeSet may carry before the
|
|
6
|
+
* plan is asked to split. Kept at 15: beyond this, a single implement node's
|
|
7
|
+
* read/edit loop inflates provider context toward overflow. */
|
|
8
|
+
const MAX_WRITE_SET_FILES = 15;
|
|
5
9
|
function normalizeRelativePath(value) {
|
|
6
10
|
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
7
11
|
}
|
|
@@ -209,7 +213,22 @@ export function checkComponentChoice(contract, componentSpecCandidatePaths) {
|
|
|
209
213
|
* must fall inside the declared writeSet patterns. */
|
|
210
214
|
export function checkTargetPaths(contract, writeSetPatterns) {
|
|
211
215
|
const findings = [];
|
|
216
|
+
// No frozen writeSet patterns (legacy / unconfigured run) → nothing to
|
|
217
|
+
// check; an empty pattern set must not report every file as out of scope.
|
|
218
|
+
if (writeSetPatterns.length === 0)
|
|
219
|
+
return findings;
|
|
212
220
|
const matches = (file) => writeSetPatterns.some((pattern) => pathMatchesPattern(file, pattern));
|
|
221
|
+
// A writeSet this large forces a single implement node to read/edit many
|
|
222
|
+
// files, inflating the provider context until it overflows (400
|
|
223
|
+
// request-too-large). Flag it so the plan is asked to split (the contract
|
|
224
|
+
// node already has record_split_proposal) instead of letting the writer
|
|
225
|
+
// blow up mid-run.
|
|
226
|
+
if (contract.targets.files.length > MAX_WRITE_SET_FILES) {
|
|
227
|
+
findings.push({
|
|
228
|
+
code: "write-set-too-large",
|
|
229
|
+
message: `implementation writeSet has ${contract.targets.files.length} files (limit ${MAX_WRITE_SET_FILES}); split the task (record_split_proposal) so no single implement node exceeds the bound`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
213
232
|
for (const file of contract.targets.files) {
|
|
214
233
|
if (!matches(file)) {
|
|
215
234
|
findings.push({
|
|
@@ -12,8 +12,9 @@ import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
|
12
12
|
// Cycle-safe value imports: the check functions are hoisted declarations used
|
|
13
13
|
// only at call time, so the contract module and the policy module can depend
|
|
14
14
|
// on each other without module-init ordering issues.
|
|
15
|
-
import { checkDependencies, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
15
|
+
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
16
16
|
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
17
|
+
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
17
18
|
import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
|
|
18
19
|
export const frontendNormalizationActionSchema = z.enum([
|
|
19
20
|
"remove-trailing-commas",
|
|
@@ -142,6 +143,35 @@ async function deriveAllowedDependenciesFromRun(runDir) {
|
|
|
142
143
|
}
|
|
143
144
|
return [...deps];
|
|
144
145
|
}
|
|
146
|
+
/** Read the frozen implementation writeSet patterns from run.json so the plan
|
|
147
|
+
* validator can front-load checkTargetPaths (write-set-too-large) before the
|
|
148
|
+
* design-policy shell terminates the run. */
|
|
149
|
+
async function deriveWriteSetFromRun(runDir) {
|
|
150
|
+
const specPath = path.join(runDir, "run.json");
|
|
151
|
+
let raw;
|
|
152
|
+
try {
|
|
153
|
+
raw = await readFile(specPath, "utf8");
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
let spec;
|
|
159
|
+
try {
|
|
160
|
+
spec = JSON.parse(raw);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
const writeSet = new Set();
|
|
166
|
+
for (const task of spec.tasks ?? []) {
|
|
167
|
+
const patterns = task.shell?.frontendDesignPolicy?.implementationWriteSet;
|
|
168
|
+
if (patterns) {
|
|
169
|
+
for (const pattern of patterns)
|
|
170
|
+
writeSet.add(pattern);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [...writeSet];
|
|
174
|
+
}
|
|
145
175
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
146
176
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
147
177
|
/**
|
|
@@ -309,8 +339,50 @@ function stripNullValuesDeep(value) {
|
|
|
309
339
|
}
|
|
310
340
|
return value;
|
|
311
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Normalize a model-emitted `uiComponentChoices` where a `new` choice carries a
|
|
344
|
+
* degenerate empty `specReference` object (`{ path: "", section: "", line:
|
|
345
|
+
* null }`) instead of the required `null`. The model intent is unambiguous
|
|
346
|
+
* (new components must not carry a specReference; the empty object is a format
|
|
347
|
+
* slip, not a real spec hit), so dropping the empty object before validation
|
|
348
|
+
* avoids a full plan retry on an otherwise-correct emission. A `specReference`
|
|
349
|
+
* with a non-empty `path` is preserved (it is a genuine `specified` hit).
|
|
350
|
+
*/
|
|
351
|
+
export function normalizeNewComponentSpecReference(value) {
|
|
352
|
+
if (!Array.isArray(value))
|
|
353
|
+
return value;
|
|
354
|
+
return value.map((item) => {
|
|
355
|
+
if (!item || typeof item !== "object")
|
|
356
|
+
return item;
|
|
357
|
+
const record = item;
|
|
358
|
+
if (record.decision !== "new")
|
|
359
|
+
return item;
|
|
360
|
+
const ref = record.specReference;
|
|
361
|
+
if (!ref || typeof ref !== "object")
|
|
362
|
+
return item;
|
|
363
|
+
const refRecord = ref;
|
|
364
|
+
const path = typeof refRecord.path === "string" ? refRecord.path.trim() : "";
|
|
365
|
+
if (path.length > 0)
|
|
366
|
+
return item;
|
|
367
|
+
// Empty/degenerate specReference on a new choice → absent (null intent).
|
|
368
|
+
const { specReference: _dropped, ...rest } = record;
|
|
369
|
+
return rest;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
312
372
|
export const frontendImplementationContractSchema = z
|
|
313
|
-
.preprocess(
|
|
373
|
+
.preprocess((value) => {
|
|
374
|
+
const record = asRecord(value);
|
|
375
|
+
if (!record)
|
|
376
|
+
return stripNullValuesDeep(value);
|
|
377
|
+
return stripNullValuesDeep({
|
|
378
|
+
...record,
|
|
379
|
+
...(record.uiComponentChoices !== undefined
|
|
380
|
+
? {
|
|
381
|
+
uiComponentChoices: normalizeNewComponentSpecReference(record.uiComponentChoices),
|
|
382
|
+
}
|
|
383
|
+
: {}),
|
|
384
|
+
});
|
|
385
|
+
}, z
|
|
314
386
|
.object({
|
|
315
387
|
schemaVersion: z.literal(1),
|
|
316
388
|
sourceBinding: z
|
|
@@ -1974,6 +2046,7 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1974
2046
|
...checkUiDesignCoverage(analysis.canonical),
|
|
1975
2047
|
...checkUiStateAttribution(analysis.canonical),
|
|
1976
2048
|
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
2049
|
+
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
1977
2050
|
];
|
|
1978
2051
|
if (policyPreFindings.length > 0)
|
|
1979
2052
|
throw new Error(`frontend plan policy pre-check failed (fix these plan facts, then re-commit and finalize): ${policyPreFindings
|
|
@@ -2003,11 +2076,17 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
2003
2076
|
sha256: canonicalArtifact.sha256,
|
|
2004
2077
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2005
2078
|
};
|
|
2079
|
+
// Human-readable plan summary appended after the artifact pointer. The
|
|
2080
|
+
// pointer remains the machine authority (path/schema/sha256); the
|
|
2081
|
+
// rendered summary is a deterministic view of the canonical contract so
|
|
2082
|
+
// Console/reviewers can read the plan without opening the JSON. It never
|
|
2083
|
+
// participates in downstream validation.
|
|
2084
|
+
const planSummary = renderFrontendPlanMarkdown(analysis.canonical);
|
|
2006
2085
|
return {
|
|
2007
2086
|
ok: true,
|
|
2008
2087
|
contract: analysis.canonical,
|
|
2009
2088
|
artifact,
|
|
2010
|
-
normalizedText: formatStructuredArtifactPointer(artifact)
|
|
2089
|
+
normalizedText: `${formatStructuredArtifactPointer(artifact)}\n\n${planSummary}`,
|
|
2011
2090
|
};
|
|
2012
2091
|
}
|
|
2013
2092
|
catch (error) {
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readTypedEventStoreFromJsonl, } from "./frontend-typed-event-store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Frontend review findings → downstream work (direction A).
|
|
6
|
+
*
|
|
7
|
+
* When `frontend-review-pi` commits `request_review_changes`, the implementation
|
|
8
|
+
* must not silently finish: the run becomes "not passed" (partial_failed +
|
|
9
|
+
* terminalReason=review-request-changes), and the committed findings are
|
|
10
|
+
* rendered into a downstream-task PRD draft that a later DAG round can consume
|
|
11
|
+
* instead of repeating the previous flow. Rendering is deterministic (pure);
|
|
12
|
+
* only the JSONL read and PRD write are IO.
|
|
13
|
+
*
|
|
14
|
+
* The design-review analog (`request_design_changes`) reuses the same PRD
|
|
15
|
+
* rendering and typed-fact channel: a design rejection is a definite finding
|
|
16
|
+
* (not a protocol/environment error), so it auto-re-enters the next plan round
|
|
17
|
+
* with the committed design findings instead of terminating the run.
|
|
18
|
+
*/
|
|
19
|
+
export const FRONTEND_REVIEW_FINDINGS_PRD_REL = "contracts/frontend-review-findings-prd.md";
|
|
20
|
+
/** Review node id whose committed typed facts drive the findings PRD. */
|
|
21
|
+
export const FRONTEND_REVIEW_NODE_ID = "frontend-review-pi";
|
|
22
|
+
/** Design review node id whose committed typed facts drive the findings PRD. */
|
|
23
|
+
export const FRONTEND_DESIGN_REVIEW_NODE_ID = "frontend-design-review-pi";
|
|
24
|
+
/** Deterministic issueCategory → failure owner mapping (A+B AC-009 §4.10). */
|
|
25
|
+
export function classifyReviewIssueCategoryFailureOwner(issueCategory) {
|
|
26
|
+
switch (issueCategory) {
|
|
27
|
+
case "implementation-mismatch":
|
|
28
|
+
return "implementation";
|
|
29
|
+
case "approved-design-defect":
|
|
30
|
+
return "plan";
|
|
31
|
+
case "target-surface-defect":
|
|
32
|
+
return "scout";
|
|
33
|
+
case "contract-requirement-gap":
|
|
34
|
+
return "contract";
|
|
35
|
+
case "unknown":
|
|
36
|
+
default:
|
|
37
|
+
return "unknown";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const API_SIGNAL_RE = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i;
|
|
41
|
+
/** Negations for the API signal. CJK "无 API/无接口" has no \b boundary, so
|
|
42
|
+
* it is matched separately from the ASCII no api/not-needed forms. */
|
|
43
|
+
const API_NEGATION_RE = /(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])|无\s*api|无\s*接口/i;
|
|
44
|
+
const SMALL_SIGNAL_WORDS = [
|
|
45
|
+
"单文件局部修复",
|
|
46
|
+
"局部样式",
|
|
47
|
+
"文案调整",
|
|
48
|
+
"无远程接口",
|
|
49
|
+
];
|
|
50
|
+
function topLevelDir(p) {
|
|
51
|
+
const normalized = p.replace(/\\/g, "/");
|
|
52
|
+
const first = normalized.split("/").filter(Boolean)[0] ?? "";
|
|
53
|
+
return first;
|
|
54
|
+
}
|
|
55
|
+
/** Collect unique non-empty finding `file` values (sorted, de-duplicated). */
|
|
56
|
+
function findingTargetFiles(fact) {
|
|
57
|
+
const files = new Set();
|
|
58
|
+
for (const finding of fact.findings) {
|
|
59
|
+
if (typeof finding.file === "string" && finding.file.trim()) {
|
|
60
|
+
files.add(finding.file.trim());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...files].sort();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Deterministically derive the fix scope for the downstream task. Same fact
|
|
67
|
+
* always yields the same scope. The shape mirrors `resolveFrontendTaskShape`
|
|
68
|
+
* signals (concentrated paths, no remote API → micro/small) so that when the
|
|
69
|
+
* PRD draft is used as the next task's requirement, the frontend DAG generator
|
|
70
|
+
* routes a small fix to micro/small instead of the full 11-node standard
|
|
71
|
+
* topology.
|
|
72
|
+
*/
|
|
73
|
+
export function deriveFrontendReviewFixScope(fact) {
|
|
74
|
+
const targetFiles = findingTargetFiles(fact);
|
|
75
|
+
const blob = fact.findings
|
|
76
|
+
.map((f) => `${f.issue} ${f.requiredChange ?? ""} ${f.file ?? ""}`)
|
|
77
|
+
.join("\n");
|
|
78
|
+
const hasApiSignal = API_SIGNAL_RE.test(blob) && !API_NEGATION_RE.test(blob);
|
|
79
|
+
const topDirs = new Set(targetFiles.map(topLevelDir));
|
|
80
|
+
const concentrated = targetFiles.length > 0 && topDirs.size <= 1 && targetFiles.length <= 4;
|
|
81
|
+
let shape;
|
|
82
|
+
let complexity;
|
|
83
|
+
let reason;
|
|
84
|
+
if (targetFiles.length === 0) {
|
|
85
|
+
shape = "standard";
|
|
86
|
+
complexity = "medium";
|
|
87
|
+
reason =
|
|
88
|
+
"findings carry no file references; scope unknown, keep standard topology";
|
|
89
|
+
}
|
|
90
|
+
else if (hasApiSignal) {
|
|
91
|
+
shape = "standard";
|
|
92
|
+
complexity = "medium";
|
|
93
|
+
reason = `findings reference a remote API signal; standard topology required (files=${targetFiles.length})`;
|
|
94
|
+
}
|
|
95
|
+
else if (concentrated && targetFiles.length <= 1) {
|
|
96
|
+
shape = "micro";
|
|
97
|
+
complexity = "trivial";
|
|
98
|
+
reason = "single-file concentrated fix without remote API";
|
|
99
|
+
}
|
|
100
|
+
else if (concentrated) {
|
|
101
|
+
shape = "small";
|
|
102
|
+
complexity = "low";
|
|
103
|
+
reason = `concentrated fix across ${targetFiles.length} file(s) in one top-level directory`;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
shape = "standard";
|
|
107
|
+
complexity = "medium";
|
|
108
|
+
reason = `fix spans ${targetFiles.length} file(s) across ${topDirs.size} top-level directories; standard topology`;
|
|
109
|
+
}
|
|
110
|
+
const allowedPaths = [
|
|
111
|
+
...new Set([
|
|
112
|
+
...targetFiles,
|
|
113
|
+
...targetFiles.map((f) => `${topLevelDir(f)}/**`),
|
|
114
|
+
]),
|
|
115
|
+
].sort();
|
|
116
|
+
return {
|
|
117
|
+
shape,
|
|
118
|
+
complexity,
|
|
119
|
+
targetFiles,
|
|
120
|
+
allowedPaths,
|
|
121
|
+
smallSignals: [...SMALL_SIGNAL_WORDS],
|
|
122
|
+
hasApiSignal,
|
|
123
|
+
reason,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Read the committed `request_review_changes` fact from the review node's typed
|
|
128
|
+
* event store. Returns undefined when no committed request fact exists (e.g.
|
|
129
|
+
* approve_review, missing store, or an unreadable store).
|
|
130
|
+
*/
|
|
131
|
+
export async function readCommittedReviewRequestFact(input) {
|
|
132
|
+
const { runDir: reviewRunDir } = input;
|
|
133
|
+
const reviewNodeId = input.reviewNodeId ?? FRONTEND_REVIEW_NODE_ID;
|
|
134
|
+
return readCommittedRequestFactByKind({
|
|
135
|
+
runDir: reviewRunDir,
|
|
136
|
+
nodeId: reviewNodeId,
|
|
137
|
+
factsFile: "review-typed-facts.jsonl",
|
|
138
|
+
kind: "request_review_changes",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read the committed `request_design_changes` fact from the design review
|
|
143
|
+
* node's typed event store. Returns undefined when no committed request fact
|
|
144
|
+
* exists (e.g. approve_design, missing store, or an unreadable store).
|
|
145
|
+
*/
|
|
146
|
+
export async function readCommittedDesignRequestFact(input) {
|
|
147
|
+
const { runDir: designRunDir } = input;
|
|
148
|
+
const designNodeId = input.designNodeId ?? FRONTEND_DESIGN_REVIEW_NODE_ID;
|
|
149
|
+
return readCommittedRequestFactByKind({
|
|
150
|
+
runDir: designRunDir,
|
|
151
|
+
nodeId: designNodeId,
|
|
152
|
+
factsFile: "design-typed-facts.jsonl",
|
|
153
|
+
kind: "request_design_changes",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function readCommittedRequestFactByKind(input) {
|
|
157
|
+
let records;
|
|
158
|
+
try {
|
|
159
|
+
records = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.nodeId, input.factsFile));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const committed = records.filter((record) => record.phase === "committed");
|
|
165
|
+
if (committed.length === 0)
|
|
166
|
+
return undefined;
|
|
167
|
+
const request = committed.find((record) => record.fact.kind === input.kind);
|
|
168
|
+
if (!request)
|
|
169
|
+
return undefined;
|
|
170
|
+
const fact = request.fact;
|
|
171
|
+
if (typeof fact !== "object" || fact === null)
|
|
172
|
+
return undefined;
|
|
173
|
+
return fact;
|
|
174
|
+
}
|
|
175
|
+
function bulletList(items) {
|
|
176
|
+
if (!items || items.length === 0)
|
|
177
|
+
return "_(none)_";
|
|
178
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
179
|
+
}
|
|
180
|
+
function findingLines(findings) {
|
|
181
|
+
if (findings.length === 0)
|
|
182
|
+
return "_(none)_";
|
|
183
|
+
return findings
|
|
184
|
+
.map((finding) => {
|
|
185
|
+
const location = [finding.file, finding.line]
|
|
186
|
+
.filter((value) => value !== undefined)
|
|
187
|
+
.join(":");
|
|
188
|
+
const prefix = location ? `[${location}] ` : "";
|
|
189
|
+
const requiredChange = finding.requiredChange
|
|
190
|
+
? `\n - requiredChange: ${finding.requiredChange}`
|
|
191
|
+
: "";
|
|
192
|
+
return `- ${prefix}${finding.severity}: ${finding.issue}${requiredChange}`;
|
|
193
|
+
})
|
|
194
|
+
.join("\n");
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Deterministically render a downstream-task PRD draft from a committed
|
|
198
|
+
* `request_review_changes` / `request_design_changes` fact. Same fact always
|
|
199
|
+
* renders byte-identical output; the parent run id and review node id are the
|
|
200
|
+
* only context.
|
|
201
|
+
*/
|
|
202
|
+
export function renderFrontendReviewFindingsPrd(input) {
|
|
203
|
+
const { parentRunId, fact } = input;
|
|
204
|
+
const isDesign = fact.kind === "request_design_changes";
|
|
205
|
+
const reviewNodeId = input.reviewNodeId ??
|
|
206
|
+
(isDesign ? FRONTEND_DESIGN_REVIEW_NODE_ID : FRONTEND_REVIEW_NODE_ID);
|
|
207
|
+
const restartPhase = classifyReviewIssueCategoryFailureOwner(fact.issueCategory);
|
|
208
|
+
const scope = deriveFrontendReviewFixScope(fact);
|
|
209
|
+
const targetSummary = scope.targetFiles.length > 0
|
|
210
|
+
? scope.targetFiles.join(", ")
|
|
211
|
+
: fact.issueCategory;
|
|
212
|
+
return [
|
|
213
|
+
`# 修复 ${isDesign ? "设计" : "review"} 发现(${scope.shape}):${targetSummary}`,
|
|
214
|
+
"",
|
|
215
|
+
"## 背景",
|
|
216
|
+
"",
|
|
217
|
+
`- parentRunId: \`${parentRunId}\``,
|
|
218
|
+
`- reviewNodeId: \`${reviewNodeId}\``,
|
|
219
|
+
`- typedVerdict: \`${fact.kind}\``,
|
|
220
|
+
`- issueCategory: \`${fact.issueCategory}\``,
|
|
221
|
+
`- restartPhase/failureOwner: \`${restartPhase}\``,
|
|
222
|
+
"",
|
|
223
|
+
"## 修复范围(供下一轮任务生成路由)",
|
|
224
|
+
"",
|
|
225
|
+
`- 建议 shape:\`${scope.shape}\`(micro=7 / small=10 / standard=11 节点)`,
|
|
226
|
+
`- 建议 complexity:\`${scope.complexity}\``,
|
|
227
|
+
`- 推导依据:${scope.reason}`,
|
|
228
|
+
...(scope.hasApiSignal ? ["- ⚠️ findings 含远程 API/接口信号,走 standard 拓扑"] : []),
|
|
229
|
+
scope.targetFiles.length > 0
|
|
230
|
+
? [`- 目标文件:${scope.targetFiles.map((f) => `\`${f}\``).join(", ")}`]
|
|
231
|
+
: ["- 目标文件:_(findings 未提供 file 引用,需人工补全)_"],
|
|
232
|
+
scope.allowedPaths.length > 0
|
|
233
|
+
? [`- 建议 allowedPaths:${scope.allowedPaths.map((p) => `\`${p}\``).join(", ")}`]
|
|
234
|
+
: [],
|
|
235
|
+
scope.shape === "micro" || scope.shape === "small"
|
|
236
|
+
? [`- 范围信号:${scope.smallSignals.join("、")}(供 shape 分类器识别,避免退化为全节点流程)`]
|
|
237
|
+
: [],
|
|
238
|
+
"",
|
|
239
|
+
"## 问题清单",
|
|
240
|
+
"",
|
|
241
|
+
findingLines(fact.findings),
|
|
242
|
+
"",
|
|
243
|
+
"## 证据引用",
|
|
244
|
+
"",
|
|
245
|
+
bulletList(fact.evidenceRefs),
|
|
246
|
+
"",
|
|
247
|
+
"## 修复目标",
|
|
248
|
+
"",
|
|
249
|
+
`- 按 issueCategory 映射的重启阶段(${restartPhase})定点修复,不复用上一轮未通过的实现。`,
|
|
250
|
+
"- 逐条关闭上面「问题清单」中的 findings,并保留上一轮已通过的验证目标继续有效。",
|
|
251
|
+
`- 完成修复后重新走 frontend DAG 的 ${isDesign ? "plan → design" : "implement → verify → review"} 链,直到${isDesign ? " design-review 提交 approve_design" : " review 提交 approve_review"}。`,
|
|
252
|
+
"",
|
|
253
|
+
].join("\n");
|
|
254
|
+
}
|
|
255
|
+
/** Write the findings PRD draft under the run dir (run-owned artifact). */
|
|
256
|
+
export async function writeFrontendReviewFindingsPrd(input) {
|
|
257
|
+
const markdown = renderFrontendReviewFindingsPrd(input);
|
|
258
|
+
const abs = path.join(input.runDir, FRONTEND_REVIEW_FINDINGS_PRD_REL);
|
|
259
|
+
await mkdir(path.dirname(abs), { recursive: true });
|
|
260
|
+
await writeFile(abs, markdown, "utf8");
|
|
261
|
+
return FRONTEND_REVIEW_FINDINGS_PRD_REL;
|
|
262
|
+
}
|
|
263
|
+
export async function readFrontendReviewFindingsPrd(runDir) {
|
|
264
|
+
try {
|
|
265
|
+
return await readFile(path.join(runDir, FRONTEND_REVIEW_FINDINGS_PRD_REL), "utf8");
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -29,7 +29,7 @@ const SHAPE_ESCALATION_PATTERNS = [
|
|
|
29
29
|
];
|
|
30
30
|
const SHAPE_SMALL_PATTERNS = [
|
|
31
31
|
{ id: "single-component", re: /single\s+component|单一组件|局部样式|style\s+tweak|css\s+only|文案|copy\s+change/i },
|
|
32
|
-
{ id: "no-remote", re: /no\s+api
|
|
32
|
+
{ id: "no-remote", re: /no\s+api|无\s*api|无\s*接口|本地状态|local\s+state\s+only|pure\s+ui/i },
|
|
33
33
|
];
|
|
34
34
|
const SHAPE_SPLIT_REQUIRED_PATTERN = /拆分|split\s+(?:requirement|task|需求)|large\s+requirement|大型需求|多步骤需求/i;
|
|
35
35
|
const EXECUTABLE_SHAPE_RANK = {
|
|
@@ -77,7 +77,8 @@ function resolveBaselineFrontendTaskShape(input) {
|
|
|
77
77
|
const spread = pathSpreadScore(paths);
|
|
78
78
|
const concentrated = spread <= 1 && paths.length <= 4;
|
|
79
79
|
const hasApiSignal = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i.test(blob) &&
|
|
80
|
-
|
|
80
|
+
!(/(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])/i.test(blob) ||
|
|
81
|
+
/无\s*api|无\s*接口/i.test(blob));
|
|
81
82
|
const complexity = (input.complexity ?? "").toLowerCase();
|
|
82
83
|
// split-required wins over everything: it is a termination/escalation
|
|
83
84
|
// signal, not an executable topology.
|
|
@@ -258,7 +259,8 @@ export function deriveFrontendRuntimeShapeFacts(input) {
|
|
|
258
259
|
const escalationSignals = collectEscalationSignals(blob, supervised);
|
|
259
260
|
const paths = input.allowedPaths ?? [];
|
|
260
261
|
const hasApiSignal = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i.test(blob) &&
|
|
261
|
-
|
|
262
|
+
!(/(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])/i.test(blob) ||
|
|
263
|
+
/无\s*api|无\s*接口/i.test(blob));
|
|
262
264
|
const requiresFull = escalationSignals.length > 0 || pathSpreadScore(paths) > 1 || hasApiSignal;
|
|
263
265
|
return {
|
|
264
266
|
splitRequired: input.splitRequired,
|