@tea-agent/loop-agent 0.21.0 → 0.22.0
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 +46 -0
- package/bin/agent-worker.js +0 -0
- package/dist/adapters/loop-agent.js +52 -0
- package/dist/commands/init.js +97 -0
- package/dist/executors/dag-pi-executor.js +2 -0
- package/dist/executors/shell-executor.js +162 -19
- package/dist/shared/openspec-spec.js +49 -0
- package/dist/worker/observability/read-model.js +21 -1
- package/dist/worker/observe/spec-evidence.js +12 -15
- package/dist/worker/observe/static/dag-helpers.js +22 -0
- package/dist/worker/observe/static/views/dag.js +5 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +37 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +141 -32
- package/dist/workflows/dag/frontend-lint-baseline.js +471 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +79 -16
- package/dist/workflows/dag/frontend-project-capability.js +11 -8
- package/dist/workflows/dag/frontend-repair.js +6 -4
- package/dist/workflows/dag/frontend-review-context.js +67 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +105 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +71 -66
- package/dist/workflows/dag/frontend-verification-trace.js +31 -1
- package/dist/workflows/dag/frontend-worktree-diff.js +81 -6
- package/dist/workflows/dag/init-hybrid.js +344 -64
- package/dist/workflows/dag/types.js +62 -1
- package/docs/templates/agent-dag.schema.json +15 -5
- package/docs/templates/backend-test-dag.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +4 -3
- package/docs/templates/frontend-test-case-checklist.md +6 -2
- package/docs/templates/frontend-test-dag.json +2 -2
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +12 -10
- package/skills/frontend-design-review/references/review-checklist.md +4 -4
- package/skills/frontend-implementation/SKILL.md +2 -2
- package/skills/frontend-implementation/references/code-standards.md +4 -3
- package/skills/frontend-implementation/references/design-spec.md +19 -14
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/frontend-review/SKILL.md +15 -28
- package/skills/frontend-review/references/review-findings.md +16 -18
- package/skills/frontend-verification/SKILL.md +16 -13
- package/skills/frontend-verification/references/verification-checklist.md +18 -30
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access, readdir, readFile, realpath, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { assertValidDagSpec } from "./validate.js";
|
|
@@ -829,6 +830,9 @@ function extractFrontendMockVerifyCommandsFromMarkdown(input) {
|
|
|
829
830
|
return commands.filter((command, index, all) => all.findIndex((candidate) => candidate.args.join("\0") === command.args.join("\0")) === index);
|
|
830
831
|
}
|
|
831
832
|
function chooseFrontendVerifyCommands(input) {
|
|
833
|
+
if (input.explicitCommands && input.explicitCommands.length > 0) {
|
|
834
|
+
return { commands: input.explicitCommands, commandSource: "inline" };
|
|
835
|
+
}
|
|
832
836
|
if (input.parsedCommands.length > 0) {
|
|
833
837
|
return { commands: input.parsedCommands, commandSource: "inline" };
|
|
834
838
|
}
|
|
@@ -837,6 +841,36 @@ function chooseFrontendVerifyCommands(input) {
|
|
|
837
841
|
}
|
|
838
842
|
return { commandSource: "inline" };
|
|
839
843
|
}
|
|
844
|
+
function classifyFrontendVerifyCommand(command) {
|
|
845
|
+
return /\b(typecheck|check-types|lint|eslint|tsc|build|check)\b/i.test(command) ||
|
|
846
|
+
/\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command)
|
|
847
|
+
? "static"
|
|
848
|
+
: "behavior";
|
|
849
|
+
}
|
|
850
|
+
function buildExplicitFrontendVerifyCommands(taskConfig, repoRoot) {
|
|
851
|
+
const staticCommands = [];
|
|
852
|
+
const behaviorCommands = [];
|
|
853
|
+
if (!repoRoot)
|
|
854
|
+
return { staticCommands, behaviorCommands };
|
|
855
|
+
for (const command of taskConfig.verifyCommands) {
|
|
856
|
+
const verifyCommand = {
|
|
857
|
+
args: ["bash", "-lc", command.command],
|
|
858
|
+
cwd: repoRoot,
|
|
859
|
+
label: command.label,
|
|
860
|
+
timeoutMs: command.timeoutMs,
|
|
861
|
+
};
|
|
862
|
+
if (classifyFrontendVerifyCommand(command.command) === "static") {
|
|
863
|
+
staticCommands.push(verifyCommand);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
behaviorCommands.push(verifyCommand);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return { staticCommands, behaviorCommands };
|
|
870
|
+
}
|
|
871
|
+
function verifyCommandKey(command) {
|
|
872
|
+
return `${command.cwd}\0${command.args.join("\0")}`;
|
|
873
|
+
}
|
|
840
874
|
function resolveDagVerifyStrategy(taskConfig) {
|
|
841
875
|
return {
|
|
842
876
|
intermediateQuota: taskConfig.dagVerifyStrategy?.intermediateQuota ?? taskConfig.verifyQuota,
|
|
@@ -851,19 +885,80 @@ function buildVerifyEvidence(input) {
|
|
|
851
885
|
commandSource: input.commandSource,
|
|
852
886
|
commandCount: input.commands?.length ?? input.fallbackCommands.length,
|
|
853
887
|
commandLabels: input.commands?.map((command) => command.label) ?? input.fallbackCommands,
|
|
888
|
+
commandTexts: input.commandTexts ?? [],
|
|
854
889
|
finalFullRequired: input.finalFullRequired,
|
|
855
890
|
};
|
|
856
891
|
}
|
|
892
|
+
function isFrontendLintVerifyCommand(command) {
|
|
893
|
+
const text = `${command.label}\n${command.args.join(" ")}`;
|
|
894
|
+
return /\b(?:lint|eslint)\b/i.test(text);
|
|
895
|
+
}
|
|
896
|
+
function isManagedCiWrapperVerifyCommand(command) {
|
|
897
|
+
const text = `${command.label}\n${command.args.join(" ")}`.replaceAll("\\", "/");
|
|
898
|
+
return /\bscripts\/ci(?:-tests)?\.sh\b/.test(text);
|
|
899
|
+
}
|
|
900
|
+
function repoHasNpmScript(repoRoot, scriptName) {
|
|
901
|
+
if (!repoRoot)
|
|
902
|
+
return false;
|
|
903
|
+
const packagePath = path.join(repoRoot, "package.json");
|
|
904
|
+
if (!existsSync(packagePath))
|
|
905
|
+
return false;
|
|
906
|
+
try {
|
|
907
|
+
const decoded = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
908
|
+
return typeof decoded.scripts?.[scriptName] === "string";
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function partitionFrontendStaticVerifyCommands(input) {
|
|
915
|
+
const commands = input.commands ?? [];
|
|
916
|
+
const lintCommands = commands.filter(isFrontendLintVerifyCommand);
|
|
917
|
+
const staticCommands = commands.filter((command) => !isFrontendLintVerifyCommand(command));
|
|
918
|
+
if (lintCommands.length === 0 &&
|
|
919
|
+
commands.some(isManagedCiWrapperVerifyCommand) &&
|
|
920
|
+
repoHasNpmScript(input.repoRoot, "lint")) {
|
|
921
|
+
lintCommands.push({
|
|
922
|
+
args: ["npm", "run", "lint"],
|
|
923
|
+
cwd: input.repoRoot,
|
|
924
|
+
label: "npm run lint",
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
lint: {
|
|
929
|
+
...(lintCommands.length > 0 ? { commands: lintCommands } : {}),
|
|
930
|
+
commandSource: input.commandSource,
|
|
931
|
+
},
|
|
932
|
+
static: {
|
|
933
|
+
...(staticCommands.length > 0 ? { commands: staticCommands } : {}),
|
|
934
|
+
commandSource: input.commandSource,
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function isFrontendTestPathPattern(entry) {
|
|
939
|
+
const segments = entry.replaceAll("\\", "/").split("/");
|
|
940
|
+
return segments.some((segment) => /^(?:test|tests|spec|specs|e2e|__tests__|__specs__)$/i.test(segment));
|
|
941
|
+
}
|
|
942
|
+
function replaceSourceSegmentWithTestSegment(entry) {
|
|
943
|
+
const normalized = entry.replaceAll("\\", "/");
|
|
944
|
+
const segments = normalized.split("/");
|
|
945
|
+
const sourceIndex = segments.findIndex((segment) => /^(?:src|source|app|lib)$/i.test(segment));
|
|
946
|
+
if (sourceIndex < 0)
|
|
947
|
+
return null;
|
|
948
|
+
const testSegments = [...segments];
|
|
949
|
+
testSegments[sourceIndex] = "test";
|
|
950
|
+
return testSegments.join("/");
|
|
951
|
+
}
|
|
857
952
|
function deriveParallelScoutPaths(taskConfig) {
|
|
858
953
|
const allowed = taskConfig.allowedPaths;
|
|
859
954
|
if (allowed.length === 0) {
|
|
860
955
|
return { srcPaths: ["**"], testPaths: ["**"] };
|
|
861
956
|
}
|
|
862
|
-
const srcPaths = allowed.filter((entry) => !entry
|
|
863
|
-
const explicitTestPaths = allowed.filter(
|
|
957
|
+
const srcPaths = allowed.filter((entry) => !isFrontendTestPathPattern(entry));
|
|
958
|
+
const explicitTestPaths = allowed.filter(isFrontendTestPathPattern);
|
|
864
959
|
const derivedTestPaths = allowed
|
|
865
|
-
.
|
|
866
|
-
.
|
|
960
|
+
.map(replaceSourceSegmentWithTestSegment)
|
|
961
|
+
.filter((entry) => Boolean(entry));
|
|
867
962
|
return {
|
|
868
963
|
srcPaths: srcPaths.length > 0 ? srcPaths : allowed,
|
|
869
964
|
testPaths: explicitTestPaths.length > 0
|
|
@@ -873,6 +968,80 @@ function deriveParallelScoutPaths(taskConfig) {
|
|
|
873
968
|
: allowed,
|
|
874
969
|
};
|
|
875
970
|
}
|
|
971
|
+
/**
|
|
972
|
+
* Resolve frontend verification fallbacks from the target project's own
|
|
973
|
+
* package scripts. The DAG builder is also used by unit fixtures without a
|
|
974
|
+
* package.json, so those fixtures retain the historical generic fallback.
|
|
975
|
+
* Real projects never inherit loop-agent's commands when package.json exists.
|
|
976
|
+
*/
|
|
977
|
+
async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
978
|
+
const genericFallback = {
|
|
979
|
+
staticCommands: ["npm run typecheck", "npm run build"],
|
|
980
|
+
behaviorCommands: ["npm test"],
|
|
981
|
+
};
|
|
982
|
+
if (!repoRoot)
|
|
983
|
+
return genericFallback;
|
|
984
|
+
let scripts;
|
|
985
|
+
try {
|
|
986
|
+
const packageJson = JSON.parse(await readFile(path.join(repoRoot, "package.json"), "utf8"));
|
|
987
|
+
if (packageJson.scripts && typeof packageJson.scripts === "object") {
|
|
988
|
+
scripts = packageJson.scripts;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
return genericFallback;
|
|
993
|
+
}
|
|
994
|
+
if (!scripts)
|
|
995
|
+
return genericFallback;
|
|
996
|
+
let packageManager = "npm";
|
|
997
|
+
for (const [lockfile, manager] of [
|
|
998
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
999
|
+
["yarn.lock", "yarn"],
|
|
1000
|
+
["bun.lockb", "bun"],
|
|
1001
|
+
["bun.lock", "bun"],
|
|
1002
|
+
]) {
|
|
1003
|
+
try {
|
|
1004
|
+
await access(path.join(repoRoot, lockfile));
|
|
1005
|
+
packageManager = manager;
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
// Try the next package-manager marker.
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const commandForScript = (script) => script === "test"
|
|
1013
|
+
? `${packageManager} test`
|
|
1014
|
+
: `${packageManager} run ${script}`;
|
|
1015
|
+
const hasScript = (script) => typeof scripts?.[script] === "string" &&
|
|
1016
|
+
String(scripts[script]).trim().length > 0;
|
|
1017
|
+
const firstExisting = (names) => {
|
|
1018
|
+
const selected = [];
|
|
1019
|
+
for (const name of names) {
|
|
1020
|
+
if (hasScript(name) && !selected.includes(name))
|
|
1021
|
+
selected.push(name);
|
|
1022
|
+
}
|
|
1023
|
+
return selected.map(commandForScript);
|
|
1024
|
+
};
|
|
1025
|
+
const staticCommands = firstExisting([
|
|
1026
|
+
"typecheck",
|
|
1027
|
+
"check-types",
|
|
1028
|
+
"lint",
|
|
1029
|
+
"check",
|
|
1030
|
+
"build",
|
|
1031
|
+
]);
|
|
1032
|
+
const behaviorCommands = firstExisting([
|
|
1033
|
+
"test:unit",
|
|
1034
|
+
"test:frontend",
|
|
1035
|
+
"test:component",
|
|
1036
|
+
"test",
|
|
1037
|
+
"test:e2e",
|
|
1038
|
+
"e2e",
|
|
1039
|
+
]);
|
|
1040
|
+
return {
|
|
1041
|
+
staticCommands: staticCommands.length > 0 ? staticCommands : behaviorCommands,
|
|
1042
|
+
behaviorCommands: behaviorCommands.length > 0 ? behaviorCommands : staticCommands,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
876
1045
|
function deriveFrontendBehaviorPaths(taskConfig) {
|
|
877
1046
|
if (taskConfig.allowedPaths.length === 0)
|
|
878
1047
|
return ["**"];
|
|
@@ -1507,6 +1676,7 @@ function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbid
|
|
|
1507
1676
|
commandSource: commands.length > 0 ? "inline" : "adapter",
|
|
1508
1677
|
commands: verifyCommands.length > 0 ? verifyCommands : undefined,
|
|
1509
1678
|
fallbackCommands: [],
|
|
1679
|
+
commandTexts: commands,
|
|
1510
1680
|
}),
|
|
1511
1681
|
cwd: ".",
|
|
1512
1682
|
timeoutMs: 300000,
|
|
@@ -1688,7 +1858,7 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
1688
1858
|
return { ...task, depends_on };
|
|
1689
1859
|
});
|
|
1690
1860
|
}
|
|
1691
|
-
function buildFrontendHybridDagFromTask(sources) {
|
|
1861
|
+
async function buildFrontendHybridDagFromTask(sources) {
|
|
1692
1862
|
const { taskConfig } = sources;
|
|
1693
1863
|
const mockCapability = sources.frontendMockCapability ?? {
|
|
1694
1864
|
status: "absent",
|
|
@@ -1756,6 +1926,8 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1756
1926
|
"- verificationTargets is a TOP-LEVEL required array",
|
|
1757
1927
|
"- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
|
|
1758
1928
|
"- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
|
|
1929
|
+
"- All implementation files, verification files, symbols, and commands must be discovered from the current target workspace and current task. Never copy paths, symbols, or commands from the loop-agent repository, an example task, or prior run output.",
|
|
1930
|
+
"- Use relative POSIX paths rooted at the target workspace. Do not assume a particular src/test directory layout; preserve the target project's actual app/, packages/, spec/, __tests__, or other layout.",
|
|
1759
1931
|
].join("\n");
|
|
1760
1932
|
})();
|
|
1761
1933
|
const sourceContext = [
|
|
@@ -1797,26 +1969,42 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1797
1969
|
if (mockMode === "blocked") {
|
|
1798
1970
|
return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints);
|
|
1799
1971
|
}
|
|
1800
|
-
const
|
|
1801
|
-
const
|
|
1972
|
+
const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
|
|
1973
|
+
const staticFallbackCommands = fallbackVerifyCommands.staticCommands;
|
|
1974
|
+
const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
|
|
1802
1975
|
const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
|
|
1803
1976
|
repoRoot: sources.repoRoot,
|
|
1804
1977
|
requirementMarkdown: sources.requirementMarkdown,
|
|
1805
1978
|
constraintMarkdown: sources.constraintMarkdown,
|
|
1806
1979
|
});
|
|
1980
|
+
const explicitFrontendVerifyCommands = buildExplicitFrontendVerifyCommands(taskConfig, sources.repoRoot);
|
|
1981
|
+
const explicitCommandKeys = new Set([...explicitFrontendVerifyCommands.staticCommands, ...explicitFrontendVerifyCommands.behaviorCommands].map(verifyCommandKey));
|
|
1982
|
+
const adapterVerifyCommands = (sources.verifyCommands?.final ?? []).filter((command) => !explicitCommandKeys.has(verifyCommandKey(command)));
|
|
1807
1983
|
const staticVerifyCommands = chooseFrontendVerifyCommands({
|
|
1984
|
+
explicitCommands: explicitFrontendVerifyCommands.staticCommands,
|
|
1808
1985
|
parsedCommands: parsedFrontendVerifyCommands.staticCommands,
|
|
1809
|
-
adapterCommands:
|
|
1986
|
+
adapterCommands: adapterVerifyCommands,
|
|
1987
|
+
});
|
|
1988
|
+
const partitionedStaticVerifyCommands = partitionFrontendStaticVerifyCommands({
|
|
1989
|
+
repoRoot: sources.repoRoot,
|
|
1990
|
+
commands: staticVerifyCommands.commands,
|
|
1991
|
+
commandSource: staticVerifyCommands.commandSource,
|
|
1810
1992
|
});
|
|
1811
1993
|
const behaviorVerifyCommands = chooseFrontendVerifyCommands({
|
|
1994
|
+
explicitCommands: explicitFrontendVerifyCommands.behaviorCommands,
|
|
1812
1995
|
parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
|
|
1813
|
-
adapterCommands:
|
|
1996
|
+
adapterCommands: adapterVerifyCommands,
|
|
1814
1997
|
});
|
|
1815
1998
|
const staticShellCommands = buildVerifyShellCommands({
|
|
1816
1999
|
repoRoot: sources.repoRoot,
|
|
1817
|
-
commands:
|
|
2000
|
+
commands: partitionedStaticVerifyCommands.static.commands,
|
|
1818
2001
|
fallbackCommands: staticFallbackCommands,
|
|
1819
2002
|
});
|
|
2003
|
+
const lintShellCommands = buildVerifyShellCommands({
|
|
2004
|
+
repoRoot: sources.repoRoot,
|
|
2005
|
+
commands: partitionedStaticVerifyCommands.lint.commands,
|
|
2006
|
+
fallbackCommands: [],
|
|
2007
|
+
});
|
|
1820
2008
|
const behaviorShellCommands = buildVerifyShellCommands({
|
|
1821
2009
|
repoRoot: sources.repoRoot,
|
|
1822
2010
|
commands: behaviorVerifyCommands.commands,
|
|
@@ -1825,16 +2013,28 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1825
2013
|
const staticVerifyEvidence = buildVerifyEvidence({
|
|
1826
2014
|
phase: "intermediate",
|
|
1827
2015
|
quota: strategy.intermediateQuota ?? "full",
|
|
1828
|
-
commandSource:
|
|
1829
|
-
commands:
|
|
2016
|
+
commandSource: partitionedStaticVerifyCommands.static.commandSource,
|
|
2017
|
+
commands: partitionedStaticVerifyCommands.static.commands,
|
|
1830
2018
|
fallbackCommands: staticFallbackCommands,
|
|
2019
|
+
commandTexts: staticShellCommands,
|
|
1831
2020
|
});
|
|
2021
|
+
const lintVerifyEvidence = lintShellCommands.length > 0
|
|
2022
|
+
? buildVerifyEvidence({
|
|
2023
|
+
phase: "intermediate",
|
|
2024
|
+
quota: strategy.intermediateQuota ?? "full",
|
|
2025
|
+
commandSource: partitionedStaticVerifyCommands.lint.commandSource,
|
|
2026
|
+
commands: partitionedStaticVerifyCommands.lint.commands,
|
|
2027
|
+
fallbackCommands: [],
|
|
2028
|
+
commandTexts: lintShellCommands,
|
|
2029
|
+
})
|
|
2030
|
+
: undefined;
|
|
1832
2031
|
const behaviorVerifyEvidence = buildVerifyEvidence({
|
|
1833
2032
|
phase: "final",
|
|
1834
2033
|
quota: "full",
|
|
1835
2034
|
commandSource: behaviorVerifyCommands.commandSource,
|
|
1836
2035
|
commands: behaviorVerifyCommands.commands,
|
|
1837
2036
|
fallbackCommands: behaviorFallbackCommands,
|
|
2037
|
+
commandTexts: behaviorShellCommands,
|
|
1838
2038
|
finalFullRequired: true,
|
|
1839
2039
|
});
|
|
1840
2040
|
const mockVerifyTemplate = mockMode === "required" && hasMockVerifyCommands
|
|
@@ -1897,6 +2097,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1897
2097
|
subtask_prompt: [
|
|
1898
2098
|
"Inspect frontend code, routing, components, styles, package scripts, and tests.",
|
|
1899
2099
|
"Return code and design observations, existing reuse opportunities, and verification entry points.",
|
|
2100
|
+
"Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
|
|
1900
2101
|
"Read-only: do not modify repository files.",
|
|
1901
2102
|
sourceContext,
|
|
1902
2103
|
].join("\n\n"),
|
|
@@ -1921,6 +2122,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1921
2122
|
"Based on frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence, return a minimal frontend implementation plan.",
|
|
1922
2123
|
"Select the Mock / API strategy inside the plan and structured contract. Carry endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into both outputs.",
|
|
1923
2124
|
"Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, Mock/API strategy, dependency policy, deterministic verification entrypoints, and residual risks. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
2125
|
+
"Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
|
|
1924
2126
|
"End with exactly one fenced json object conforming to frontend-implementation-contract-v1 so small topology can materialize the contract without plan-revision.",
|
|
1925
2127
|
requirementCoverageInstruction,
|
|
1926
2128
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
@@ -2042,16 +2244,47 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2042
2244
|
: ["native", "browser-intercept", "request-adapter", "not-needed"],
|
|
2043
2245
|
artifactName: "frontend-implementation-contract.json",
|
|
2044
2246
|
outputDir: "contracts",
|
|
2247
|
+
requireSourceFreshness: true,
|
|
2248
|
+
implementationWriteSet: implementPaths.writeSet,
|
|
2045
2249
|
openspecCandidatePaths: sources.frontendProjectCapability?.designEvidence.normativePaths ?? [],
|
|
2046
2250
|
},
|
|
2047
2251
|
cwd: ".",
|
|
2048
2252
|
timeoutMs: 60000,
|
|
2049
2253
|
},
|
|
2050
2254
|
},
|
|
2255
|
+
...(lintShellCommands.length > 0 && lintVerifyEvidence
|
|
2256
|
+
? [
|
|
2257
|
+
{
|
|
2258
|
+
id: "frontend-lint-baseline-shell",
|
|
2259
|
+
depends_on: ["frontend-prewrite-gate-shell"],
|
|
2260
|
+
role: "verifier",
|
|
2261
|
+
executor: "shell",
|
|
2262
|
+
complexity: "LOW",
|
|
2263
|
+
writePolicy: "read-only",
|
|
2264
|
+
allowedPaths: readOnlyPaths,
|
|
2265
|
+
forbiddenPaths,
|
|
2266
|
+
outputContract: "Capture writer-preceding lint output as frontend-lint-baseline-v1 without treating existing lint diagnostics as writer failure.",
|
|
2267
|
+
subtask_prompt: "Run the frozen lint commands read-only. Preserve raw output and mark the baseline unavailable on timeout, execution failure, unparseable output, or worktree mutation.",
|
|
2268
|
+
shell: {
|
|
2269
|
+
commands: lintShellCommands,
|
|
2270
|
+
frontendLintBaseline: {
|
|
2271
|
+
schemaVersion: 1,
|
|
2272
|
+
lintCommands: lintShellCommands,
|
|
2273
|
+
lintEvidence: lintVerifyEvidence,
|
|
2274
|
+
},
|
|
2275
|
+
cwd: ".",
|
|
2276
|
+
timeoutMs: 300000,
|
|
2277
|
+
},
|
|
2278
|
+
},
|
|
2279
|
+
]
|
|
2280
|
+
: []),
|
|
2051
2281
|
{
|
|
2052
2282
|
id: implementId,
|
|
2053
2283
|
depends_on: [
|
|
2054
2284
|
"frontend-prewrite-gate-shell",
|
|
2285
|
+
...(lintShellCommands.length > 0
|
|
2286
|
+
? ["frontend-lint-baseline-shell"]
|
|
2287
|
+
: []),
|
|
2055
2288
|
"frontend-plan-revision-pi",
|
|
2056
2289
|
"frontend-final-design-review-pi",
|
|
2057
2290
|
"frontend-plan-pi",
|
|
@@ -2098,11 +2331,17 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2098
2331
|
frontendVerificationBundle: {
|
|
2099
2332
|
schemaVersion: 1,
|
|
2100
2333
|
mockCommands: mockShellCommands,
|
|
2334
|
+
lintCommands: lintShellCommands,
|
|
2101
2335
|
staticCommands: staticShellCommands,
|
|
2102
2336
|
behaviorCommands: behaviorShellCommands,
|
|
2103
2337
|
mockEvidence: mockVerifyEvidence,
|
|
2338
|
+
lintEvidence: lintVerifyEvidence,
|
|
2104
2339
|
staticEvidence: staticVerifyEvidence,
|
|
2105
2340
|
behaviorEvidence: behaviorVerifyEvidence,
|
|
2341
|
+
lintBaselineNodeId: lintShellCommands.length > 0
|
|
2342
|
+
? "frontend-lint-baseline-shell"
|
|
2343
|
+
: undefined,
|
|
2344
|
+
writerNodeIds: lintShellCommands.length > 0 ? [implementId] : [],
|
|
2106
2345
|
mode: "initial",
|
|
2107
2346
|
},
|
|
2108
2347
|
cwd: ".",
|
|
@@ -2151,11 +2390,19 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2151
2390
|
frontendVerificationBundle: {
|
|
2152
2391
|
schemaVersion: 1,
|
|
2153
2392
|
mockCommands: mockShellCommands,
|
|
2393
|
+
lintCommands: lintShellCommands,
|
|
2154
2394
|
staticCommands: staticShellCommands,
|
|
2155
2395
|
behaviorCommands: behaviorShellCommands,
|
|
2156
2396
|
mockEvidence: mockVerifyEvidence,
|
|
2397
|
+
lintEvidence: lintVerifyEvidence,
|
|
2157
2398
|
staticEvidence: staticVerifyEvidence,
|
|
2158
2399
|
behaviorEvidence: behaviorVerifyEvidence,
|
|
2400
|
+
lintBaselineNodeId: lintShellCommands.length > 0
|
|
2401
|
+
? "frontend-lint-baseline-shell"
|
|
2402
|
+
: undefined,
|
|
2403
|
+
writerNodeIds: lintShellCommands.length > 0
|
|
2404
|
+
? [implementId, "frontend-repair-pi"]
|
|
2405
|
+
: [],
|
|
2159
2406
|
mode: "repair",
|
|
2160
2407
|
},
|
|
2161
2408
|
cwd: ".",
|
|
@@ -2177,11 +2424,11 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2177
2424
|
writePolicy: "read-only",
|
|
2178
2425
|
allowedPaths: readOnlyPaths,
|
|
2179
2426
|
forbiddenPaths,
|
|
2180
|
-
outputContract: "Canonical frontend review context containing validated contract, effective verification trace, repair assessment, and actual worktree diff.",
|
|
2427
|
+
outputContract: "Canonical frontend review context containing validated contract, lint assessment when configured, effective verification trace, repair assessment, and actual worktree diff.",
|
|
2181
2428
|
subtask_prompt: "Capture the actual diff and bind it to the effective initial-or-post-repair verification evidence for final review.",
|
|
2182
2429
|
shell: {
|
|
2183
2430
|
commands: [],
|
|
2184
|
-
frontendReviewContext: { schemaVersion: 1 },
|
|
2431
|
+
frontendReviewContext: { schemaVersion: 1, requireBaseline: true },
|
|
2185
2432
|
cwd: ".",
|
|
2186
2433
|
timeoutMs: 120000,
|
|
2187
2434
|
},
|
|
@@ -2213,7 +2460,8 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2213
2460
|
"Review the frontend implementation and verification evidence.",
|
|
2214
2461
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2215
2462
|
"Any Critical or Important finding must force VERDICT: request-revision.",
|
|
2216
|
-
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
|
|
2463
|
+
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
|
|
2464
|
+
"Treat lint status exactly as passed | baseline-debt | failed | unavailable. baseline-debt may continue only with intact evidence and zero diagnostics on writer-changed files; report the tolerated debt count and never rewrite it as lint passed. Typecheck, build, and test still require successful final exits.",
|
|
2217
2465
|
"Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
|
|
2218
2466
|
"Use the direct contract and the effective plan/design branch: original plan plus initial pass when revision was skipped, or revised plan plus final design review when revision ran. Do not infer them from the implementation summary.",
|
|
2219
2467
|
"Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence. not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case verify that the real request remains the default and the Real Integration Gap is preserved.",
|
|
@@ -2271,7 +2519,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
2271
2519
|
outputContract: "Markdown closeout summary with Changes, Mock Decision / Strategy / Files / Verification / Production Boundary, Verification Evidence, Review Result, Frontend Status, Real Integration Status, Known Risks, and Follow-up. No file writes.",
|
|
2272
2520
|
subtask_prompt: [
|
|
2273
2521
|
"Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
|
|
2274
|
-
"Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use contracts/frontend-review-context.json and the effective frontend-verify-assess-shell or frontend-reverify-shell facts; do not invent Browser evidence from component tests.",
|
|
2522
|
+
"Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Report lint separately as passed|baseline-debt|failed|unavailable; baseline-debt is explicit debt, not passed. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use contracts/frontend-review-context.json and the effective frontend-verify-assess-shell or frontend-reverify-shell facts; do not invent Browser evidence from component tests.",
|
|
2275
2523
|
`When only Mock-backed evidence passed, state exactly Frontend status: mock-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task to create/run after backend readiness. This follow-up is not auto-created or auto-executed. Never describe Mock evidence as real API integration.`,
|
|
2276
2524
|
`When Mock was skipped in auto mode and no real API evidence passed, state exactly Frontend status: locally-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task when backend readiness matters.`,
|
|
2277
2525
|
"Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
|
|
@@ -3100,7 +3348,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3100
3348
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
3101
3349
|
'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
|
|
3102
3350
|
].join("; ");
|
|
3103
|
-
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Validate JUnit, then render the primary self-contained Chinese HTML report from the same JUnit plus final Markdown case metadata without rerun. Keep 测试结论, quality status, failure overview, and a polished per-case result card with concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing valid JUnit with per-case captured output, self-contained HTML and reports/backend-test-facts.md; exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
|
|
3351
|
+
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Validate JUnit, then render the primary self-contained Chinese HTML report from the same JUnit plus final Markdown case metadata without rerun. Keep 测试结论, quality status, failure overview, and a polished per-case result card with concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing valid JUnit with per-case captured output, self-contained HTML, reports/backend-test.md and reports/backend-test-facts.md; exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
|
|
3104
3352
|
if (execute.shell) {
|
|
3105
3353
|
execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
3106
3354
|
}
|
|
@@ -3175,51 +3423,52 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3175
3423
|
const evidenceRoot = "testcase/frontend/evidence";
|
|
3176
3424
|
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
3177
3425
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
3426
|
+
const checklistScript = [
|
|
3427
|
+
"const fs=require('fs'),path=require('path');",
|
|
3428
|
+
"const root='testcase/frontend/cases';",
|
|
3429
|
+
"const draft=path.join(root,'manifest.draft.json');",
|
|
3430
|
+
"const final=path.join(root,'manifest.json');",
|
|
3431
|
+
"const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
|
|
3432
|
+
"if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
|
|
3433
|
+
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
|
|
3434
|
+
"if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
|
|
3435
|
+
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
3436
|
+
"const issues=[];",
|
|
3437
|
+
"const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
|
|
3438
|
+
"const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
|
|
3439
|
+
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;", ,
|
|
3440
|
+
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
3441
|
+
"for(const c of manifest.cases){",
|
|
3442
|
+
" const id=c&&c.caseId||'?';",
|
|
3443
|
+
" if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
|
|
3444
|
+
" if(typeof c.caseId==='string'&&/^AC-/i.test(c.caseId))issues.push({ruleId:'case-id-is-ac',caseId:id,detail:'do not use acceptance id as caseId; put AC-FE-* only in acIds'});",
|
|
3445
|
+
" const casePath=typeof c.casePath==='string'?c.casePath:null;",
|
|
3446
|
+
" if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
|
|
3447
|
+
" if(typeof c.caseId==='string'&&casePath!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
|
|
3448
|
+
" const body=fs.readFileSync(casePath,'utf8');",
|
|
3449
|
+
" if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>; playwright-cli is strongly recommended for browser execution'});", ,
|
|
3450
|
+
" const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
|
|
3451
|
+
" if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
|
|
3452
|
+
" if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required (AC-FE-* acceptance ids, not caseId)'});",
|
|
3453
|
+
" else {",
|
|
3454
|
+
" for(const ac of c.acIds){",
|
|
3455
|
+
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
3456
|
+
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
3457
|
+
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
3458
|
+
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
3459
|
+
" }",
|
|
3460
|
+
" }",
|
|
3461
|
+
"}",
|
|
3462
|
+
"if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
|
|
3463
|
+
"console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
|
|
3464
|
+
].join("");
|
|
3465
|
+
// Pass generated JavaScript as base64 rather than embedding it inside a
|
|
3466
|
+
// shell-quoted `node -e` argument. Git Bash otherwise consumes backslashes
|
|
3467
|
+
// such as \\s/\\d and interprets Markdown backticks before Node sees them.
|
|
3468
|
+
const checklistScriptBase64 = Buffer.from(checklistScript, "utf8").toString("base64");
|
|
3178
3469
|
const checklistValidation = [
|
|
3179
|
-
"node -e",
|
|
3180
|
-
|
|
3181
|
-
"const fs=require('fs'),path=require('path');",
|
|
3182
|
-
"const root='testcase/frontend/cases';",
|
|
3183
|
-
"const draft=path.join(root,'manifest.draft.json');",
|
|
3184
|
-
"const final=path.join(root,'manifest.json');",
|
|
3185
|
-
"const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
|
|
3186
|
-
"if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
|
|
3187
|
-
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
|
|
3188
|
-
"if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
|
|
3189
|
-
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
3190
|
-
"const issues=[];",
|
|
3191
|
-
"const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
|
|
3192
|
-
"const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
|
|
3193
|
-
"const codeRe=/\\b(pytest|playwright\\.test|@playwright\\/test)\\b/i;",
|
|
3194
|
-
"const barePwRe=/(?:^|[\\s\"'(])(?:npx\\s+playwright\\b|playwright\\s+test\\b|from\\s+['\"]@playwright\\/|require\\(['\"]@playwright\\/|import\\s+.*@playwright\\/|(?<![\\w-])playwright(?!-cli)\\b)/i;",
|
|
3195
|
-
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
|
|
3196
|
-
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
3197
|
-
"for(const c of manifest.cases){",
|
|
3198
|
-
" const id=c&&c.caseId||'?';",
|
|
3199
|
-
" if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
|
|
3200
|
-
" if(typeof c.caseId==='string'&&/^AC-/i.test(c.caseId))issues.push({ruleId:'case-id-is-ac',caseId:id,detail:'do not use acceptance id as caseId; put AC-FE-* only in acIds'});",
|
|
3201
|
-
" const casePath=typeof c.casePath==='string'?c.casePath:null;",
|
|
3202
|
-
" if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
|
|
3203
|
-
" if(typeof c.caseId==='string'&&casePath!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
|
|
3204
|
-
" const body=fs.readFileSync(casePath,'utf8');",
|
|
3205
|
-
" if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>'});",
|
|
3206
|
-
" const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
|
|
3207
|
-
" if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
|
|
3208
|
-
" if(codeRe.test(body))issues.push({ruleId:'no-test-source',caseId:id,detail:'pytest/playwright test source forbidden'});",
|
|
3209
|
-
" if(barePwRe.test(body))issues.push({ruleId:'playwright-cli-only',caseId:id,detail:'only playwright-cli skill commands allowed; bare Playwright CLI/API/test runner forbidden'});",
|
|
3210
|
-
" if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required (AC-FE-* acceptance ids, not caseId)'});",
|
|
3211
|
-
" else {",
|
|
3212
|
-
" for(const ac of c.acIds){",
|
|
3213
|
-
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
3214
|
-
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
3215
|
-
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
3216
|
-
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
3217
|
-
" }",
|
|
3218
|
-
" }",
|
|
3219
|
-
"}",
|
|
3220
|
-
"if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
|
|
3221
|
-
"console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
|
|
3222
|
-
].join("")),
|
|
3470
|
+
"node -e \"eval(Buffer.from(process.argv[1],'base64').toString('utf8'))\"",
|
|
3471
|
+
checklistScriptBase64,
|
|
3223
3472
|
].join(" ");
|
|
3224
3473
|
const manifestValidation = [
|
|
3225
3474
|
"node -e",
|
|
@@ -3260,6 +3509,23 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3260
3509
|
// Heal malformed/missing case evidence to blocked; only unsafe evidenceDir hard-fails.
|
|
3261
3510
|
const evidenceValidation = buildFrontendCaseEvidenceValidateShellSnippet();
|
|
3262
3511
|
const frontendTestOutcomeGate = buildFrontendTestOutcomeGateShellSnippet();
|
|
3512
|
+
const frontendCaseQualityAdvisory = [
|
|
3513
|
+
"node -e",
|
|
3514
|
+
JSON.stringify([
|
|
3515
|
+
"const fs=require('fs'),path=require('path');",
|
|
3516
|
+
"const runDir=process.env.HARNESS_DAG_RUN_DIR; if(!runDir)throw new Error('missing HARNESS_DAG_RUN_DIR');",
|
|
3517
|
+
"const resultPath=path.join(runDir,'contracts','frontend-test-result.json'); if(!fs.existsSync(resultPath))throw new Error('missing '+resultPath);",
|
|
3518
|
+
"const r=JSON.parse(fs.readFileSync(resultPath,'utf8')); const findings=Array.isArray(r.advisoryFindings)?r.advisoryFindings:[];",
|
|
3519
|
+
"const reports='testcase/frontend/reports'; fs.mkdirSync(reports,{recursive:true});",
|
|
3520
|
+
"const esc=v=>String(v??'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('\\\"','"');",
|
|
3521
|
+
"const labels={cases:'用例总数',passed:'通过',failed:'失败',blocked:'阻塞'}; const totals=r.totals||{}; const missing=r.acceptanceCoverage&&Array.isArray(r.acceptanceCoverage.missing)?r.acceptanceCoverage.missing:[];",
|
|
3522
|
+
"const md=['# 前端测试质量建议报告','','> Advisory:以下 finding 用于改进测试资产和证据质量,不阻塞后续流程。','','## 执行摘要','',...Object.entries(labels).map(([k,v])=>'- '+v+': '+Number(totals[k]||0)),'- 执行结果: '+String(r.outcome||'unknown'),'- 缺失 AC: '+(missing.join(', ')||'无'),'','## 建议项','',...(findings.length?findings.map(f=>'- ['+f.ruleId+']'+(f.caseId?' '+f.caseId:'')+': '+f.detail):['- 未发现建议项。']),''];",
|
|
3523
|
+
"fs.writeFileSync(path.join(reports,'frontend-test-case-quality-advisory.md'),md.join('\\n'));",
|
|
3524
|
+
"const rows=findings.length?findings.map(f=>'<tr><td><code>'+esc(f.ruleId)+'</code></td><td>'+esc(f.caseId||'-')+'</td><td>'+esc(f.detail)+'</td></tr>').join(''):'<tr><td colspan=3>未发现建议项。</td></tr>';",
|
|
3525
|
+
"const html='<!doctype html><html lang=\\\"zh-CN\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>前端测试质量建议报告</title><style>body{font:16px system-ui,Microsoft YaHei,sans-serif;background:#f5f7fb;color:#172033;margin:0;padding:32px}main{max-width:1100px;margin:auto;background:#fff;padding:32px;border-radius:16px}.badge{display:inline-block;padding:6px 10px;border-radius:999px;background:#fff3cd;color:#946200}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.card{padding:16px;background:#f8fafc;border-radius:10px}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:10px;border-bottom:1px solid #e5e7eb}code{color:#475467}@media(max-width:700px){.grid{grid-template-columns:1fr 1fr}}</style></head><body><main><h1>前端测试质量建议报告</h1><p><span class=\\\"badge\\\">Advisory,不阻塞后续流程</span></p><h2>执行摘要</h2><div class=\\\"grid\\\">'+Object.entries(labels).map(([k,v])=>'<div class=\\\"card\\\"><strong>'+v+'</strong><div>'+Number(totals[k]||0)+'</div></div>').join('')+'</div><p>执行结果:'+esc(r.outcome||'unknown')+';缺失 AC:'+esc(missing.join(', ')||'无')+'</p><h2>建议项</h2><table><thead><tr><th>规则</th><th>Case ID</th><th>说明与建议</th></tr></thead><tbody>'+rows+'</tbody></table></main></body></html>';",
|
|
3526
|
+
"fs.writeFileSync(path.join(reports,'frontend-test-case-quality-advisory.html'),html); console.log('frontend-test advisory report findings='+findings.length);",
|
|
3527
|
+
].join("")),
|
|
3528
|
+
].join(" ");
|
|
3263
3529
|
const tasks = [
|
|
3264
3530
|
{
|
|
3265
3531
|
id: "retrieve-frontend-test-context-pi",
|
|
@@ -3413,8 +3679,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3413
3679
|
writePolicy: "read-only",
|
|
3414
3680
|
allowedPaths: [...ragWriteSet, ...casesWriteSet],
|
|
3415
3681
|
forbiddenPaths: forbidden,
|
|
3416
|
-
outputContract: "Mechanical checklist:
|
|
3417
|
-
subtask_prompt: "Scan generated cases/manifest
|
|
3682
|
+
outputContract: "Mechanical checklist: manifest/case paths, Case ID, non-production playwright-cli open prefix, and AC mapping; alternative executable tool commands are not inspected or blocked.",
|
|
3683
|
+
subtask_prompt: "Scan generated cases/manifest for structural and safety rules only. Strongly recommend playwright-cli for browser execution, but do not inspect or reject alternative executable tool commands and do not use free-form LLM verdicts.",
|
|
3418
3684
|
shell: { commands: [checklistValidation], cwd: ".", timeoutMs: 120000 },
|
|
3419
3685
|
}, {
|
|
3420
3686
|
id: "materialize-frontend-case-manifest-shell",
|
|
@@ -3548,6 +3814,20 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3548
3814
|
outputContract: "Write frontend-test retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, execution evidence review, risks, findings, and A/B/C/D rating — even when outcome is failed/incomplete. Pipeline acceptance = this report exists (not case 100% pass).",
|
|
3549
3815
|
subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization (do not wait for outcome=pass). Combine AC→case→browser-evidence review with the closeout report: coverage, passed/failed/blocked (including token-budget-exhausted / executor-auth-unavailable), evidence gaps, browser anomalies, residual risks, and A/B/C/D rating. Passed cases need assertion plus screenshot or equivalent evidence when available; failed/blocked need explicit reasons. Blocked cases never count as passed. Do not replace browser evidence with model conclusions. Do not write docs/**. Pipeline success is report production, not case full green.",
|
|
3550
3816
|
});
|
|
3817
|
+
tasks.push({
|
|
3818
|
+
id: "frontend-test-quality-report-html-shell",
|
|
3819
|
+
depends_on: ["frontend-test-retrospect-pi"],
|
|
3820
|
+
role: "verifier",
|
|
3821
|
+
executor: "shell",
|
|
3822
|
+
complexity: "LOW",
|
|
3823
|
+
writePolicy: "exclusive",
|
|
3824
|
+
writeSet: ["testcase/frontend/reports/**"],
|
|
3825
|
+
allowedPaths: ["testcase/frontend/**"],
|
|
3826
|
+
forbiddenPaths: forbidden,
|
|
3827
|
+
outputContract: "Write a user-readable HTML advisory report after the final Markdown retrospective; findings never block the workflow.",
|
|
3828
|
+
subtask_prompt: "Generate the frontend case quality advisory Markdown and HTML report after the final test report. Record content findings only and never fail because findings exist.",
|
|
3829
|
+
shell: { commands: [frontendCaseQualityAdvisory], cwd: ".", timeoutMs: 120000 },
|
|
3830
|
+
});
|
|
3551
3831
|
const globalConstraints = [
|
|
3552
3832
|
...sources.taskConfig.hardConstraints,
|
|
3553
3833
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
@@ -4570,7 +4850,7 @@ async function buildHybridDagForTemplate(sources, template) {
|
|
|
4570
4850
|
const taskContractBinding = await resolveTaskContractBindingForGenerate(sources);
|
|
4571
4851
|
let spec;
|
|
4572
4852
|
if (template === "frontend-implementation") {
|
|
4573
|
-
spec = buildFrontendHybridDagFromTask(sources);
|
|
4853
|
+
spec = await buildFrontendHybridDagFromTask(sources);
|
|
4574
4854
|
}
|
|
4575
4855
|
else if (template === "frontend-test-dag")
|
|
4576
4856
|
spec = buildFrontendTestHybridDag(sources);
|