@arcadialdev/arcality 4.1.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.agents/skills/db-validation-evidence/SKILL.md +43 -0
  2. package/.agents/skills/db-validation-evidence/agents/openai.yaml +4 -0
  3. package/.agents/skills/db-validation-evidence/references/mission-patterns.md +130 -0
  4. package/.agents/skills/evidence-synthesis-reporting/SKILL.md +31 -0
  5. package/.agents/skills/form-expert/SKILL.md +98 -0
  6. package/.agents/skills/investigation-protocol/SKILL.md +56 -0
  7. package/.agents/skills/mission-generation-reviewer/SKILL.md +25 -0
  8. package/.agents/skills/modal-master/SKILL.md +46 -0
  9. package/.agents/skills/native-control-expert/SKILL.md +74 -0
  10. package/.agents/skills/qa-context-governance/SKILL.md +23 -0
  11. package/.agents/skills/security-evidence-triage/SKILL.md +23 -0
  12. package/.agents/skills/test-data-lifecycle/SKILL.md +24 -0
  13. package/README.md +103 -163
  14. package/bin/arcality.mjs +25 -25
  15. package/package.json +75 -75
  16. package/scripts/edit-config.mjs +843 -0
  17. package/scripts/gen-and-run.mjs +260 -170
  18. package/scripts/generate.mjs +236 -236
  19. package/scripts/init.mjs +47 -47
  20. package/src/configManager.mjs +13 -13
  21. package/src/envSetup.ts +229 -205
  22. package/src/services/codebaseAnalyzer.mjs +59 -59
  23. package/src/services/databaseValidationService.mjs +598 -0
  24. package/src/services/executionEvidenceService.mjs +124 -0
  25. package/src/services/generatedMissionSchema.mjs +76 -76
  26. package/src/services/generatedMissionStore.mjs +117 -117
  27. package/src/services/generationContext.mjs +242 -242
  28. package/src/services/mcpStdioClient.mjs +204 -0
  29. package/src/services/missionGenerator.mjs +329 -329
  30. package/src/services/routeDiscovery.mjs +762 -762
  31. package/tests/_helpers/ArcalityReporter.js +1342 -1255
  32. package/tests/_helpers/agentic-runner.bundle.spec.js +1377 -244
  33. package/.agent/skills/form-expert.md +0 -102
  34. package/.agent/skills/investigation-protocol.md +0 -61
  35. package/.agent/skills/modal-master.md +0 -41
  36. package/.agent/skills/native-control-expert.md +0 -82
@@ -39,7 +39,7 @@ __export(asset_resolver_exports, {
39
39
  syncAssetsFromBackend: () => syncAssetsFromBackend
40
40
  });
41
41
  function readJsonFile(filePath) {
42
- const raw = fs2.readFileSync(filePath, "utf8").trim();
42
+ const raw = fs3.readFileSync(filePath, "utf8").trim();
43
43
  if (!raw)
44
44
  return null;
45
45
  return JSON.parse(raw);
@@ -48,7 +48,7 @@ function sanitizeIdPart(value) {
48
48
  return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80);
49
49
  }
50
50
  function normalizeMimeFromPath(filePath) {
51
- const ext = path2.extname(filePath || "").toLowerCase();
51
+ const ext = path4.extname(filePath || "").toLowerCase();
52
52
  if (ext === ".jpg" || ext === ".jpeg")
53
53
  return "image/jpeg";
54
54
  if (ext === ".webp")
@@ -72,16 +72,16 @@ function inferAssetType(...values) {
72
72
  function normalizeAsset(raw, assetsDir = null) {
73
73
  const assetId = String(raw.asset_id || raw.assetId || raw.id || "").trim();
74
74
  const rawPath = String(raw.path || raw.file_path || raw.filePath || raw.storage_key || raw.storageKey || "").trim();
75
- const filename = String(raw.filename || raw.fileName || (rawPath ? path2.basename(rawPath) : "") || "").trim();
76
- const derivedId = filename ? `asset_${sanitizeIdPart(path2.basename(filename, path2.extname(filename)))}` : "";
75
+ const filename = String(raw.filename || raw.fileName || (rawPath ? path4.basename(rawPath) : "") || "").trim();
76
+ const derivedId = filename ? `asset_${sanitizeIdPart(path4.basename(filename, path4.extname(filename)))}` : "";
77
77
  const finalId = assetId || derivedId;
78
78
  if (!finalId)
79
79
  return null;
80
80
  let filePath = rawPath || void 0;
81
- if (filePath && !path2.isAbsolute(filePath)) {
82
- const cwdCandidate = path2.resolve(process.cwd(), filePath);
83
- const assetDirCandidate = assetsDir ? path2.resolve(assetsDir, filePath) : "";
84
- filePath = fs2.existsSync(cwdCandidate) ? cwdCandidate : assetDirCandidate || cwdCandidate;
81
+ if (filePath && !path4.isAbsolute(filePath)) {
82
+ const cwdCandidate = path4.resolve(process.cwd(), filePath);
83
+ const assetDirCandidate = assetsDir ? path4.resolve(assetsDir, filePath) : "";
84
+ filePath = fs3.existsSync(cwdCandidate) ? cwdCandidate : assetDirCandidate || cwdCandidate;
85
85
  }
86
86
  const label = String(raw.label || raw.user_label || raw.userLabel || filename || finalId).trim();
87
87
  const type = String(raw.type || inferAssetType(label, filename)).trim().toLowerCase();
@@ -99,14 +99,14 @@ function normalizeAsset(raw, assetsDir = null) {
99
99
  };
100
100
  }
101
101
  function getDefaultAssetsDir() {
102
- return process.env.ARCALITY_ASSETS_DIR || path2.join(process.cwd(), ".arcality", "assets");
102
+ return process.env.ARCALITY_ASSETS_DIR || path4.join(process.cwd(), ".arcality", "assets");
103
103
  }
104
104
  function loadManifestAssets(assetsDir) {
105
105
  const explicitFile = process.env.ARCALITY_ASSETS_FILE;
106
- const manifestPath = explicitFile || path2.join(process.cwd(), ".arcality", "assets.json");
106
+ const manifestPath = explicitFile || path4.join(process.cwd(), ".arcality", "assets.json");
107
107
  const inlineJson = process.env.ARCALITY_ASSETS_JSON;
108
108
  try {
109
- const parsed = inlineJson ? JSON.parse(inlineJson) : fs2.existsSync(manifestPath) ? readJsonFile(manifestPath) : null;
109
+ const parsed = inlineJson ? JSON.parse(inlineJson) : fs3.existsSync(manifestPath) ? readJsonFile(manifestPath) : null;
110
110
  const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.assets) ? parsed.assets : [];
111
111
  return list.map((entry) => normalizeAsset(entry, assetsDir)).filter(Boolean);
112
112
  } catch (e) {
@@ -116,13 +116,13 @@ function loadManifestAssets(assetsDir) {
116
116
  }
117
117
  function scanAssetsDir(assetsDir) {
118
118
  try {
119
- if (!fs2.existsSync(assetsDir))
119
+ if (!fs3.existsSync(assetsDir))
120
120
  return [];
121
- return fs2.readdirSync(assetsDir, { withFileTypes: true }).filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path2.extname(entry.name).toLowerCase())).map((entry) => {
122
- const filePath = path2.join(assetsDir, entry.name);
121
+ return fs3.readdirSync(assetsDir, { withFileTypes: true }).filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path4.extname(entry.name).toLowerCase())).map((entry) => {
122
+ const filePath = path4.join(assetsDir, entry.name);
123
123
  return normalizeAsset({
124
- id: `asset_${sanitizeIdPart(path2.basename(entry.name, path2.extname(entry.name)))}`,
125
- label: path2.basename(entry.name, path2.extname(entry.name)).replace(/[_-]+/g, " "),
124
+ id: `asset_${sanitizeIdPart(path4.basename(entry.name, path4.extname(entry.name)))}`,
125
+ label: path4.basename(entry.name, path4.extname(entry.name)).replace(/[_-]+/g, " "),
126
126
  path: filePath,
127
127
  mime: normalizeMimeFromPath(filePath),
128
128
  type: inferAssetType(entry.name)
@@ -247,7 +247,7 @@ function resolveUploadAsset(value, label = "file upload") {
247
247
  const subtype = raw.startsWith("placeholder:image/") ? raw.substring("placeholder:image/".length).toLowerCase() : "";
248
248
  const targetType = subtype || inferAssetType(label, raw);
249
249
  const matchingAsset = assets.find((a) => a.type === targetType || a.label.toLowerCase().includes(targetType)) || assets.find((a) => !a.isDefault);
250
- if (matchingAsset?.filePath && fs2.existsSync(matchingAsset.filePath)) {
250
+ if (matchingAsset?.filePath && fs3.existsSync(matchingAsset.filePath)) {
251
251
  console.log(`>>ARCALITY_STATUS>> Usando asset real configurado para el placeholder "${raw}": ${matchingAsset.filePath}`);
252
252
  return matchingAsset.filePath;
253
253
  }
@@ -255,14 +255,14 @@ function resolveUploadAsset(value, label = "file upload") {
255
255
  }
256
256
  if (/^arcasset:\/\//i.test(raw) || /^asset_[a-z0-9_-]+$/i.test(raw)) {
257
257
  const asset = findAsset(raw);
258
- if (asset?.filePath && fs2.existsSync(asset.filePath)) {
258
+ if (asset?.filePath && fs3.existsSync(asset.filePath)) {
259
259
  return asset.filePath;
260
260
  }
261
261
  console.warn(`>>ARCALITY_STATUS>> Asset "${raw}" no tiene archivo local disponible. Usando placeholder.`);
262
262
  return createPlaceholderUpload(label, raw);
263
263
  }
264
- const filePath = path2.isAbsolute(raw) ? raw : path2.resolve(process.cwd(), raw);
265
- if (fs2.existsSync(filePath)) {
264
+ const filePath = path4.isAbsolute(raw) ? raw : path4.resolve(process.cwd(), raw);
265
+ if (fs3.existsSync(filePath)) {
266
266
  return filePath;
267
267
  }
268
268
  console.warn(`>>ARCALITY_STATUS>> Archivo de upload no encontrado: "${raw}". Usando placeholder.`);
@@ -278,8 +278,8 @@ async function syncAssetsFromBackend() {
278
278
  }
279
279
  try {
280
280
  const assetsDir = getDefaultAssetsDir();
281
- if (!fs2.existsSync(assetsDir)) {
282
- fs2.mkdirSync(assetsDir, { recursive: true });
281
+ if (!fs3.existsSync(assetsDir)) {
282
+ fs3.mkdirSync(assetsDir, { recursive: true });
283
283
  }
284
284
  const url = `${apiUrl}/api/v1/projects/${projectId}/assets`;
285
285
  console.log(`>>ARCALITY_STATUS>> \u{1F4E6} Consultando assets configurados del proyecto...`);
@@ -314,14 +314,14 @@ async function syncAssetsFromBackend() {
314
314
  continue;
315
315
  }
316
316
  const prefix = `${assetId}_`;
317
- const files = fs2.existsSync(assetsDir) ? fs2.readdirSync(assetsDir) : [];
317
+ const files = fs3.existsSync(assetsDir) ? fs3.readdirSync(assetsDir) : [];
318
318
  const existingFile = files.find((f) => f.startsWith(prefix));
319
319
  let filename = asset.filename || asset.fileName;
320
320
  let localFilePath = "";
321
321
  let downloadNeeded = true;
322
322
  if (existingFile) {
323
- const candidatePath = path2.join(assetsDir, existingFile);
324
- if (fs2.existsSync(candidatePath) && fs2.statSync(candidatePath).size > 0) {
323
+ const candidatePath = path4.join(assetsDir, existingFile);
324
+ if (fs3.existsSync(candidatePath) && fs3.statSync(candidatePath).size > 0) {
325
325
  filename = filename || existingFile.substring(prefix.length);
326
326
  localFilePath = candidatePath;
327
327
  downloadNeeded = false;
@@ -342,13 +342,13 @@ async function syncAssetsFromBackend() {
342
342
  const matFilename = matData.filename || matData.fileName || filename || `${assetId}.png`;
343
343
  filename = matFilename;
344
344
  const localFilename = `${assetId}_${matFilename}`;
345
- localFilePath = path2.join(assetsDir, localFilename);
345
+ localFilePath = path4.join(assetsDir, localFilename);
346
346
  if (downloadUrl) {
347
347
  console.log(`>>ARCALITY_STATUS>> \u{1F4E5} Descargando archivo de asset: "${asset.label}"...`);
348
348
  const fileRes = await globalThis.fetch(downloadUrl);
349
349
  if (fileRes.ok) {
350
350
  const buffer = Buffer.from(await fileRes.arrayBuffer());
351
- fs2.writeFileSync(localFilePath, buffer);
351
+ fs3.writeFileSync(localFilePath, buffer);
352
352
  console.log(`>>ARCALITY_STATUS>> \u2705 Asset guardado localmente: ${localFilePath}`);
353
353
  } else {
354
354
  console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al descargar archivo de asset: ${fileRes.status}`);
@@ -357,7 +357,7 @@ async function syncAssetsFromBackend() {
357
357
  } else {
358
358
  console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al materializar asset: ${matRes.status}`);
359
359
  filename = filename || `${assetId}.png`;
360
- localFilePath = path2.join(assetsDir, `${assetId}_${filename}`);
360
+ localFilePath = path4.join(assetsDir, `${assetId}_${filename}`);
361
361
  }
362
362
  }
363
363
  localAssets.push({
@@ -373,12 +373,12 @@ async function syncAssetsFromBackend() {
373
373
  is_default: asset.is_default || asset.isDefault || false
374
374
  });
375
375
  }
376
- const manifestPath = process.env.ARCALITY_ASSETS_FILE || path2.join(process.cwd(), ".arcality", "assets.json");
377
- const manifestDir = path2.dirname(manifestPath);
378
- if (!fs2.existsSync(manifestDir)) {
379
- fs2.mkdirSync(manifestDir, { recursive: true });
376
+ const manifestPath = process.env.ARCALITY_ASSETS_FILE || path4.join(process.cwd(), ".arcality", "assets.json");
377
+ const manifestDir = path4.dirname(manifestPath);
378
+ if (!fs3.existsSync(manifestDir)) {
379
+ fs3.mkdirSync(manifestDir, { recursive: true });
380
380
  }
381
- fs2.writeFileSync(manifestPath, JSON.stringify(localAssets, null, 2), "utf8");
381
+ fs3.writeFileSync(manifestPath, JSON.stringify(localAssets, null, 2), "utf8");
382
382
  console.log(`>>ARCALITY_STATUS>> \u2705 Inventario local de assets actualizado (${localAssets.length}).`);
383
383
  if (process.env.DEBUG) {
384
384
  console.log(`[AssetSync] Local manifest updated at ${manifestPath}`);
@@ -387,11 +387,11 @@ async function syncAssetsFromBackend() {
387
387
  console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error durante la sincronizaci\xF3n de assets: ${e.message}`);
388
388
  }
389
389
  }
390
- var fs2, path2, zlib, IMAGE_EXTENSIONS;
390
+ var fs3, path4, zlib, IMAGE_EXTENSIONS;
391
391
  var init_asset_resolver = __esm({
392
392
  "tests/_helpers/asset-resolver.ts"() {
393
- fs2 = __toESM(require("fs"));
394
- path2 = __toESM(require("path"));
393
+ fs3 = __toESM(require("fs"));
394
+ path4 = __toESM(require("path"));
395
395
  zlib = __toESM(require("zlib"));
396
396
  IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
397
397
  }
@@ -1072,6 +1072,156 @@ var scrollPageTool = {
1072
1072
  required: ["direction"]
1073
1073
  }
1074
1074
  };
1075
+ var validateDatabaseStateTool = {
1076
+ name: "validate_database_state",
1077
+ description: "QA Skill: Validate backend state in PostgreSQL using a safe query_id from the local Arcality database validation catalog. Use this after creating/updating records when the mission asks to corroborate persistence in the database. Supports record existence and exact field-value checks. Never accepts raw SQL.",
1078
+ input_schema: {
1079
+ type: "object",
1080
+ properties: {
1081
+ name: {
1082
+ type: "string",
1083
+ description: "Friendly validation name for the report, e.g. 'cliente_creado_en_bd'."
1084
+ },
1085
+ query_id: {
1086
+ type: "string",
1087
+ description: "Query id from .arcality/db-validations.yaml. Raw SQL is not accepted."
1088
+ },
1089
+ params: {
1090
+ type: "object",
1091
+ description: "Named params required by the catalog query, e.g. { name: 'Cliente QA 001' }."
1092
+ },
1093
+ expect: {
1094
+ type: "object",
1095
+ description: "Expected database state. Supports { exists: true/false, fields: { status: 'ACTIVE' } }.",
1096
+ properties: {
1097
+ exists: {
1098
+ type: "boolean",
1099
+ description: "Whether at least one row should exist."
1100
+ },
1101
+ fields: {
1102
+ type: "object",
1103
+ description: "Exact field-value expectations checked against the first returned row."
1104
+ }
1105
+ }
1106
+ },
1107
+ timeout_ms: {
1108
+ type: "number",
1109
+ description: "Optional query timeout in milliseconds."
1110
+ }
1111
+ },
1112
+ required: ["query_id", "params", "expect"]
1113
+ }
1114
+ };
1115
+ var previewDatabaseValidationTool = {
1116
+ name: "preview_db_validation",
1117
+ description: "QA Skill: Preview an allowlisted PostgreSQL validation query before running a full mission. Use this to verify query_id, params, returned rows, and provider connectivity without requiring an expect block. Never accepts raw SQL.",
1118
+ input_schema: {
1119
+ type: "object",
1120
+ properties: {
1121
+ name: {
1122
+ type: "string",
1123
+ description: "Friendly preview name for the output."
1124
+ },
1125
+ query_id: {
1126
+ type: "string",
1127
+ description: "Query id from .arcality/db-validations.yaml. Raw SQL is not accepted."
1128
+ },
1129
+ params: {
1130
+ type: "object",
1131
+ description: "Named params required by the catalog query."
1132
+ },
1133
+ timeout_ms: {
1134
+ type: "number",
1135
+ description: "Optional query timeout in milliseconds."
1136
+ }
1137
+ },
1138
+ required: ["query_id"]
1139
+ }
1140
+ };
1141
+ var registerDbProfileTool = {
1142
+ name: "register_db_profile",
1143
+ description: "QA Skill: Register or validate local database profile metadata without exposing secrets. Use this to confirm the provider, profile name, expected env var, catalog path, timeout defaults, and optional MCP config before running database corroboration.",
1144
+ input_schema: {
1145
+ type: "object",
1146
+ properties: {
1147
+ profile: {
1148
+ type: "string",
1149
+ description: "Logical database profile name, e.g. 'qa_local'."
1150
+ },
1151
+ provider: {
1152
+ enum: ["postgres", "mcp"],
1153
+ description: "Database provider mode to validate."
1154
+ },
1155
+ connection_env: {
1156
+ type: "string",
1157
+ description: "Optional explicit env var name that should hold the connection string."
1158
+ },
1159
+ catalog_path: {
1160
+ type: "string",
1161
+ description: "Optional path to the allowlisted query catalog file."
1162
+ },
1163
+ timeout_ms: {
1164
+ type: "number",
1165
+ description: "Optional timeout default for validations."
1166
+ },
1167
+ mcp_config_path: {
1168
+ type: "string",
1169
+ description: "Optional MCP config path when provider is mcp."
1170
+ },
1171
+ validate_catalog: {
1172
+ type: "boolean",
1173
+ description: "Whether to validate that the catalog file can be loaded.",
1174
+ default: true
1175
+ },
1176
+ persist: {
1177
+ type: "boolean",
1178
+ description: "If true, saves non-secret profile metadata into a local registry file.",
1179
+ default: false
1180
+ },
1181
+ registry_path: {
1182
+ type: "string",
1183
+ description: "Optional local registry path used only when persist=true."
1184
+ }
1185
+ }
1186
+ }
1187
+ };
1188
+ var summarizeExecutionEvidenceTool = {
1189
+ name: "summarize_execution_evidence",
1190
+ description: "QA Skill: Merge UI outcome, database corroboration, console errors, and attachments into one concise evidence summary object. Use this when closing a mission, preparing a report narrative, or resolving contradictions between visible behavior and backend state.",
1191
+ input_schema: {
1192
+ type: "object",
1193
+ properties: {
1194
+ mission_name: {
1195
+ type: "string",
1196
+ description: "Friendly mission name for the summary."
1197
+ },
1198
+ ui_status: {
1199
+ enum: ["passed", "failed", "unknown"],
1200
+ description: "Observed UI outcome status."
1201
+ },
1202
+ ui_summary: {
1203
+ type: "string",
1204
+ description: "Short human summary of what happened in the UI."
1205
+ },
1206
+ expected_business_outcome: {
1207
+ type: "string",
1208
+ description: "Expected business result in plain language."
1209
+ },
1210
+ database_status: {
1211
+ enum: ["passed", "failed", "unknown"],
1212
+ description: "Observed database corroboration status, if already known."
1213
+ },
1214
+ database_validation_name: {
1215
+ type: "string",
1216
+ description: "Optional validation name associated with the DB check."
1217
+ },
1218
+ query_id: {
1219
+ type: "string",
1220
+ description: "Optional query_id associated with the DB check."
1221
+ }
1222
+ }
1223
+ }
1224
+ };
1075
1225
  var qaAdvancedTools = [
1076
1226
  validateElementStateTool,
1077
1227
  extractTableDataTool,
@@ -1085,7 +1235,11 @@ var qaAdvancedTools = [
1085
1235
  sendKeyboardEventTool,
1086
1236
  typeSequentiallyTool,
1087
1237
  interactNativeControlTool,
1088
- scrollPageTool
1238
+ scrollPageTool,
1239
+ validateDatabaseStateTool,
1240
+ previewDatabaseValidationTool,
1241
+ registerDbProfileTool,
1242
+ summarizeExecutionEvidenceTool
1089
1243
  ];
1090
1244
 
1091
1245
  // tests/_helpers/qa-security-tools.ts
@@ -1200,8 +1354,8 @@ async function scan_sensitive_data_exposure(page) {
1200
1354
  }
1201
1355
 
1202
1356
  // tests/_helpers/ai-agent-helper.ts
1203
- var fs5 = __toESM(require("fs"));
1204
- var path5 = __toESM(require("path"));
1357
+ var fs6 = __toESM(require("fs"));
1358
+ var path7 = __toESM(require("path"));
1205
1359
 
1206
1360
  // src/KnowledgeService.ts
1207
1361
  var crypto = __toESM(require("crypto"));
@@ -2088,6 +2242,822 @@ Local context fingerprint: ${localAnalysis.fingerprint || "N/A"}
2088
2242
  `;
2089
2243
  }
2090
2244
 
2245
+ // src/services/databaseValidationService.mjs
2246
+ var import_fs = __toESM(require("fs"), 1);
2247
+ var import_path2 = __toESM(require("path"), 1);
2248
+ var import_module = require("module");
2249
+ var import_js_yaml = require("js-yaml");
2250
+
2251
+ // src/services/mcpStdioClient.mjs
2252
+ var import_child_process = require("child_process");
2253
+ var import_path = __toESM(require("path"), 1);
2254
+ var MCP_PROTOCOL_VERSION = "2024-11-05";
2255
+ function isPlainObject(value) {
2256
+ return !!value && typeof value === "object" && !Array.isArray(value);
2257
+ }
2258
+ function getNestedValue(source, dottedPath) {
2259
+ return String(dottedPath || "").split(".").filter(Boolean).reduce((current, key) => current && Object.prototype.hasOwnProperty.call(current, key) ? current[key] : void 0, source);
2260
+ }
2261
+ function replaceEnvTokens(value, envSource = process.env) {
2262
+ return String(value).replace(/\$\{([A-Z0-9_]+)\}/gi, (_, envName) => {
2263
+ const resolved = envSource[envName];
2264
+ return resolved === void 0 ? "" : String(resolved);
2265
+ });
2266
+ }
2267
+ function resolveTemplateValue(value, context = {}, envSource = process.env) {
2268
+ if (typeof value === "string") {
2269
+ const envResolved = replaceEnvTokens(value, envSource);
2270
+ const exactMatch = envResolved.match(/^\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}$/);
2271
+ if (exactMatch) {
2272
+ return getNestedValue(context, exactMatch[1]);
2273
+ }
2274
+ return envResolved.replace(/\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g, (_, tokenPath) => {
2275
+ const resolved = getNestedValue(context, tokenPath);
2276
+ if (resolved === void 0 || resolved === null)
2277
+ return "";
2278
+ if (typeof resolved === "string" || typeof resolved === "number" || typeof resolved === "boolean") {
2279
+ return String(resolved);
2280
+ }
2281
+ return JSON.stringify(resolved);
2282
+ });
2283
+ }
2284
+ if (Array.isArray(value)) {
2285
+ return value.map((entry) => resolveTemplateValue(entry, context, envSource));
2286
+ }
2287
+ if (isPlainObject(value)) {
2288
+ return Object.fromEntries(
2289
+ Object.entries(value).map(([key, entryValue]) => [key, resolveTemplateValue(entryValue, context, envSource)])
2290
+ );
2291
+ }
2292
+ return value;
2293
+ }
2294
+ function extractRowsFromMcpToolResult(result, preferredRowsPath = "") {
2295
+ const candidate = preferredRowsPath ? getNestedValue(result, preferredRowsPath) : void 0;
2296
+ if (Array.isArray(candidate))
2297
+ return candidate;
2298
+ if (Array.isArray(result?.rows))
2299
+ return result.rows;
2300
+ const content = Array.isArray(result?.content) ? result.content : [];
2301
+ for (const item of content) {
2302
+ if (!item || item.type !== "text" || typeof item.text !== "string")
2303
+ continue;
2304
+ const text = item.text.trim();
2305
+ if (!text)
2306
+ continue;
2307
+ try {
2308
+ const parsed = JSON.parse(text);
2309
+ if (Array.isArray(parsed))
2310
+ return parsed;
2311
+ if (Array.isArray(parsed?.rows))
2312
+ return parsed.rows;
2313
+ if (preferredRowsPath) {
2314
+ const nested = getNestedValue(parsed, preferredRowsPath);
2315
+ if (Array.isArray(nested))
2316
+ return nested;
2317
+ }
2318
+ } catch {
2319
+ }
2320
+ }
2321
+ return [];
2322
+ }
2323
+ function createFrame(message) {
2324
+ const body = Buffer.from(JSON.stringify(message), "utf8");
2325
+ const header = Buffer.from(`Content-Length: ${body.length}\r
2326
+ \r
2327
+ `, "utf8");
2328
+ return Buffer.concat([header, body]);
2329
+ }
2330
+ function createParser(onMessage) {
2331
+ let buffer = Buffer.alloc(0);
2332
+ return (chunk) => {
2333
+ buffer = Buffer.concat([buffer, chunk]);
2334
+ while (true) {
2335
+ const separatorIndex = buffer.indexOf("\r\n\r\n");
2336
+ if (separatorIndex === -1)
2337
+ return;
2338
+ const headerText = buffer.slice(0, separatorIndex).toString("utf8");
2339
+ const headers = headerText.split("\r\n");
2340
+ const lengthHeader = headers.find((line) => line.toLowerCase().startsWith("content-length:"));
2341
+ if (!lengthHeader) {
2342
+ throw new Error("Invalid MCP response: missing Content-Length header.");
2343
+ }
2344
+ const length = Number(lengthHeader.split(":")[1]?.trim() || 0);
2345
+ const bodyStart = separatorIndex + 4;
2346
+ const bodyEnd = bodyStart + length;
2347
+ if (buffer.length < bodyEnd)
2348
+ return;
2349
+ const body = buffer.slice(bodyStart, bodyEnd).toString("utf8");
2350
+ buffer = buffer.slice(bodyEnd);
2351
+ onMessage(JSON.parse(body));
2352
+ }
2353
+ };
2354
+ }
2355
+ async function callMcpToolWithConfig(config, toolContext = {}) {
2356
+ if (!config || !config.command) {
2357
+ throw new Error("MCP configuration is missing the command field.");
2358
+ }
2359
+ const timeoutMs = Number(config.timeoutMs || toolContext.timeoutMs || 15e3);
2360
+ const command = resolveTemplateValue(config.command, toolContext);
2361
+ const args = Array.isArray(config.args) ? resolveTemplateValue(config.args, toolContext) : [];
2362
+ const envFromConfig = isPlainObject(config.env) ? resolveTemplateValue(config.env, toolContext) : {};
2363
+ const cwd = config.cwd ? resolveTemplateValue(config.cwd, toolContext) : process.cwd();
2364
+ const toolName = String(resolveTemplateValue(config.toolName || "query", toolContext) || "").trim();
2365
+ const toolArguments = isPlainObject(config.toolArguments) ? resolveTemplateValue(config.toolArguments, toolContext) : {};
2366
+ const child = (0, import_child_process.spawn)(command, args, {
2367
+ cwd: import_path.default.isAbsolute(cwd) ? cwd : import_path.default.join(process.cwd(), cwd),
2368
+ env: { ...process.env, ...envFromConfig },
2369
+ stdio: ["pipe", "pipe", "pipe"],
2370
+ windowsHide: true
2371
+ });
2372
+ let requestId = 1;
2373
+ const pending = /* @__PURE__ */ new Map();
2374
+ let startupError = "";
2375
+ const parser = createParser((message) => {
2376
+ if (message.id && pending.has(message.id)) {
2377
+ const entry = pending.get(message.id);
2378
+ pending.delete(message.id);
2379
+ if (message.error) {
2380
+ entry.reject(new Error(message.error.message || "Unknown MCP error."));
2381
+ } else {
2382
+ entry.resolve(message.result);
2383
+ }
2384
+ }
2385
+ });
2386
+ child.stdout.on("data", (chunk) => parser(chunk));
2387
+ child.stderr.on("data", (chunk) => {
2388
+ startupError += chunk.toString("utf8");
2389
+ });
2390
+ child.on("error", (error) => {
2391
+ for (const [, entry] of pending)
2392
+ entry.reject(error);
2393
+ pending.clear();
2394
+ });
2395
+ const sendRequest = (method, params) => new Promise((resolve2, reject) => {
2396
+ const id = requestId++;
2397
+ pending.set(id, { resolve: resolve2, reject });
2398
+ child.stdin.write(createFrame({ jsonrpc: "2.0", id, method, params }));
2399
+ });
2400
+ const sendNotification = (method, params) => {
2401
+ child.stdin.write(createFrame({ jsonrpc: "2.0", method, params }));
2402
+ };
2403
+ const timeout = new Promise((_, reject) => {
2404
+ setTimeout(() => reject(new Error(`MCP tool call timed out after ${timeoutMs}ms.`)), timeoutMs);
2405
+ });
2406
+ try {
2407
+ const result = await Promise.race([
2408
+ (async () => {
2409
+ await sendRequest("initialize", {
2410
+ protocolVersion: MCP_PROTOCOL_VERSION,
2411
+ capabilities: {},
2412
+ clientInfo: { name: "arcality", version: "4.1.0" }
2413
+ });
2414
+ sendNotification("notifications/initialized", {});
2415
+ return await sendRequest("tools/call", {
2416
+ name: toolName,
2417
+ arguments: toolArguments
2418
+ });
2419
+ })(),
2420
+ timeout
2421
+ ]);
2422
+ return result;
2423
+ } catch (error) {
2424
+ const details = startupError.trim();
2425
+ if (details) {
2426
+ throw new Error(`${error.message} MCP stderr: ${details}`);
2427
+ }
2428
+ throw error;
2429
+ } finally {
2430
+ child.kill();
2431
+ }
2432
+ }
2433
+
2434
+ // src/services/databaseValidationService.mjs
2435
+ var DEFAULT_TIMEOUT_MS = 8e3;
2436
+ var DEFAULT_SAMPLE_LIMIT = 3;
2437
+ var DEFAULT_CATALOG_PATH = import_path2.default.join(".arcality", "db-validations.yaml");
2438
+ var DEFAULT_MCP_CONFIG_PATH = import_path2.default.join(".arcality", "mcp.postgres.json");
2439
+ var SENSITIVE_FIELD_PATTERN = /(password|passwd|pwd|secret|token|api[_-]?key|authorization|auth|credential|session|cookie)/i;
2440
+ var SQL_WRITE_PATTERN = /\b(insert|update|delete|drop|alter|create|truncate|grant|revoke|vacuum|copy|call|execute|merge)\b/i;
2441
+ function toEnvKey(value) {
2442
+ return String(value || "").trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
2443
+ }
2444
+ function isEnabled(value) {
2445
+ return ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
2446
+ }
2447
+ function isPlainObject2(value) {
2448
+ return !!value && typeof value === "object" && !Array.isArray(value);
2449
+ }
2450
+ function normalizeDbValue(value) {
2451
+ if (value instanceof Date)
2452
+ return value.toISOString();
2453
+ if (typeof value === "bigint")
2454
+ return value.toString();
2455
+ if (Buffer.isBuffer(value))
2456
+ return "[binary]";
2457
+ if (Array.isArray(value))
2458
+ return value.map(normalizeDbValue);
2459
+ if (isPlainObject2(value)) {
2460
+ return Object.fromEntries(
2461
+ Object.entries(value).map(([key, entryValue]) => [key, normalizeDbValue(entryValue)])
2462
+ );
2463
+ }
2464
+ return value;
2465
+ }
2466
+ function normalizeRows(rows) {
2467
+ if (!Array.isArray(rows))
2468
+ return [];
2469
+ return rows.map((row) => normalizeDbValue(row));
2470
+ }
2471
+ function normalizeRedactSet(...sources) {
2472
+ const redact = /* @__PURE__ */ new Set();
2473
+ for (const source of sources) {
2474
+ if (Array.isArray(source)) {
2475
+ for (const field of source) {
2476
+ const name = String(field || "").trim();
2477
+ if (name)
2478
+ redact.add(name);
2479
+ }
2480
+ }
2481
+ }
2482
+ return redact;
2483
+ }
2484
+ function shouldRedactField(field, redactSet) {
2485
+ return redactSet.has(field) || SENSITIVE_FIELD_PATTERN.test(field);
2486
+ }
2487
+ function redactValue(value) {
2488
+ if (value === void 0 || value === null)
2489
+ return value;
2490
+ return "[REDACTED]";
2491
+ }
2492
+ function redactObject(value, redactSet) {
2493
+ if (!isPlainObject2(value))
2494
+ return value;
2495
+ return Object.fromEntries(
2496
+ Object.entries(value).map(([key, entryValue]) => [
2497
+ key,
2498
+ shouldRedactField(key, redactSet) ? redactValue(entryValue) : entryValue
2499
+ ])
2500
+ );
2501
+ }
2502
+ function redactRows(rows, redactSet, limit = DEFAULT_SAMPLE_LIMIT) {
2503
+ return normalizeRows(rows).slice(0, limit).map((row) => redactObject(row, redactSet));
2504
+ }
2505
+ function validateReadOnlySql(sql) {
2506
+ const text = String(sql || "").trim();
2507
+ if (!text) {
2508
+ throw new Error("Database validation query is empty.");
2509
+ }
2510
+ const withoutTrailingSemicolon = text.replace(/;\s*$/, "");
2511
+ if (withoutTrailingSemicolon.includes(";")) {
2512
+ throw new Error("Database validation queries must contain a single statement.");
2513
+ }
2514
+ if (!/^(select|with)\b/i.test(withoutTrailingSemicolon)) {
2515
+ throw new Error("Database validation queries must start with SELECT or WITH.");
2516
+ }
2517
+ if (SQL_WRITE_PATTERN.test(withoutTrailingSemicolon)) {
2518
+ throw new Error("Database validation queries must be read-only.");
2519
+ }
2520
+ }
2521
+ function resolveConfigPath(configPath, fallbackPath, options = {}) {
2522
+ const projectRoot = options.projectRoot || process.env.ARCALITY_PROJECT_ROOT || process.cwd();
2523
+ return import_path2.default.isAbsolute(configPath) ? configPath : import_path2.default.join(projectRoot, configPath || fallbackPath);
2524
+ }
2525
+ function readJsonOrYamlFile(filePath) {
2526
+ const raw = import_fs.default.readFileSync(filePath, "utf8");
2527
+ if (/\.json$/i.test(filePath)) {
2528
+ return JSON.parse(raw);
2529
+ }
2530
+ return (0, import_js_yaml.load)(raw);
2531
+ }
2532
+ function resolveCatalogPath(options = {}) {
2533
+ const configuredPath = options.catalogPath || process.env.ARCALITY_DB_CATALOG || process.env.ARCALITY_DB_VALIDATIONS_FILE || DEFAULT_CATALOG_PATH;
2534
+ return resolveConfigPath(configuredPath, DEFAULT_CATALOG_PATH, options);
2535
+ }
2536
+ function resolveMcpConfigPath(options = {}) {
2537
+ const configuredPath = options.mcpConfigPath || process.env.ARCALITY_DB_MCP_CONFIG || DEFAULT_MCP_CONFIG_PATH;
2538
+ return resolveConfigPath(configuredPath, DEFAULT_MCP_CONFIG_PATH, options);
2539
+ }
2540
+ function loadDatabaseValidationCatalog(options = {}) {
2541
+ const catalogPath = resolveCatalogPath(options);
2542
+ if (!import_fs.default.existsSync(catalogPath)) {
2543
+ throw new Error(`Database validation catalog not found: ${catalogPath}`);
2544
+ }
2545
+ const raw = readJsonOrYamlFile(catalogPath);
2546
+ const catalog = raw && typeof raw === "object" ? raw : {};
2547
+ const queries = isPlainObject2(catalog.queries) ? catalog.queries : {};
2548
+ return { catalogPath, queries };
2549
+ }
2550
+ function loadDatabaseMcpConfig(options = {}) {
2551
+ const configPath = resolveMcpConfigPath(options);
2552
+ if (!import_fs.default.existsSync(configPath)) {
2553
+ throw new Error(`MCP database config not found: ${configPath}`);
2554
+ }
2555
+ const raw = readJsonOrYamlFile(configPath);
2556
+ if (!isPlainObject2(raw)) {
2557
+ throw new Error(`Invalid MCP database config: ${configPath}`);
2558
+ }
2559
+ return { configPath, config: raw };
2560
+ }
2561
+ function resolveDatabaseProvider(options = {}) {
2562
+ const rawProvider = String(options.provider || process.env.ARCALITY_DB_PROVIDER || "postgres").trim().toLowerCase();
2563
+ if (["mcp", "mcp_postgres", "mcp-postgres"].includes(rawProvider)) {
2564
+ return "mcp";
2565
+ }
2566
+ return "postgres";
2567
+ }
2568
+ function resolveDatabaseConnectionEnv(options = {}) {
2569
+ const profile = String(options.profile || process.env.ARCALITY_DB_PROFILE || "qa_local").trim();
2570
+ const profileKey = toEnvKey(profile);
2571
+ const profileEnvName = profileKey ? `ARCALITY_DB_${profileKey}_URL` : "";
2572
+ const connectionEnvName = options.connectionEnv || process.env.ARCALITY_DB_CONNECTION_ENV || profileEnvName || "ARCALITY_DB_CONNECTION";
2573
+ const connectionString = process.env[connectionEnvName] || process.env.ARCALITY_DB_CONNECTION || process.env.ARCALITY_QA_POSTGRES_URL || "";
2574
+ return {
2575
+ profile,
2576
+ connectionEnvName,
2577
+ hasConnectionString: !!connectionString,
2578
+ connectionString
2579
+ };
2580
+ }
2581
+ function normalizeTimeoutMs(value) {
2582
+ const parsed = Number(value);
2583
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
2584
+ }
2585
+ function resolveProfileRegistryPath(options = {}) {
2586
+ const configuredPath = options.registryPath || options.registry_path || import_path2.default.join(".arcality", "db-profiles.json");
2587
+ return resolveConfigPath(configuredPath, import_path2.default.join(".arcality", "db-profiles.json"), options);
2588
+ }
2589
+ function registerDatabaseProfile(profileInput = {}, options = {}) {
2590
+ const profile = String(profileInput.profile || options.profile || process.env.ARCALITY_DB_PROFILE || "qa_local").trim() || "qa_local";
2591
+ const provider = resolveDatabaseProvider({ provider: profileInput.provider || options.provider || process.env.ARCALITY_DB_PROVIDER });
2592
+ const connection = resolveDatabaseConnectionEnv({
2593
+ profile,
2594
+ connectionEnv: profileInput.connection_env || profileInput.connectionEnv || options.connectionEnv
2595
+ });
2596
+ const catalogPath = resolveCatalogPath({
2597
+ ...options,
2598
+ catalogPath: profileInput.catalog_path || profileInput.catalogPath
2599
+ });
2600
+ const timeoutMs = normalizeTimeoutMs(profileInput.timeout_ms || profileInput.timeoutMs || options.timeoutMs || process.env.ARCALITY_DB_TIMEOUT_MS);
2601
+ const warnings = [];
2602
+ const result = {
2603
+ type: "database_profile_registration",
2604
+ profile,
2605
+ provider,
2606
+ connection_env: connection.connectionEnvName,
2607
+ has_connection_string: connection.hasConnectionString,
2608
+ catalog_path: catalogPath,
2609
+ catalog_exists: import_fs.default.existsSync(catalogPath),
2610
+ timeout_ms: timeoutMs,
2611
+ warnings
2612
+ };
2613
+ if (!result.has_connection_string) {
2614
+ warnings.push("Missing PostgreSQL connection string in " + connection.connectionEnvName + ".");
2615
+ }
2616
+ if (!result.catalog_exists) {
2617
+ warnings.push("Database validation catalog not found: " + catalogPath);
2618
+ }
2619
+ if (profileInput.validate_catalog !== false && result.catalog_exists) {
2620
+ try {
2621
+ const catalog = loadDatabaseValidationCatalog({ ...options, catalogPath });
2622
+ result.query_count = Object.keys(catalog.queries || {}).length;
2623
+ } catch (error) {
2624
+ result.query_count = 0;
2625
+ warnings.push(error instanceof Error ? error.message : String(error));
2626
+ }
2627
+ }
2628
+ if (provider === "mcp") {
2629
+ const mcpConfigPath = resolveMcpConfigPath({
2630
+ ...options,
2631
+ mcpConfigPath: profileInput.mcp_config_path || profileInput.mcpConfigPath
2632
+ });
2633
+ result.mcp_config_path = mcpConfigPath;
2634
+ result.mcp_config_exists = import_fs.default.existsSync(mcpConfigPath);
2635
+ if (!result.mcp_config_exists) {
2636
+ warnings.push("MCP database config not found: " + mcpConfigPath);
2637
+ }
2638
+ }
2639
+ if (profileInput.persist === true) {
2640
+ const registryPath = resolveProfileRegistryPath({
2641
+ ...options,
2642
+ registryPath: profileInput.registry_path || profileInput.registryPath
2643
+ });
2644
+ let registry = { profiles: {} };
2645
+ if (import_fs.default.existsSync(registryPath)) {
2646
+ try {
2647
+ const existing = JSON.parse(import_fs.default.readFileSync(registryPath, "utf8"));
2648
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
2649
+ registry = existing;
2650
+ registry.profiles = registry.profiles && typeof registry.profiles === "object" ? registry.profiles : {};
2651
+ }
2652
+ } catch {
2653
+ }
2654
+ }
2655
+ registry.profiles[profile] = {
2656
+ provider,
2657
+ connection_env: connection.connectionEnvName,
2658
+ catalog_path: catalogPath,
2659
+ timeout_ms: timeoutMs,
2660
+ ...result.mcp_config_path ? { mcp_config_path: result.mcp_config_path } : {}
2661
+ };
2662
+ import_fs.default.mkdirSync(import_path2.default.dirname(registryPath), { recursive: true });
2663
+ import_fs.default.writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf8");
2664
+ result.persisted = true;
2665
+ result.registry_path = registryPath;
2666
+ } else {
2667
+ result.persisted = false;
2668
+ }
2669
+ result.status = warnings.length === 0 ? "ready" : "warning";
2670
+ return result;
2671
+ }
2672
+ function loadPgPool() {
2673
+ const roots = [process.env.ARCALITY_ROOT, process.cwd()].filter(Boolean);
2674
+ for (const root of roots) {
2675
+ try {
2676
+ const requireFromRoot = (0, import_module.createRequire)(import_path2.default.join(root, "package.json"));
2677
+ const pg = requireFromRoot("pg");
2678
+ return pg.Pool;
2679
+ } catch {
2680
+ }
2681
+ }
2682
+ throw new Error('PostgreSQL driver "pg" is not installed. Run `npm install pg --save` in this project to enable database validation.');
2683
+ }
2684
+ function getQueryEntry(catalog, queryId) {
2685
+ const entry = catalog.queries[queryId];
2686
+ if (!isPlainObject2(entry)) {
2687
+ throw new Error(`Database validation query_id not found in catalog: ${queryId}`);
2688
+ }
2689
+ validateReadOnlySql(entry.sql);
2690
+ return entry;
2691
+ }
2692
+ function buildQueryValues(entry, params) {
2693
+ const paramNames = Array.isArray(entry.params) ? entry.params : [];
2694
+ return paramNames.map((name) => {
2695
+ const key = String(name);
2696
+ if (!Object.prototype.hasOwnProperty.call(params || {}, key)) {
2697
+ throw new Error(`Missing database validation param: ${key}`);
2698
+ }
2699
+ return params[key];
2700
+ });
2701
+ }
2702
+ function evaluateDatabaseValidationRows(validation, rows, queryEntry = {}) {
2703
+ if (!isPlainObject2(validation)) {
2704
+ throw new Error("Database validation must be an object.");
2705
+ }
2706
+ const normalizedRows = normalizeRows(rows);
2707
+ const expectBlock = isPlainObject2(validation.expect) ? validation.expect : {};
2708
+ const expectsFields = isPlainObject2(expectBlock.fields);
2709
+ const expectedExists = typeof expectBlock.exists === "boolean" ? expectBlock.exists : expectsFields ? true : void 0;
2710
+ const redactSet = normalizeRedactSet(queryEntry.redact, validation.redact);
2711
+ const checks = [];
2712
+ if (typeof expectedExists === "boolean") {
2713
+ const actualExists = normalizedRows.length > 0;
2714
+ checks.push({
2715
+ type: "exists",
2716
+ expected: expectedExists,
2717
+ actual: actualExists,
2718
+ passed: actualExists === expectedExists
2719
+ });
2720
+ }
2721
+ if (expectsFields) {
2722
+ const firstRow = normalizedRows[0] || {};
2723
+ for (const [field, expected] of Object.entries(expectBlock.fields)) {
2724
+ const hasField = Object.prototype.hasOwnProperty.call(firstRow, field);
2725
+ const actual = hasField ? firstRow[field] : void 0;
2726
+ const passed2 = hasField && actual === normalizeDbValue(expected);
2727
+ const redacted = shouldRedactField(field, redactSet);
2728
+ checks.push({
2729
+ type: "field_exact",
2730
+ field,
2731
+ expected: redacted ? redactValue(expected) : normalizeDbValue(expected),
2732
+ actual: redacted ? redactValue(actual) : actual,
2733
+ passed: passed2
2734
+ });
2735
+ }
2736
+ }
2737
+ const passed = checks.length > 0 && checks.every((check) => check.passed);
2738
+ return {
2739
+ name: String(validation.name || validation.query_id || validation.queryId || "database_validation"),
2740
+ query_id: String(validation.query_id || validation.queryId || ""),
2741
+ status: passed ? "passed" : "failed",
2742
+ passed,
2743
+ row_count: normalizedRows.length,
2744
+ checks,
2745
+ rows_sample: redactRows(normalizedRows, redactSet)
2746
+ };
2747
+ }
2748
+ async function queryWithTimeout(pool, query, timeoutMs) {
2749
+ let timeoutHandle;
2750
+ const timeout = new Promise((_, reject) => {
2751
+ timeoutHandle = setTimeout(() => {
2752
+ reject(new Error(`Database validation query timed out after ${timeoutMs}ms.`));
2753
+ }, timeoutMs);
2754
+ });
2755
+ try {
2756
+ return await Promise.race([pool.query(query), timeout]);
2757
+ } finally {
2758
+ clearTimeout(timeoutHandle);
2759
+ }
2760
+ }
2761
+ async function queryDatabaseRowsViaPostgres({ validation, queryEntry, timeoutMs, startedAt, options = {} }) {
2762
+ const connection = resolveDatabaseConnectionEnv({ profile: validation.profile || options.profile });
2763
+ if (!connection.hasConnectionString) {
2764
+ throw new Error(`Missing PostgreSQL connection string in ${connection.connectionEnvName}.`);
2765
+ }
2766
+ const Pool = loadPgPool();
2767
+ const values = buildQueryValues(queryEntry, validation.params || {});
2768
+ const pool = new Pool({
2769
+ connectionString: connection.connectionString,
2770
+ max: 1,
2771
+ idleTimeoutMillis: 1e3,
2772
+ connectionTimeoutMillis: Math.min(timeoutMs, 1e4),
2773
+ statement_timeout: timeoutMs
2774
+ });
2775
+ try {
2776
+ const result = await queryWithTimeout(pool, { text: queryEntry.sql, values }, timeoutMs);
2777
+ return {
2778
+ rows: result.rows || [],
2779
+ metadata: {
2780
+ provider: "postgres",
2781
+ profile: connection.profile,
2782
+ catalog_path: options.catalogPath || resolveCatalogPath(options),
2783
+ duration_ms: Date.now() - startedAt,
2784
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
2785
+ }
2786
+ };
2787
+ } finally {
2788
+ await pool.end().catch(() => {
2789
+ });
2790
+ }
2791
+ }
2792
+ async function executeDatabaseValidationViaPostgres({ validation, queryEntry, timeoutMs, startedAt, options = {} }) {
2793
+ const { rows, metadata } = await queryDatabaseRowsViaPostgres({ validation, queryEntry, timeoutMs, startedAt, options });
2794
+ const evaluated = evaluateDatabaseValidationRows(validation, rows, queryEntry);
2795
+ return {
2796
+ ...evaluated,
2797
+ ...metadata
2798
+ };
2799
+ }
2800
+ async function queryDatabaseRowsViaMcp({ validation, queryEntry, timeoutMs, startedAt, options = {} }) {
2801
+ const { configPath, config } = loadDatabaseMcpConfig(options);
2802
+ const values = buildQueryValues(queryEntry, validation.params || {});
2803
+ const toolContext = {
2804
+ sql: queryEntry.sql,
2805
+ params: values,
2806
+ paramsNamed: validation.params || {},
2807
+ paramsJson: JSON.stringify(values),
2808
+ queryId: validation.query_id || validation.queryId || "",
2809
+ validationName: validation.name || validation.query_id || validation.queryId || "database_validation",
2810
+ timeoutMs
2811
+ };
2812
+ const result = await callMcpToolWithConfig(config, toolContext);
2813
+ const rows = extractRowsFromMcpToolResult(result, String(config.resultRowsPath || "").trim());
2814
+ return {
2815
+ rows,
2816
+ metadata: {
2817
+ provider: "mcp",
2818
+ mcp_config_path: configPath,
2819
+ mcp_tool_name: String(config.toolName || "query"),
2820
+ catalog_path: options.catalogPath || resolveCatalogPath(options),
2821
+ duration_ms: Date.now() - startedAt,
2822
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
2823
+ }
2824
+ };
2825
+ }
2826
+ async function executeDatabaseValidationViaMcp({ validation, queryEntry, timeoutMs, startedAt, options = {} }) {
2827
+ const { rows, metadata } = await queryDatabaseRowsViaMcp({ validation, queryEntry, timeoutMs, startedAt, options });
2828
+ const evaluated = evaluateDatabaseValidationRows(validation, rows, queryEntry);
2829
+ return {
2830
+ ...evaluated,
2831
+ ...metadata
2832
+ };
2833
+ }
2834
+ async function executeDatabaseValidation(validation, options = {}) {
2835
+ if (!isEnabled(process.env.ARCALITY_DB_ENABLED) && !options.allowDisabled) {
2836
+ throw new Error("Database validation is disabled. Set ARCALITY_DB_ENABLED=true to use it.");
2837
+ }
2838
+ const queryId = String(validation?.query_id || validation?.queryId || "").trim();
2839
+ if (!queryId) {
2840
+ throw new Error("Database validation requires query_id.");
2841
+ }
2842
+ const startedAt = Date.now();
2843
+ const catalog = loadDatabaseValidationCatalog(options);
2844
+ const queryEntry = getQueryEntry(catalog, queryId);
2845
+ const timeoutMs = Number(validation.timeout_ms || validation.timeoutMs || process.env.ARCALITY_DB_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
2846
+ const provider = resolveDatabaseProvider(options);
2847
+ if (provider === "mcp") {
2848
+ return executeDatabaseValidationViaMcp({ validation, queryEntry, timeoutMs, startedAt, options: { ...options, catalogPath: catalog.catalogPath } });
2849
+ }
2850
+ return executeDatabaseValidationViaPostgres({ validation, queryEntry, timeoutMs, startedAt, options: { ...options, catalogPath: catalog.catalogPath } });
2851
+ }
2852
+ function buildDatabaseValidationPreview(validation, rows, queryEntry = {}, metadata = {}) {
2853
+ const redactSet = normalizeRedactSet(queryEntry.redact, validation?.redact);
2854
+ const normalizedRows = normalizeRows(rows);
2855
+ return {
2856
+ type: "database_validation_preview",
2857
+ name: String(validation?.name || validation?.query_id || validation?.queryId || "database_validation_preview"),
2858
+ query_id: String(validation?.query_id || validation?.queryId || ""),
2859
+ params: redactObject(normalizeDbValue(validation?.params || {}), redactSet),
2860
+ row_count: normalizedRows.length,
2861
+ rows_sample: redactRows(normalizedRows, redactSet),
2862
+ query: {
2863
+ description: String(queryEntry.description || ""),
2864
+ params: Array.isArray(queryEntry.params) ? queryEntry.params.map((name) => String(name)) : []
2865
+ },
2866
+ supports_expect: {
2867
+ exists: true,
2868
+ fields: true
2869
+ },
2870
+ ...metadata
2871
+ };
2872
+ }
2873
+ async function previewDatabaseValidation(validation, options = {}) {
2874
+ if (!isEnabled(process.env.ARCALITY_DB_ENABLED) && !options.allowDisabled) {
2875
+ throw new Error("Database validation is disabled. Set ARCALITY_DB_ENABLED=true to use it.");
2876
+ }
2877
+ const queryId = String(validation?.query_id || validation?.queryId || "").trim();
2878
+ if (!queryId) {
2879
+ throw new Error("Database validation preview requires query_id.");
2880
+ }
2881
+ const startedAt = Date.now();
2882
+ const catalog = loadDatabaseValidationCatalog(options);
2883
+ const queryEntry = getQueryEntry(catalog, queryId);
2884
+ const timeoutMs = Number(validation?.timeout_ms || validation?.timeoutMs || process.env.ARCALITY_DB_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
2885
+ const provider = resolveDatabaseProvider(options);
2886
+ const normalizedValidation = {
2887
+ ...validation,
2888
+ query_id: queryId,
2889
+ name: String(validation?.name || queryId || "database_validation_preview"),
2890
+ params: isPlainObject2(validation?.params) ? validation.params : {}
2891
+ };
2892
+ const execution = provider === "mcp" ? await queryDatabaseRowsViaMcp({ validation: normalizedValidation, queryEntry, timeoutMs, startedAt, options: { ...options, catalogPath: catalog.catalogPath } }) : await queryDatabaseRowsViaPostgres({ validation: normalizedValidation, queryEntry, timeoutMs, startedAt, options: { ...options, catalogPath: catalog.catalogPath } });
2893
+ return buildDatabaseValidationPreview(normalizedValidation, execution.rows, queryEntry, execution.metadata);
2894
+ }
2895
+ async function runDatabaseValidations(validations, options = {}) {
2896
+ const list = Array.isArray(validations) ? validations : [];
2897
+ const startedAt = Date.now();
2898
+ const results = [];
2899
+ for (const validation of list) {
2900
+ try {
2901
+ results.push(await executeDatabaseValidation(validation, options));
2902
+ } catch (error) {
2903
+ results.push({
2904
+ name: String(validation?.name || validation?.query_id || validation?.queryId || "database_validation"),
2905
+ query_id: String(validation?.query_id || validation?.queryId || ""),
2906
+ status: "failed",
2907
+ passed: false,
2908
+ row_count: 0,
2909
+ checks: [],
2910
+ rows_sample: [],
2911
+ error: error instanceof Error ? error.message : String(error),
2912
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
2913
+ });
2914
+ }
2915
+ }
2916
+ const passedCount = results.filter((result) => result.passed).length;
2917
+ return {
2918
+ type: "database_validation_report",
2919
+ status: passedCount === results.length ? "passed" : "failed",
2920
+ passed: results.length > 0 && passedCount === results.length,
2921
+ summary: {
2922
+ total: results.length,
2923
+ passed: passedCount,
2924
+ failed: results.length - passedCount
2925
+ },
2926
+ duration_ms: Date.now() - startedAt,
2927
+ executed_at: (/* @__PURE__ */ new Date()).toISOString(),
2928
+ validations: results
2929
+ };
2930
+ }
2931
+ function parseDatabaseValidationsFromEnv(env = process.env) {
2932
+ const raw = env.ARCALITY_DB_VALIDATIONS_JSON || env.ARCALITY_DB_VALIDATIONS || "";
2933
+ if (!raw.trim())
2934
+ return [];
2935
+ try {
2936
+ const parsed = JSON.parse(raw);
2937
+ return Array.isArray(parsed) ? parsed : [];
2938
+ } catch {
2939
+ return [];
2940
+ }
2941
+ }
2942
+ async function runDatabaseValidationsFromEnv(options = {}) {
2943
+ const validations = parseDatabaseValidationsFromEnv();
2944
+ if (validations.length === 0)
2945
+ return null;
2946
+ return runDatabaseValidations(validations, options);
2947
+ }
2948
+
2949
+ // src/services/executionEvidenceService.mjs
2950
+ function normalizeStatus(value) {
2951
+ const normalized = String(value || "").trim().toLowerCase();
2952
+ if (normalized === "passed" || normalized === "pass" || normalized === "success")
2953
+ return "passed";
2954
+ if (normalized === "failed" || normalized === "fail" || normalized === "error")
2955
+ return "failed";
2956
+ return "unknown";
2957
+ }
2958
+ function classifyLogs(logs = []) {
2959
+ const entries = Array.isArray(logs) ? logs.map((entry) => String(entry || "").trim()).filter(Boolean) : [];
2960
+ const errors = entries.filter((entry) => entry.includes("[ERROR]") || entry.includes("[NETWORK_ERROR]"));
2961
+ const warnings = entries.filter((entry) => entry.includes("[WARNING]"));
2962
+ return {
2963
+ total: entries.length,
2964
+ error_count: errors.length,
2965
+ warning_count: warnings.length,
2966
+ recent: entries.slice(-10)
2967
+ };
2968
+ }
2969
+ function summarizeAttachments(attachments = []) {
2970
+ const normalized = Array.isArray(attachments) ? attachments.map((att) => ({
2971
+ name: String(att?.name || "").trim(),
2972
+ contentType: String(att?.contentType || "").trim(),
2973
+ path: att?.path ? String(att.path) : void 0
2974
+ })).filter((att) => att.name || att.contentType || att.path) : [];
2975
+ return {
2976
+ total: normalized.length,
2977
+ images: normalized.filter((att) => att.contentType.startsWith("image/")).length,
2978
+ videos: normalized.filter((att) => att.contentType.startsWith("video/")).length,
2979
+ json: normalized.filter((att) => att.contentType === "application/json").length,
2980
+ names: normalized.map((att) => att.name || att.path || "attachment").slice(0, 12)
2981
+ };
2982
+ }
2983
+ function deriveDatabaseStatus(input) {
2984
+ const explicit = normalizeStatus(input?.database_status);
2985
+ if (explicit !== "unknown")
2986
+ return explicit;
2987
+ if (typeof input?.database_report?.passed === "boolean") {
2988
+ return input.database_report.passed ? "passed" : "failed";
2989
+ }
2990
+ return "unknown";
2991
+ }
2992
+ function buildContradictions(uiStatus, dbStatus) {
2993
+ const contradictions = [];
2994
+ if (uiStatus === "passed" && dbStatus === "failed")
2995
+ contradictions.push("ui_passed_but_db_failed");
2996
+ if (uiStatus === "failed" && dbStatus === "passed")
2997
+ contradictions.push("ui_failed_but_db_passed");
2998
+ return contradictions;
2999
+ }
3000
+ function buildRemediationHints(finalStatus, contradictions, consoleSummary) {
3001
+ const hints = [];
3002
+ if (contradictions.includes("ui_passed_but_db_failed")) {
3003
+ hints.push("Revisar la persistencia backend o la confirmacion visual de guardado.");
3004
+ }
3005
+ if (contradictions.includes("ui_failed_but_db_passed")) {
3006
+ hints.push("Revisar si la UI muestra un error falso o si el flujo se completo parcialmente.");
3007
+ }
3008
+ if (consoleSummary.error_count > 0) {
3009
+ hints.push("Inspeccionar errores de consola o red asociados al momento de la falla.");
3010
+ }
3011
+ if (finalStatus === "unknown") {
3012
+ hints.push("Completar evidencia faltante antes de cerrar la conclusion.");
3013
+ }
3014
+ return hints;
3015
+ }
3016
+ function summarizeExecutionEvidence(input = {}, context = {}) {
3017
+ const uiStatus = normalizeStatus(input.ui_status);
3018
+ const dbStatus = deriveDatabaseStatus(input);
3019
+ const consoleSummary = classifyLogs(context.logs || input.console_logs);
3020
+ const attachmentSummary = summarizeAttachments(context.attachments || input.attachments);
3021
+ const contradictions = buildContradictions(uiStatus, dbStatus);
3022
+ let finalStatus = "unknown";
3023
+ if (contradictions.length > 0) {
3024
+ finalStatus = "contradiction";
3025
+ } else if (uiStatus === "failed" || dbStatus === "failed") {
3026
+ finalStatus = "failed";
3027
+ } else if (uiStatus === "passed" || dbStatus === "passed") {
3028
+ finalStatus = "passed";
3029
+ }
3030
+ const remediationHints = buildRemediationHints(finalStatus, contradictions, consoleSummary);
3031
+ const missionName = String(input.mission_name || input.name || "execution_evidence").trim();
3032
+ const expectedOutcome = String(input.expected_business_outcome || "").trim();
3033
+ const uiSummary = String(input.ui_summary || "").trim();
3034
+ const validationName = String(input.database_validation_name || input.database_report?.name || "").trim();
3035
+ const queryId = String(input.query_id || input.database_report?.query_id || "").trim();
3036
+ const summary = finalStatus === "contradiction" ? "La evidencia es contradictoria entre UI y base de datos." : finalStatus === "failed" ? "La ejecucion contiene evidencia suficiente para marcar falla." : finalStatus === "passed" ? "La evidencia disponible respalda un resultado exitoso." : "La ejecucion requiere mas evidencia para una conclusion firme.";
3037
+ return {
3038
+ type: "execution_evidence_summary",
3039
+ mission_name: missionName,
3040
+ final_status: finalStatus,
3041
+ summary,
3042
+ expected_business_outcome: expectedOutcome || void 0,
3043
+ current_url: context.current_url || input.current_url || void 0,
3044
+ ui: {
3045
+ status: uiStatus,
3046
+ summary: uiSummary || void 0
3047
+ },
3048
+ database: {
3049
+ status: dbStatus,
3050
+ validation_name: validationName || void 0,
3051
+ query_id: queryId || void 0
3052
+ },
3053
+ console: consoleSummary,
3054
+ attachments: attachmentSummary,
3055
+ contradictions,
3056
+ remediation_hints: remediationHints,
3057
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
3058
+ };
3059
+ }
3060
+
2091
3061
  // tests/_helpers/ai-agent-helper.ts
2092
3062
  var qaSecurityTools = [
2093
3063
  {
@@ -2190,11 +3160,11 @@ var AIAgentHelper = class {
2190
3160
  }
2191
3161
  });
2192
3162
  this.page.on("pageerror", (exception) => {
2193
- this.criticalJsError = `Excepci\xF3n JS no manejada: ${exception.message}`;
3163
+ this.criticalJsError = `Excepci\uFFFDn JS no manejada: ${exception.message}`;
2194
3164
  this.logs.push(`[JS_CRASH] ${exception.message}`);
2195
3165
  });
2196
3166
  this.page.on("crash", () => {
2197
- this.criticalJsError = `Browser Crash: La pesta\xF1a del navegador colaps\xF3 inesperadamente.`;
3167
+ this.criticalJsError = `Browser Crash: La pesta\uFFFDa del navegador colaps\uFFFD inesperadamente.`;
2198
3168
  });
2199
3169
  }
2200
3170
  clearNetworkError() {
@@ -2204,54 +3174,51 @@ var AIAgentHelper = class {
2204
3174
  try {
2205
3175
  let skillsContext = "";
2206
3176
  let loadedCount = 0;
2207
- const toolsRoot = process.env.ARCALITY_ROOT || path5.join(__dirname, "..", "..");
2208
- const possibleDirs = [
2209
- path5.join(toolsRoot, ".agent", "skills"),
2210
- path5.join(toolsRoot, ".agents", "skills")
2211
- ];
2212
- for (const rootDir of possibleDirs) {
2213
- if (!fs5.existsSync(rootDir))
2214
- continue;
2215
- if (loadedCount === 0) {
2216
- skillsContext += "\n\u{1F6E0}\uFE0F SKILLS INSTALADAS (Metodolog\xEDas activas):\n";
2217
- }
2218
- const entries = fs5.readdirSync(rootDir, { withFileTypes: true });
2219
- for (const entry of entries) {
2220
- let content = "";
2221
- let skillName = entry.name;
2222
- if (entry.isDirectory()) {
2223
- const skillPath = path5.join(rootDir, entry.name, "SKILL.md");
2224
- if (fs5.existsSync(skillPath)) {
2225
- content = fs5.readFileSync(skillPath, "utf8");
2226
- }
2227
- } else if (entry.name.endsWith(".md")) {
2228
- const skillPath = path5.join(rootDir, entry.name);
2229
- content = fs5.readFileSync(skillPath, "utf8");
3177
+ const toolsRoot = process.env.ARCALITY_ROOT || path7.join(__dirname, "..", "..");
3178
+ const rootDir = path7.join(toolsRoot, ".agents", "skills");
3179
+ if (!fs6.existsSync(rootDir)) {
3180
+ return "";
3181
+ }
3182
+ if (loadedCount === 0) {
3183
+ skillsContext += "\nSKILLS INSTALADAS (Metodologias activas):\n";
3184
+ }
3185
+ const entries = fs6.readdirSync(rootDir, { withFileTypes: true });
3186
+ for (const entry of entries) {
3187
+ let content = "";
3188
+ let skillName = entry.name;
3189
+ if (entry.isDirectory()) {
3190
+ const skillPath = path7.join(rootDir, entry.name, "SKILL.md");
3191
+ if (fs6.existsSync(skillPath)) {
3192
+ content = fs6.readFileSync(skillPath, "utf8");
2230
3193
  }
2231
- if (content) {
2232
- skillsContext += `
3194
+ } else if (entry.name === "SKILL.md") {
3195
+ const skillPath = path7.join(rootDir, entry.name);
3196
+ skillName = path7.basename(rootDir);
3197
+ content = fs6.readFileSync(skillPath, "utf8");
3198
+ }
3199
+ if (content) {
3200
+ skillsContext += `
2233
3201
  --- SKILL: ${skillName} ---
2234
3202
  ${content}
2235
3203
  `;
2236
- loadedCount++;
2237
- }
3204
+ loadedCount++;
2238
3205
  }
2239
3206
  }
2240
3207
  if (loadedCount > 0) {
2241
3208
  if (process.env.DEBUG)
2242
- console.log(`>> \u{1F4D1} ${loadedCount} habilidades QA inyectadas al cerebro.`);
3209
+ console.log(`>> [skills] ${loadedCount} habilidades QA inyectadas al cerebro.`);
2243
3210
  return skillsContext;
2244
3211
  }
2245
3212
  } catch (e) {
2246
- console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error cargando habilidades: ${e.message}`);
3213
+ console.warn(`>>ARCALITY_STATUS>> Error cargando habilidades: ${e.message}`);
2247
3214
  }
2248
3215
  return "";
2249
3216
  }
2250
3217
  loadMemory(prompt) {
2251
3218
  try {
2252
- const memoryFile = path5.join(this.contextDir, "memoria-agente.json");
2253
- if (fs5.existsSync(memoryFile)) {
2254
- const memories = JSON.parse(fs5.readFileSync(memoryFile, "utf8"));
3219
+ const memoryFile = path7.join(this.contextDir, "memoria-agente.json");
3220
+ if (fs6.existsSync(memoryFile)) {
3221
+ const memories = JSON.parse(fs6.readFileSync(memoryFile, "utf8"));
2255
3222
  const keywords = prompt.toLowerCase().split(" ").filter((w) => w.length > 4);
2256
3223
  const relevantSuccess = memories.filter((m) => {
2257
3224
  if (!m.success)
@@ -2265,7 +3232,7 @@ ${content}
2265
3232
  if (relevantSuccess.length) {
2266
3233
  const best = relevantSuccess[0];
2267
3234
  memoryContext += `
2268
- \u{1F4DA} SUCCESS GUIDANCE FOR THIS MISSION:
3235
+ ?? SUCCESS GUIDANCE FOR THIS MISSION:
2269
3236
  The following steps led to SUCCESS for a similar mission ("${best.prompt}"):
2270
3237
  ${best.steps.slice(0, 15).map((s) => ` - ${s}`).join("\n")}
2271
3238
  (Use these steps as a roadmap if the current UI allows it!)
@@ -2273,7 +3240,7 @@ ${best.steps.slice(0, 15).map((s) => ` - ${s}`).join("\n")}
2273
3240
  }
2274
3241
  if (relevantFailures.length) {
2275
3242
  memoryContext += `
2276
- \u26A0\uFE0F PREVIOUS FAILURES TO AVOID:
3243
+ ?? PREVIOUS FAILURES TO AVOID:
2277
3244
  ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2278
3245
  `;
2279
3246
  }
@@ -2325,11 +3292,11 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2325
3292
  const role = htmlEl.getAttribute("role") || "";
2326
3293
  const text = (htmlEl.innerText || htmlEl.getAttribute("aria-label") || htmlEl.getAttribute("title") || "").trim().replace(/\s+/g, " ");
2327
3294
  const textLower = text.toLowerCase();
2328
- const isCriticalFailure = (textLower.includes("error") || textLower.includes("fall\xF3") || textLower.includes("no se puede") || textLower.includes("ya existe") || textLower.includes("inv\xE1lido") || textLower.includes("incorrecto") || textLower.includes("ligado") || textLower.includes("depende") || textLower.includes("duplicado") || textLower.includes("repetido") || /* Validaciones ZOD / Wizard de campos de formulario */
3295
+ const isCriticalFailure = (textLower.includes("error") || textLower.includes("fall\uFFFD") || textLower.includes("no se puede") || textLower.includes("ya existe") || textLower.includes("inv\uFFFDlido") || textLower.includes("incorrecto") || textLower.includes("ligado") || textLower.includes("depende") || textLower.includes("duplicado") || textLower.includes("repetido") || /* Validaciones ZOD / Wizard de campos de formulario */
2329
3296
  textLower.includes("debe ser mayor") || textLower.includes("debe ser menor") || textLower.includes("debe ser igual") || textLower.includes("debe ser mayor o igual") || textLower.includes("debe ser menor o igual") || /* ZOD date messages (Spanish) */
2330
- textLower.includes("debe ser posterior") || textLower.includes("debe ser anterior") || textLower.includes("debe ser una fecha") || textLower.includes("fecha debe ser") || textLower.includes("posterior o igual a la fecha") || textLower.includes("anterior o igual a la fecha") || textLower.includes("posterior a la fecha") || textLower.includes("anterior a la fecha") || textLower.includes("fecha") && (textLower.includes("actual") || textLower.includes("pasada") || textLower.includes("futura") || textLower.includes("requerida") || textLower.includes("inv\xE1lida") || textLower.includes("incorrecta") || textLower.includes("posterior") || textLower.includes("anterior")) || /* ZOD number/range messages */
2331
- textLower.includes("debe ser un n\xFAmero") || textLower.includes("debe ser positivo") || textLower.includes("debe ser negativo") || textLower.includes("m\xEDnimo") && textLower.includes("caracteres") || textLower.includes("m\xE1ximo") && textLower.includes("caracteres") || /* Generic ZOD/form required must be specific, NOT broad instructional text */
2332
- textLower.includes("campo requerido") || textLower.includes("campo obligatorio") || textLower === "requerido" || textLower === "obligatorio" || textLower.includes("este campo es requerido") || textLower.includes("este campo es obligatorio") || textLower.includes("no puede estar vac\xEDo") || textLower.includes("no puede ser") || textLower.includes("no es v\xE1lido") || textLower.includes("no es correcto") || textLower.includes("formato incorrecto") || textLower.includes("formato inv\xE1lido") || textLower.includes("valor no permitido")) && text.length < 200 && !textLower.includes("recuerda") && !textLower.includes("antes de guardar");
3297
+ textLower.includes("debe ser posterior") || textLower.includes("debe ser anterior") || textLower.includes("debe ser una fecha") || textLower.includes("fecha debe ser") || textLower.includes("posterior o igual a la fecha") || textLower.includes("anterior o igual a la fecha") || textLower.includes("posterior a la fecha") || textLower.includes("anterior a la fecha") || textLower.includes("fecha") && (textLower.includes("actual") || textLower.includes("pasada") || textLower.includes("futura") || textLower.includes("requerida") || textLower.includes("inv\uFFFDlida") || textLower.includes("incorrecta") || textLower.includes("posterior") || textLower.includes("anterior")) || /* ZOD number/range messages */
3298
+ textLower.includes("debe ser un n\uFFFDmero") || textLower.includes("debe ser positivo") || textLower.includes("debe ser negativo") || textLower.includes("m\uFFFDnimo") && textLower.includes("caracteres") || textLower.includes("m\uFFFDximo") && textLower.includes("caracteres") || /* Generic ZOD/form required must be specific, NOT broad instructional text */
3299
+ textLower.includes("campo requerido") || textLower.includes("campo obligatorio") || textLower === "requerido" || textLower === "obligatorio" || textLower.includes("este campo es requerido") || textLower.includes("este campo es obligatorio") || textLower.includes("no puede estar vac\uFFFDo") || textLower.includes("no puede ser") || textLower.includes("no es v\uFFFDlido") || textLower.includes("no es correcto") || textLower.includes("formato incorrecto") || textLower.includes("formato inv\uFFFDlido") || textLower.includes("valor no permitido")) && text.length < 200 && !textLower.includes("recuerda") && !textLower.includes("antes de guardar");
2333
3300
  const isCriticalSuccess = (textLower.includes("exitosamente") || textLower.includes("guardado") || textLower.includes("creado") || textLower.includes("success") || textLower.includes("correctamente") && text.length < 100) && text.length < 200;
2334
3301
  const isCriticalFeedback = isCriticalFailure || isCriticalSuccess;
2335
3302
  const isFormSelection = tag === "input" && (htmlEl.getAttribute("type") === "radio" || htmlEl.getAttribute("type") === "checkbox") || (role === "radio" || role === "checkbox" || role === "switch");
@@ -2444,8 +3411,8 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2444
3411
  let type = "INFO";
2445
3412
  const isErrorClass = classLower.includes("error") || classLower.includes("danger") || classLower.includes("fail");
2446
3413
  const isError = isCriticalFailure || isErrorClass && text.split(" ").length > 2;
2447
- const isSuccess = classLower.includes("success") || isCriticalSuccess || textLower.includes("\xE9xito");
2448
- const isTab = role === "tab" || classLower.includes("tab") || classLower.includes("pesta\xF1a");
3414
+ const isSuccess = classLower.includes("success") || isCriticalSuccess || textLower.includes("\uFFFDxito");
3415
+ const isTab = role === "tab" || classLower.includes("tab") || classLower.includes("pesta\uFFFDa");
2449
3416
  if (tag === "input" || tag === "textarea" || tag === "select" || isFormSelection)
2450
3417
  type = "FIELD";
2451
3418
  else if (tag === "button" || role === "button" || tag === "a" || role === "link" || isTab)
@@ -2458,7 +3425,7 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2458
3425
  type = isSuccess ? "SUCCESS_MESSAGE" : isError ? "ERROR_MESSAGE" : "MESSAGE";
2459
3426
  }
2460
3427
  if (type.includes("MESSAGE")) {
2461
- const prefix = isError ? "\u{1F6D1} [ERROR_CR\xCDTICO] " : isSuccess ? "\u2728 [\xC9XITO_DETECTADO] " : "[INFO] ";
3428
+ const prefix = isError ? "?? [ERROR_CR\uFFFDTICO] " : isSuccess ? "? [\uFFFDXITO_DETECTADO] " : "[INFO] ";
2462
3429
  name = prefix + (name || text || "Mensaje del sistema");
2463
3430
  }
2464
3431
  if (type === "ACTION" || type === "FIELD") {
@@ -2510,23 +3477,23 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2510
3477
  } catch {
2511
3478
  }
2512
3479
  }
2513
- console.log(`>>ARCALITY_STATUS>> \u{1F50D} Percepci\xF3n profunda encontr\xF3 ${allComponents.length} componentes`);
3480
+ console.log(`>>ARCALITY_STATUS>> ?? Percepci\uFFFDn profunda encontr\uFFFD ${allComponents.length} componentes`);
2514
3481
  const urlForBlankCheck = this.page.url();
2515
3482
  const isAuthPage = urlForBlankCheck.includes("/login") || urlForBlankCheck.includes("/auth") || urlForBlankCheck.includes("oauth") || urlForBlankCheck.includes("/#/");
2516
3483
  if (allComponents.length < 10 && !isAuthPage) {
2517
3484
  const bodyTextLength = await this.page.evaluate(() => document.body.innerText.trim().length).catch(() => 999);
2518
3485
  if (bodyTextLength < 80) {
2519
3486
  console.warn(`
2520
- \u26A0\uFE0F [BLANK_STATE] Primera medici\xF3n: ${allComponents.length} componentes, ${bodyTextLength} chars. Esperando re-render...`);
3487
+ ?? [BLANK_STATE] Primera medici\uFFFDn: ${allComponents.length} componentes, ${bodyTextLength} chars. Esperando re-render...`);
2521
3488
  await this.page.waitForTimeout(2500);
2522
3489
  const bodyTextLength2 = await this.page.evaluate(() => document.body.innerText.trim().length).catch(() => 999);
2523
3490
  const components2 = await this.page.evaluate(() => document.querySelectorAll("a, button, input, select, textarea").length).catch(() => 999);
2524
3491
  if (bodyTextLength2 < 80 && components2 < 10) {
2525
3492
  console.error(`
2526
- \u{1F6D1} [BLANK_STATE_CRASH] Vista en blanco confirmada tras espera (Comps: ${components2}, Texto: ${bodyTextLength2} chars). URL: ${urlForBlankCheck}`);
2527
- throw new Error(`[BLANK_STATE_CRASH] El portal carg\xF3 en blanco. Esto es un fallo de renderizado del portal, no de Arcality.`);
3493
+ ?? [BLANK_STATE_CRASH] Vista en blanco confirmada tras espera (Comps: ${components2}, Texto: ${bodyTextLength2} chars). URL: ${urlForBlankCheck}`);
3494
+ throw new Error(`[BLANK_STATE_CRASH] El portal carg\uFFFD en blanco. Esto es un fallo de renderizado del portal, no de Arcality.`);
2528
3495
  } else {
2529
- console.log(`>>ARCALITY_STATUS>> \u2705 [BLANK_STATE] Falsa alarma. P\xE1gina recuper\xF3 contenido tras espera (${components2} comps, ${bodyTextLength2} chars).`);
3496
+ console.log(`>>ARCALITY_STATUS>> ? [BLANK_STATE] Falsa alarma. P\uFFFDgina recuper\uFFFD contenido tras espera (${components2} comps, ${bodyTextLength2} chars).`);
2530
3497
  }
2531
3498
  }
2532
3499
  }
@@ -2608,7 +3575,7 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2608
3575
  }
2609
3576
  }
2610
3577
  } catch (e) {
2611
- console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en hook de ingesta: ${e.message}`);
3578
+ console.log(`>>ARCALITY_STATUS>> ?? Error en hook de ingesta: ${e.message}`);
2612
3579
  }
2613
3580
  return {
2614
3581
  url,
@@ -2692,18 +3659,18 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2692
3659
  }
2693
3660
  }
2694
3661
  /**
2695
- * Busca una acción sugerida en la memoria SIN usar tokens.
3662
+ * Busca una acci�n sugerida en la memoria SIN usar tokens.
2696
3663
  * Compara URL, componentes visuales Y palabras clave del prompt.
2697
3664
  */
2698
3665
  async getActionFromGuia(prompt, historyLength) {
2699
3666
  try {
2700
- const fs7 = require("fs");
2701
- const path7 = require("path");
2702
- const memoryFile = path7.join(this.contextDir, "memoria-agente.json");
2703
- if (!fs7.existsSync(memoryFile)) {
3667
+ const fs8 = require("fs");
3668
+ const path9 = require("path");
3669
+ const memoryFile = path9.join(this.contextDir, "memoria-agente.json");
3670
+ if (!fs8.existsSync(memoryFile)) {
2704
3671
  return null;
2705
3672
  }
2706
- const memories = JSON.parse(fs7.readFileSync(memoryFile, "utf8"));
3673
+ const memories = JSON.parse(fs8.readFileSync(memoryFile, "utf8"));
2707
3674
  const state = await this.getPageState();
2708
3675
  const currentUrl = this.page.url();
2709
3676
  const extractCriticalKeywords = (text) => {
@@ -2717,8 +3684,8 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2717
3684
  keywords.add("tercera");
2718
3685
  if (lower.includes("cuarta"))
2719
3686
  keywords.add("cuarta");
2720
- if (lower.includes("\xFAltima"))
2721
- keywords.add("\xFAltima");
3687
+ if (lower.includes("\uFFFDltima"))
3688
+ keywords.add("\uFFFDltima");
2722
3689
  if (lower.includes("first"))
2723
3690
  keywords.add("primera");
2724
3691
  if (lower.includes("second"))
@@ -2728,11 +3695,11 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2728
3695
  if (lower.includes("fourth"))
2729
3696
  keywords.add("cuarta");
2730
3697
  if (lower.includes("last"))
2731
- keywords.add("\xFAltima");
3698
+ keywords.add("\uFFFDltima");
2732
3699
  return keywords;
2733
3700
  };
2734
3701
  const currentKeywords = extractCriticalKeywords(prompt);
2735
- const currentVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\xF1ade", "add", "create", "new", "edita", "modifica", "cambia", "edit", "update"].filter((v) => prompt.toLowerCase().includes(v));
3702
+ const currentVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\uFFFDade", "add", "create", "new", "edita", "modifica", "cambia", "edit", "update"].filter((v) => prompt.toLowerCase().includes(v));
2736
3703
  const matchingRun = memories.filter((m) => m.success && m.steps_data && m.steps_data[historyLength]).reverse().find((m) => {
2737
3704
  const memPrompt = (m.prompt || "").toLowerCase();
2738
3705
  const currentPrompt = prompt.toLowerCase();
@@ -2744,7 +3711,7 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2744
3711
  const matchRatio = matches / words.length;
2745
3712
  if (matchRatio < 0.85)
2746
3713
  return false;
2747
- const memVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\xF1ade", "add", "create", "new", "edita", "modifica", "cambia", "edit", "update"].filter((v) => memPrompt.includes(v));
3714
+ const memVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\uFFFDade", "add", "create", "new", "edita", "modifica", "cambia", "edit", "update"].filter((v) => memPrompt.includes(v));
2748
3715
  const hasSharedVerb = currentVerbs.some((cv) => memVerbs.includes(cv));
2749
3716
  const hasOppositeIntent = currentVerbs.some((v) => ["elimina", "borra", "delete"].includes(v)) && memVerbs.some((v) => ["crea", "nueva", "create", "new"].includes(v)) || currentVerbs.some((v) => ["crea", "nueva", "create", "new"].includes(v)) && memVerbs.some((v) => ["elimina", "borra", "delete"].includes(v));
2750
3717
  if (hasOppositeIntent)
@@ -2805,21 +3772,21 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2805
3772
  }
2806
3773
  const foundComponent = bestMatchScore >= 40 ? bestMatch : null;
2807
3774
  if (!foundComponent && memStep.action_data?.action !== "wait") {
2808
- console.log(`>>ARCALITY_STATUS>> \u{1F50D} Descartado (Componente visual no encontrado: ${memStep.componentName})`);
3775
+ console.log(`>>ARCALITY_STATUS>> ?? Descartado (Componente visual no encontrado: ${memStep.componentName})`);
2809
3776
  return null;
2810
3777
  }
2811
3778
  if (foundComponent && memStep.action_data?.action === "fill") {
2812
3779
  const compType = (foundComponent.type || "").toLowerCase();
2813
3780
  const compName = (foundComponent.name || "").toLowerCase();
2814
- const looksLikeDropdown = compType.includes("select") || compType.includes("combobox") || compName.includes("tipo") || compName.includes("type") || compName.includes("categor\xEDa") || compName.includes("estado") || compName.includes("frecuencia") || compName.includes("alcance");
3781
+ const looksLikeDropdown = compType.includes("select") || compType.includes("combobox") || compName.includes("tipo") || compName.includes("type") || compName.includes("categor\uFFFDa") || compName.includes("estado") || compName.includes("frecuencia") || compName.includes("alcance");
2815
3782
  if (looksLikeDropdown) {
2816
- console.log(`>>ARCALITY_STATUS>> \u{1F50D} Descartado (fill en dropdown detectado: ${foundComponent.name}). Delegando a IA.`);
3783
+ console.log(`>>ARCALITY_STATUS>> ?? Descartado (fill en dropdown detectado: ${foundComponent.name}). Delegando a IA.`);
2817
3784
  return null;
2818
3785
  }
2819
3786
  }
2820
3787
  const memCompName = memStep.componentName || "";
2821
3788
  if (memCompName.length > 80) {
2822
- console.log(`>>ARCALITY_STATUS>> \u{1F50D} Descartado (Mensaje enorme o nombre inconsistente: ${memCompName.substring(0, 20)}...)`);
3789
+ console.log(`>>ARCALITY_STATUS>> ?? Descartado (Mensaje enorme o nombre inconsistente: ${memCompName.substring(0, 20)}...)`);
2823
3790
  return null;
2824
3791
  }
2825
3792
  if (!memCompName || memCompName.length < 3) {
@@ -2829,18 +3796,18 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2829
3796
  let actionData = { ...memStep.action_data };
2830
3797
  if (actionData.value === "{{secret}}") {
2831
3798
  const cName = (foundComponent.name || "").toLowerCase();
2832
- if (cName.includes("password") || cName.includes("contrase\xF1a")) {
3799
+ if (cName.includes("password") || cName.includes("contrase\uFFFDa")) {
2833
3800
  actionData.value = "Qa_Test123!";
2834
3801
  } else if (cName.includes("email") || cName.includes("correo")) {
2835
3802
  actionData.value = `qa_${Date.now()}@test.com`;
2836
3803
  } else if (cName.includes("descrip")) {
2837
- actionData.value = `Descripci\xF3n QA Automatizada ${Date.now()}`;
3804
+ actionData.value = `Descripci\uFFFDn QA Automatizada ${Date.now()}`;
2838
3805
  } else {
2839
3806
  actionData.value = `QA_Test_${Date.now()}`;
2840
3807
  }
2841
3808
  }
2842
3809
  return {
2843
- thought: `[GU\xCDA DE \xC9XITO] Reutilizando paso maestro aprendido: ${actionData.action} en ${foundComponent.name}`,
3810
+ thought: `[GU\uFFFDA DE \uFFFDXITO] Reutilizando paso maestro aprendido: ${actionData.action} en ${foundComponent.name}`,
2844
3811
  actions: [{
2845
3812
  ...actionData,
2846
3813
  idx: foundComponent.idx,
@@ -2962,12 +3929,18 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2962
3929
  };
2963
3930
  }
2964
3931
  const credentialsContext = process.env.LOGIN_USER && process.env.LOGIN_PASSWORD ? `
2965
- \u{1F511} QA CREDENTIALS (Use if login is needed):
3932
+ ?? QA CREDENTIALS (Use if login is needed):
2966
3933
  - User: ${process.env.LOGIN_USER}
2967
3934
  - Password: ${process.env.LOGIN_PASSWORD}
2968
3935
  ` : "";
2969
3936
  const memoryContext = this.loadMemory(prompt);
2970
3937
  const skillsContext = this.loadSkills();
3938
+ const databaseValidationEnabled = ["1", "true", "yes", "on"].includes(String(process.env.ARCALITY_DB_ENABLED || "").toLowerCase());
3939
+ const databaseProfile = process.env.ARCALITY_DB_PROFILE || "qa_local";
3940
+ const databaseContext = databaseValidationEnabled ? `
3941
+ # DATABASE VALIDATION CONTEXT
3942
+ PostgreSQL validation is enabled for profile "${databaseProfile}". If the mission asks to corroborate backend persistence or database state, use \`validate_database_state\` after the relevant UI action succeeds. If you need to verify query_id, params, connectivity, or returned rows before asserting, use \`preview_db_validation\`. Use only query_id values from the local catalog; never invent raw SQL.
3943
+ ` : "";
2971
3944
  let assetContext = "";
2972
3945
  try {
2973
3946
  const { formatUploadAssetContext: formatUploadAssetContext2 } = (init_asset_resolver(), __toCommonJS(asset_resolver_exports));
@@ -2980,7 +3953,7 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2980
3953
  const runtimeProjectRoot = process.env.ARCALITY_PROJECT_ROOT || process.cwd();
2981
3954
  const ctx = getProjectContext(runtimeProjectRoot);
2982
3955
  projectInfo = `
2983
- \u{1F3D7}\uFE0F PROJECT CONTEXT:
3956
+ ??? PROJECT CONTEXT:
2984
3957
  - Name: ${ctx.name}
2985
3958
  - Tech Stack: ${ctx.framework}
2986
3959
  - Environment: ${process.env.NODE_ENV || "development"}
@@ -2993,11 +3966,11 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2993
3966
  backendContextData = await this.knowledgeService.getContext(state.url);
2994
3967
  if (backendContextData && (backendContextData.rules?.length > 0 || backendContextData.fields?.length > 0)) {
2995
3968
  if (process.env.DEBUG)
2996
- console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Contexto de Memoria Colectiva inyectado (${backendContextData.rules?.length || 0} reglas, ${backendContextData.fields?.length || 0} campos).`);
3969
+ console.log(`>>ARCALITY_STATUS>> ?? Contexto de Memoria Colectiva inyectado (${backendContextData.rules?.length || 0} reglas, ${backendContextData.fields?.length || 0} campos).`);
2997
3970
  }
2998
3971
  } catch (e) {
2999
3972
  if (process.env.DEBUG)
3000
- console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en hook de contexto: ${e.message}`);
3973
+ console.log(`>>ARCALITY_STATUS>> ?? Error en hook de contexto: ${e.message}`);
3001
3974
  }
3002
3975
  let customUserContext = "";
3003
3976
  try {
@@ -3020,7 +3993,7 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
3020
3993
  }
3021
3994
  }
3022
3995
  if (qaContextAnalysis.exists || backendContextData?.rules?.length || backendContextData?.fields?.length) {
3023
- console.log(`>>ARCALITY_STATUS>> Pol\xEDtica efectiva de contexto: local qa-context > memoria backend > heur\xEDsticas base.`);
3996
+ console.log(`>>ARCALITY_STATUS>> Pol\uFFFDtica efectiva de contexto: local qa-context > memoria backend > heur\uFFFDsticas base.`);
3024
3997
  }
3025
3998
  this.qaContextStatusLogged = true;
3026
3999
  }
@@ -3035,22 +4008,22 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
3035
4008
  {
3036
4009
  type: "text",
3037
4010
  text: `# IDENTITY
3038
- You are **Arcality**, an elite Senior QA Web Testing Agent. You have 10+ years of experience in manual and automated testing. You are meticulous, thorough, and NEVER take shortcuts. You treat every mission as a real client engagement where your professional reputation is on the line. You MUST use your full arsenal of QA tools \u2014 guessing and blind retries are UNACCEPTABLE.
4011
+ You are **Arcality**, an elite Senior QA Web Testing Agent. You have 10+ years of experience in manual and automated testing. You are meticulous, thorough, and NEVER take shortcuts. You treat every mission as a real client engagement where your professional reputation is on the line. You MUST use your full arsenal of QA tools \uFFFD guessing and blind retries are UNACCEPTABLE.
3039
4012
 
3040
4013
  `
3041
4014
  },
3042
4015
  {
3043
4016
  type: "text",
3044
4017
  text: `
3045
- # METHODOLOGY (MANDATORY \u2014 Follow in order)
4018
+ # METHODOLOGY (MANDATORY \uFFFD Follow in order)
3046
4019
  Every turn, you MUST follow these 4 phases:
3047
4020
 
3048
4021
  ## Phase 1: OBSERVE
3049
4022
  - Read ALL components in CURRENT COMPONENTS carefully.
3050
- - Check the URL \u2014 are you on the right page?
3051
- - Read HISTORY \u2014 what have you already done? Don't repeat actions.
3052
- - Look for MESSAGE components \u2014 errors ([ERROR_CR\xCDTICO]) or success messages.
3053
- - If you see [INFERRED] in any component name \u2192 that name is UNRELIABLE. You MUST call \`inspect_element_details\` on it BEFORE interacting.
4023
+ - Check the URL \uFFFD are you on the right page?
4024
+ - Read HISTORY \uFFFD what have you already done? Don't repeat actions.
4025
+ - Look for MESSAGE components \uFFFD errors ([ERROR_CR\uFFFDTICO]) or success messages.
4026
+ - If you see [INFERRED] in any component name ? that name is UNRELIABLE. You MUST call \`inspect_element_details\` on it BEFORE interacting.
3054
4027
 
3055
4028
  ## Phase 2: PLAN
3056
4029
  - Based on your observations, decide: What is the ONE next meaningful step toward completing the mission?
@@ -3065,10 +4038,10 @@ Every turn, you MUST follow these 4 phases:
3065
4038
  - **STEP 1 (MANDATORY)**: Click the dropdown/combobox trigger to OPEN it. Wait for the list to appear.
3066
4039
  - **STEP 2 (MANDATORY, SAME or NEXT turn)**: Click the desired option directly (\`[li]\` or \`[option]\`). Do NOT use \`fill\` to type text.
3067
4040
  - **STEP 3**: If more fields remain in the same form section, do NOT group them in the same turn. Wait for re-render first.
3068
- - **NEVER** insert a \`wait\` action between clicking the trigger and clicking the option \u2014 complete both in one turn.
4041
+ - **NEVER** insert a \`wait\` action between clicking the trigger and clicking the option \uFFFD complete both in one turn.
3069
4042
  - **STRICTLY FORBIDDEN**: Sending Escape repeatedly. After selecting an option, click the NEXT logical button directly.
3070
- - For menus: Click menu icon \u2192 WAIT for next turn \u2192 Click option.
3071
- - For \`<input type="date">\` native fields: NEVER use \`fill\`. ALWAYS use \`interact_native_control\` with value in YYYY-MM-DD format. Do this ONCE per field. If it fails, do NOT retry \u2014 move on and click Siguiente to see the validation message.
4043
+ - For menus: Click menu icon ? WAIT for next turn ? Click option.
4044
+ - For \`<input type="date">\` native fields: NEVER use \`fill\`. ALWAYS use \`interact_native_control\` with value in YYYY-MM-DD format. Do this ONCE per field. If it fails, do NOT retry \uFFFD move on and click Siguiente to see the validation message.
3072
4045
 
3073
4046
  ## Phase 4: VERIFY
3074
4047
  - After critical actions (SAVE/CREATE/DELETE), verify the result:
@@ -3087,26 +4060,28 @@ These are NOT optional. You MUST use these tools in these situations:
3087
4060
  | Element name has [INFERRED] | \`inspect_element_details\` | BEFORE interacting with it |
3088
4061
  | Before clicking SAVE/GUARDAR | \`validate_element_state\` | To confirm button is enabled |
3089
4062
  | After successful SAVE | \`extract_table_data\` | To verify record was created |
4063
+ | Mission asks to validate PostgreSQL/backend data | \`validate_database_state\` | AFTER the relevant UI action succeeds, using a query_id from the local catalog |
4064
+ | Need to dry-run a database query_id or inspect returned rows before asserting | \`preview_db_validation\` | BEFORE the mission or before adding expect rules |
3090
4065
  | UI is frozen/unresponsive | \`capture_console_errors\` | To check for JS errors |
3091
4066
  | Mission mentions a concrete route like '/counter' and current URL is different, or the route has no visible menu item | \`navigate_to_url\` | IMMEDIATELY instead of improvising clicks |
3092
4067
  | Bug found in application | \`create_test_evidence\` | To document with annotated screenshot |
3093
4068
  | Mission complete | \`create_test_evidence\` | To document the final successful state |
3094
4069
  | Modal/popup blocking page | \`send_keyboard_event\` (Escape) | To close the overlay |
3095
- | Masked numeric/code input, OTP/PIN/CLABE/INE identifier, or prompt says "manualmente", "d\xEDgito por d\xEDgito", "car\xE1cter por car\xE1cter" | \`type_sequentially\` | After ONE focus attempt on the field; do not keep clicking |
3096
- | Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill \u2014 native inputs need special handling |
4070
+ | Masked numeric/code input, OTP/PIN/CLABE/INE identifier, or prompt says "manualmente", "d\uFFFDgito por d\uFFFDgito", "car\uFFFDcter por car\uFFFDcter" | \`type_sequentially\` | After ONE focus attempt on the field; do not keep clicking |
4071
+ | Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill \uFFFD native inputs need special handling |
3097
4072
  | Element not found on screen | \`scroll_page\` | To reveal off-screen content |
3098
- | Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | IMMEDIATELY after selecting any option \u2014 MANDATORY |
3099
- | Dropdown selected but still visually open | \`send_keyboard_event\` (Tab) | If Escape did not close it \u2014 force field to lose focus |
4073
+ | Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | IMMEDIATELY after selecting any option \uFFFD MANDATORY |
4074
+ | Dropdown selected but still visually open | \`send_keyboard_event\` (Tab) | If Escape did not close it \uFFFD force field to lose focus |
3100
4075
  | Navigate between form sections | \`send_keyboard_event\` (Tab) | To move focus between fields |
3101
4076
 
3102
4077
  # CRITICAL RULES
3103
4078
  1. **NEVER BLIND-RETRY**: If an action fails, you MUST investigate with \`inspect_element_details\` before retrying. Repeating the exact same failed action is FORBIDDEN.
3104
4079
  2. **NO REPEAT ACTIONS**: Check HISTORY. If you already filled a field or clicked a button, do NOT do it again unless the UI clearly reset OR the button belongs to a different step/modal/section mentioned in the mission (e.g., Save in modal vs Save in main form).
3105
4080
  3. **GROUP ACTIONS CAREFULLY**: You MUST group non-conflicting actions in a single turn to save time and tokens.
3106
- - You can \`fill\` multiple plain text inputs in the SAME turn (Nombre, Descripci\xF3n, etc.).
4081
+ - You can \`fill\` multiple plain text inputs in the SAME turn (Nombre, Descripci\uFFFDn, etc.).
3107
4082
  - You CANNOT mix dropdown selection with filling other fields in the same turn (React re-renders invalidate idx).
3108
- - For \`<input type="date">\` fields: use \`interact_native_control\` ONCE per field \u2014 do NOT retry with \`fill\` or \`click\`.
3109
- - NEVER insert a \`wait\` action between related dropdown steps (click trigger \u2192 click option). It breaks the sequence.
4083
+ - For \`<input type="date">\` fields: use \`interact_native_control\` ONCE per field \uFFFD do NOT retry with \`fill\` or \`click\`.
4084
+ - NEVER insert a \`wait\` action between related dropdown steps (click trigger ? click option). It breaks the sequence.
3110
4085
 
3111
4086
  3. **CURRENT STATE > MEMORY**: What you see in CURRENT COMPONENTS is the truth. SUCCESS MEMORY is a guide, not gospel.
3112
4087
  4. **PAGE TRANSITIONS**: If URL contains '/edit' or you see a 'Save' button, you ARE in the edit view. Don't go back.
@@ -3117,8 +4092,8 @@ These are NOT optional. You MUST use these tools in these situations:
3117
4092
  - If the dropdown selection was the LAST step in the form, you CAN immediately click Siguiente/Guardar in the same turn.
3118
4093
  - **ESCAPE RULE**: You may send Escape ONCE after selecting to help close the dropdown. If you already sent Escape and the form still shows the same state, DO NOT send Escape again. Click \`Siguiente\` or \`Guardar\` directly to proceed.
3119
4094
  - **VALIDATION ERRORS**: If Siguiente reports a validation error on a dropdown field, THEN use \`inspect_element_details\` to understand the field's selector and interact with it precisely. Not before.
3120
- 6. **ERROR HANDLING**: If you see '\u{1F6D1} [ERROR_CR\xCDTICO]', STOP everything. Analyze the error, fix the data, then retry.
3121
- 6b. **DUPLICATE / REPEATED VALUE ERROR \u2014 MANDATORY PROTOCOL**:
4095
+ 6. **ERROR HANDLING**: If you see '?? [ERROR_CR\uFFFDTICO]', STOP everything. Analyze the error, fix the data, then retry.
4096
+ 6b. **DUPLICATE / REPEATED VALUE ERROR \uFFFD MANDATORY PROTOCOL**:
3122
4097
  - If HISTORY contains a line with "ERROR VISIBLE EN PANTALLA" or "ERROR POST-GUARDADO" with words like "repetido", "duplicado", "ya existe", or "already exists":
3123
4098
  1. You MUST identify WHICH field caused the duplication (usually the main name/title field).
3124
4099
  2. You MUST use \`fill\` to REPLACE that field's value with a NEW unique value. Add a numeric suffix like \`_${(/* @__PURE__ */ new Date()).getMinutes()}${(/* @__PURE__ */ new Date()).getSeconds()}\` or a short random number.
@@ -3136,9 +4111,9 @@ These are NOT optional. You MUST use these tools in these situations:
3136
4111
  11. **NEGATIVE TESTING**: If mission expects an error (e.g., "verifica que no se pueda guardar sin campos"), and you see that error, the mission is SUCCESS. Use \`finish: true\`.
3137
4112
  12. **GUIDE CONFLICT**: If SUCCESS MEMORY disagrees with what you see on screen, ABANDON the memory and trust your eyes.
3138
4113
  13. **NATIVE CONTROLS**: For \`<input type="time">\`, \`<input type="date">\`, or similar native pickers, NEVER use \`fill\` repeatedly. Use \`interact_native_control\` or \`send_keyboard_event\` instead. If \`fill\` fails once, switch strategy immediately.
3139
- 14. **USER-DEFINED BUGS (MANDATORY & IMMEDIATE)**: If the prompt explicitly defines a condition as a bug (e.g., "Si no te deja eliminarla, es un bug de la aplicaci\xF3n", or "If X fails it's a bug"), and you encounter that condition, you MUST call \`report_application_bug\` IMMEDIATELY in that exact same turn. DO NOT perform any intermediate UI actions (such as clicking "Cancelar", closing modals, or navigating away) before calling the tool. Any intermediate UI action will cause you to lose your reasoning context in the next turn and enter an infinite loop. The call to \`report_application_bug\` must take absolute priority over any UI cleanup or close actions. DO NOT dismiss it as "expected business logic" or "referential integrity". The user's definition of a bug OVERRIDES all general reasoning.
4114
+ 14. **USER-DEFINED BUGS (MANDATORY & IMMEDIATE)**: If the prompt explicitly defines a condition as a bug (e.g., "Si no te deja eliminarla, es un bug de la aplicaci\uFFFDn", or "If X fails it's a bug"), and you encounter that condition, you MUST call \`report_application_bug\` IMMEDIATELY in that exact same turn. DO NOT perform any intermediate UI actions (such as clicking "Cancelar", closing modals, or navigating away) before calling the tool. Any intermediate UI action will cause you to lose your reasoning context in the next turn and enter an infinite loop. The call to \`report_application_bug\` must take absolute priority over any UI cleanup or close actions. DO NOT dismiss it as "expected business logic" or "referential integrity". The user's definition of a bug OVERRIDES all general reasoning.
3140
4115
 
3141
- 15. **MASKED / SEQUENTIAL INPUTS (MANDATORY PROTOCOL)**: If the field is a masked identifier/code input (examples: placeholders like \`000-000-000-0000\`, OTP/PIN boxes, CLABE/account identifiers, ID/INE codes) OR the mission explicitly says "manualmente", "n\xFAmero por n\xFAmero", "d\xEDgito por d\xEDgito", or "car\xE1cter por car\xE1cter":
4116
+ 15. **MASKED / SEQUENTIAL INPUTS (MANDATORY PROTOCOL)**: If the field is a masked identifier/code input (examples: placeholders like \`000-000-000-0000\`, OTP/PIN boxes, CLABE/account identifiers, ID/INE codes) OR the mission explicitly says "manualmente", "n\uFFFDmero por n\uFFFDmero", "d\uFFFDgito por d\uFFFDgito", or "car\uFFFDcter por car\uFFFDcter":
3142
4117
  - You get EXACTLY ONE focus attempt: click the field once, or click a nearby keyboard button/icon ONCE if the UI shows one (e.g. "Teclado").
3143
4118
  - Then you MUST use \`type_sequentially\` with the full string. Do NOT simulate manual typing by repeatedly clicking the field.
3144
4119
  - If the field still does not accept input after that, use \`inspect_element_details\` on the field or the adjacent keyboard trigger. Never click the same input 3+ times in a row.
@@ -3146,53 +4121,53 @@ These are NOT optional. You MUST use these tools in these situations:
3146
4121
 
3147
4122
  16. **FILE UPLOAD FIELDS (MANDATORY PROTOCOL)**: When the current step requires uploading an image or file (a dropzone component with text like "arrastra", "explora", "formatos", "JPEG", "PNG", or an input with aria-label containing "imagen" or "file"):
3148
4123
  - You MUST use the \`fill\` action on the file input with one of the values from the \`# AVAILABLE UPLOAD ASSETS\` section below (format: \`arcasset://<asset_id>\`).
3149
- - If no asset matches the required type, use the value \`"placeholder:image/banner"\` \u2014 the runner will automatically generate a valid placeholder PNG image.
4124
+ - If no asset matches the required type, use the value \`"placeholder:image/banner"\` \uFFFD the runner will automatically generate a valid placeholder PNG image.
3150
4125
  - **NEVER** report a bug or inability to proceed just because you see a file upload field. You ALWAYS have a fallback.
3151
4126
  - **NEVER** skip the image step. If clicking "Siguiente" is blocked by a required image field, locate the file input and use \`fill\` with \`arcasset://\` or \`placeholder:image/banner\`.
3152
4127
  - To locate the file input: look for a component with type \`[input]\` adjacent to dropzone text, OR use \`inspect_element_details\` on the dropzone container to find its nested \`input[type=file]\`.
3153
4128
 
3154
4129
  # WIZARD & MULTI-STEP FORMS (CRITICAL)
3155
- When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 Step 3):
4130
+ When a form is divided into numbered steps (e.g., Step 1 ? Step 2 ? Step 3):
3156
4131
 
3157
- 1. **VALIDATION ERROR ON ADVANCE**: If you click "Next/Siguiente" and the UI DOES NOT advance to the next step, AND you see a \u{1F6D1} [ERROR_CR\xCDTICO], you must fix the data on the CURRENT step. DO NOT GO BACK to previous steps simply because you see an error, unless the error explicitly states that a previous step's data is invalid.
4132
+ 1. **VALIDATION ERROR ON ADVANCE**: If you click "Next/Siguiente" and the UI DOES NOT advance to the next step, AND you see a ?? [ERROR_CR\uFFFDTICO], you must fix the data on the CURRENT step. DO NOT GO BACK to previous steps simply because you see an error, unless the error explicitly states that a previous step's data is invalid.
3158
4133
 
3159
- 2. **DATE VALIDATION ERRORS \u2014 MANDATORY PROTOCOL (ZOD-AWARE)**:
4134
+ 2. **DATE VALIDATION ERRORS \uFFFD MANDATORY PROTOCOL (ZOD-AWARE)**:
3160
4135
  - ZOD generates Spanish validation messages such as:
3161
4136
  - "La fecha de inicio debe ser posterior o igual a la fecha actual."
3162
4137
  - "La fecha debe ser una fecha futura."
3163
4138
  - "Debe ser posterior a la fecha de inicio."
3164
4139
  - "El valor debe ser mayor o igual a X."
3165
- - ANY \u{1F6D1} [ERROR_CR\xCDTICO] containing FECHA + (POSTERIOR/ANTERIOR/ACTUAL/PASADA/FUTURA) is a **ZOD DATE VALIDATION ERROR**.
4140
+ - ANY ?? [ERROR_CR\uFFFDTICO] containing FECHA + (POSTERIOR/ANTERIOR/ACTUAL/PASADA/FUTURA) is a **ZOD DATE VALIDATION ERROR**.
3166
4141
  - **RECOVERY PROTOCOL** (follow in order, NEVER skip):
3167
- 1. STOP \u2014 do NOT click Save/Siguiente again with the same data.
4142
+ 1. STOP \uFFFD do NOT click Save/Siguiente again with the same data.
3168
4143
  2. Identify which wizard step contains the failing date field (check HISTORY).
3169
4144
  3. If you are NOT on that step, click the Back/Anterior button to return to it.
3170
4145
  4. Locate the date input using \`validate_element_state\` with 'has_value' to confirm its current (bad) value.
3171
- 5. Clear and re-fill the date using \`interact_native_control\` with control_type \`'date'\` and a value \u2265 TODAY in YYYY-MM-DD format.
3172
- 6. TODAY: \`${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}\` \u2014 use this exact expression to compute the date dynamically.
4146
+ 5. Clear and re-fill the date using \`interact_native_control\` with control_type \`'date'\` and a value = TODAY in YYYY-MM-DD format.
4147
+ 6. TODAY: \`${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}\` \uFFFD use this exact expression to compute the date dynamically.
3173
4148
  7. After fixing ALL date fields, advance through each wizard step again.
3174
4149
 
3175
4150
  3. **NEVER LOOP ON THE SAME STEP**: If you've been on the same wizard step for 3+ turns without advancing, go back one step and verify the data is correct before trying to advance again.
3176
4151
 
3177
4152
  4. **STEP VERIFICATION**: Before clicking Next/Save on any step, VERIFY that date fields have future-or-today dates. Use \`validate_element_state\` with 'has_value' to confirm.
3178
4153
 
3179
- 5. **FINAL PREVIEW STEP**: If you reach a final step like "Previsualizaci\xF3n", "Resumen", or see a "Terminar" / "Finalizar" button, your objective is ALMOST COMPLETE. Do NOT go back to previous steps just because you see read-only data. Click the final submit button ("Terminar", "Guardar", "Finalizar") to complete the mission.
4154
+ 5. **FINAL PREVIEW STEP**: If you reach a final step like "Previsualizaci\uFFFDn", "Resumen", or see a "Terminar" / "Finalizar" button, your objective is ALMOST COMPLETE. Do NOT go back to previous steps just because you see read-only data. Click the final submit button ("Terminar", "Guardar", "Finalizar") to complete the mission.
3180
4155
 
3181
- 6. **DATE FIELD \u2014 ONE SHOT RULE**: For any \`<input type="date">\`, you get EXACTLY ONE attempt with \`interact_native_control\`. If it does not stick (field shows empty on next perception), do NOT retry the date. Instead: a) use \`send_keyboard_event\` to type the date digit by digit (e.g., "0", "5", "0", "5", "2", "0", "2", "6"), OR b) click \`Siguiente\` and see if ZOD validates it. If ZOD rejects it, THEN and ONLY THEN come back to fix. Looping 3+ times on the same date field is FORBIDDEN.
4156
+ 6. **DATE FIELD \uFFFD ONE SHOT RULE**: For any \`<input type="date">\`, you get EXACTLY ONE attempt with \`interact_native_control\`. If it does not stick (field shows empty on next perception), do NOT retry the date. Instead: a) use \`send_keyboard_event\` to type the date digit by digit (e.g., "0", "5", "0", "5", "2", "0", "2", "6"), OR b) click \`Siguiente\` and see if ZOD validates it. If ZOD rejects it, THEN and ONLY THEN come back to fix. Looping 3+ times on the same date field is FORBIDDEN.
3182
4157
 
3183
- 7. **MODAL REWARD COMBOBOX**: In the rewards modal ("Configurar Recompensas del Juego"), the "Tipo de recompensa" dropdown is a \`[div]\` trigger, NOT an \`[input]\`. Always use 2 turns: Turn A = Click the trigger div to open the option list. Turn B = Click the specific option (e.g., \`[li] Puntos\`, \`[li] Cup\xF3n\`). Do NOT use \`fill\` on this field.
4158
+ 7. **MODAL REWARD COMBOBOX**: In the rewards modal ("Configurar Recompensas del Juego"), the "Tipo de recompensa" dropdown is a \`[div]\` trigger, NOT an \`[input]\`. Always use 2 turns: Turn A = Click the trigger div to open the option list. Turn B = Click the specific option (e.g., \`[li] Puntos\`, \`[li] Cup\uFFFDn\`). Do NOT use \`fill\` on this field.
3184
4159
 
3185
- 8. **MODAL SELECTION PROTOCOL (CRITICAL)**: In selection modals (e.g., "Selecciona cup\xF3n"), you MUST select an item BEFORE clicking "Seleccionar" or "Confirmar". To select an item, click on ANY visible element within the item's row: the radio button, the item's name, OR its status label (e.g., the text "Activo" next to a coupon name). Clicking the confirm button without a prior selection WILL FAIL. If you do NOT see a radio button IDX in the component list, click the item's STATUS text (like "Activo") or its NAME text instead \u2014 this is the EXPECTED behavior for custom UI components.
4160
+ 8. **MODAL SELECTION PROTOCOL (CRITICAL)**: In selection modals (e.g., "Selecciona cup\uFFFDn"), you MUST select an item BEFORE clicking "Seleccionar" or "Confirmar". To select an item, click on ANY visible element within the item's row: the radio button, the item's name, OR its status label (e.g., the text "Activo" next to a coupon name). Clicking the confirm button without a prior selection WILL FAIL. If you do NOT see a radio button IDX in the component list, click the item's STATUS text (like "Activo") or its NAME text instead \uFFFD this is the EXPECTED behavior for custom UI components.
3186
4161
 
3187
- 9. **RADIO BUTTON FALLBACK**: If you cannot find a dedicated radio button element for a list item, do NOT loop. Instead: a) Click the item's visible text (name, status, or any text in its row). b) If that doesn't work, try clicking the row container element. c) NEVER use the search box to "select" an item \u2014 the search box only filters the list, it does NOT select.
4162
+ 9. **RADIO BUTTON FALLBACK**: If you cannot find a dedicated radio button element for a list item, do NOT loop. Instead: a) Click the item's visible text (name, status, or any text in its row). b) If that doesn't work, try clicking the row container element. c) NEVER use the search box to "select" an item \uFFFD the search box only filters the list, it does NOT select.
3188
4163
 
3189
- 10. **DROPDOWN TOGGLE WARNING (CRITICAL)**: Clicking a dropdown/combobox trigger TOGGLES it: 1st click = open, 2nd click = CLOSE, 3rd click = open again. If you clicked a dropdown trigger and do NOT see options in the next turn, do NOT click it again \u2014 you will just close it! Instead, use \`send_keyboard_event\` with "ArrowDown" to open/navigate the dropdown, then "Enter" to select.
4164
+ 10. **DROPDOWN TOGGLE WARNING (CRITICAL)**: Clicking a dropdown/combobox trigger TOGGLES it: 1st click = open, 2nd click = CLOSE, 3rd click = open again. If you clicked a dropdown trigger and do NOT see options in the next turn, do NOT click it again \uFFFD you will just close it! Instead, use \`send_keyboard_event\` with "ArrowDown" to open/navigate the dropdown, then "Enter" to select.
3190
4165
 
3191
4166
  11. **COMBOBOX KEYBOARD FALLBACK**: If you click a combobox/select field and the dropdown options (\`[li]\`, \`[OPTION]\`) are NOT visible after 1 turn, use this keyboard sequence: a) Click the combobox field ONCE. b) Use \`send_keyboard_event\` with key "ArrowDown" to open the options and highlight the first one. c) Use \`send_keyboard_event\` with key "Enter" to select it. This avoids the toggle problem and works with all dropdown implementations.
3192
4167
 
3193
4168
  12. **SEQUENTIAL CODE INPUT FALLBACK (CRITICAL)**: If you are on a code/identifier step and the UI shows a field like "Identificador", an adjacent "Teclado" button, or a mask such as \`000-000-000-0000\`, NEVER spend multiple turns trying to focus the field. One click is enough. After that, use \`type_sequentially\` with the complete code string. If the keyboard button exists and the first direct focus did not work, click the keyboard button ONCE and then use \`type_sequentially\`.
3194
4169
 
3195
- 13. **ERROR STOP PROTOCOL (CRITICAL)**: If you see a message starting with \`\u{1F6D1} [ERROR_CR\xCDTICO]\` or \`\u{1F6D1} ERROR POST-GUARDADO\`, do NOT assume it is a transient error. You MUST analyze the text. If it says "ya existe" or "duplicado", you MUST change the offending field value (e.g., add a random number) AND click the save button in the SAME turn. Do NOT waste a turn closing the error toast. If you retry without changing any data and the error persists, you MUST use \`report_inability_to_proceed\` immediately. Looping on the same error toast is FORBIDDEN.`,
4170
+ 13. **ERROR STOP PROTOCOL (CRITICAL)**: If you see a message starting with \`?? [ERROR_CR\uFFFDTICO]\` or \`?? ERROR POST-GUARDADO\`, do NOT assume it is a transient error. You MUST analyze the text. If it says "ya existe" or "duplicado", you MUST change the offending field value (e.g., add a random number) AND click the save button in the SAME turn. Do NOT waste a turn closing the error toast. If you retry without changing any data and the error persists, you MUST use \`report_inability_to_proceed\` immediately. Looping on the same error toast is FORBIDDEN.`,
3196
4171
  cache_control: { type: "ephemeral" }
3197
4172
  },
3198
4173
  {
@@ -3202,6 +4177,7 @@ When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 St
3202
4177
  ${prompt}
3203
4178
 
3204
4179
  ${projectInfo}
4180
+ ${databaseContext}
3205
4181
  ${skillsContext}
3206
4182
  ${customUserContext}
3207
4183
  ${memoryContext}
@@ -3255,7 +4231,7 @@ ${history.slice(-25).join("\n") || "None"}`
3255
4231
  ]
3256
4232
  }
3257
4233
  ];
3258
- console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Arcality est\xE1 procesando la visi\xF3n...`);
4234
+ console.log(`>>ARCALITY_STATUS>> ?? Arcality est\uFFFD procesando la visi\uFFFDn...`);
3259
4235
  const startTime = Date.now();
3260
4236
  let totalUsage = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
3261
4237
  for (let turn = 0; turn < 10; turn++) {
@@ -3266,7 +4242,7 @@ ${history.slice(-25).join("\n") || "None"}`
3266
4242
  const isProxyMode = !!process.env.ARCALITY_API_URL;
3267
4243
  const endpointUrl = isProxyMode ? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy` : "https://api.anthropic.com/v1/messages";
3268
4244
  const displayTurn = stepCount !== void 0 ? stepCount : turn + 1;
3269
- console.log(`>>ARCALITY_STATUS>> \u{1F4E1} Llamando a: ${endpointUrl} (turno IA ${displayTurn}${turn > 0 ? `, ciclo interno ${turn + 1}` : ""})`);
4245
+ console.log(`>>ARCALITY_STATUS>> ?? Llamando a: ${endpointUrl} (turno IA ${displayTurn}${turn > 0 ? `, ciclo interno ${turn + 1}` : ""})`);
3270
4246
  const headers = {
3271
4247
  "Content-Type": "application/json"
3272
4248
  };
@@ -3306,13 +4282,13 @@ ${history.slice(-25).join("\n") || "None"}`
3306
4282
  if (response.ok)
3307
4283
  break;
3308
4284
  const errorText = await response.text();
3309
- console.log(`>>ARCALITY_STATUS>> \u274C API Error ${response.status}: ${errorText.substring(0, 500)}`);
4285
+ console.log(`>>ARCALITY_STATUS>> ? API Error ${response.status}: ${errorText.substring(0, 500)}`);
3310
4286
  const isTransient = response.status === 529 || response.status === 429 || response.status === 503;
3311
4287
  if (isTransient && retryCount < maxRetries) {
3312
4288
  retryCount++;
3313
4289
  const delay = Math.pow(2, retryCount) * 1e3;
3314
4290
  console.log(`
3315
- >>ARCALITY_STATUS>> \u26A0\uFE0F Arcality est\xE1 saturado (${response.status}). Reintentando...`);
4291
+ >>ARCALITY_STATUS>> ?? Arcality est\uFFFD saturado (${response.status}). Reintentando...`);
3316
4292
  await new Promise((resolve2) => setTimeout(resolve2, delay));
3317
4293
  continue;
3318
4294
  }
@@ -3334,7 +4310,7 @@ ${history.slice(-25).join("\n") || "None"}`
3334
4310
  const textResponse = message.content.find((c) => c.type === "text")?.text || "";
3335
4311
  if (toolCalls.length === 0) {
3336
4312
  console.log(`(${((Date.now() - startTime) / 1e3).toFixed(1)}s)`);
3337
- console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Arcality devolvi\xF3 texto sin acci\xF3n. Reinyectando instrucci\xF3n mandatoria...`);
4313
+ console.log(`>>ARCALITY_STATUS>> ?? Arcality devolvi\uFFFD texto sin acci\uFFFDn. Reinyectando instrucci\uFFFDn mandatoria...`);
3338
4314
  anthropicMessages.push({
3339
4315
  role: "user",
3340
4316
  content: [{ type: "text", text: `MANDATORY: You MUST call one of the available tools right now. You are NOT allowed to return plain text only. If you are unsure, call 'perform_ui_actions' with the most logical next action based on the mission and history. If you are truly stuck, call 'report_inability_to_proceed'. DO NOT return text without a tool call.` }]
@@ -3354,7 +4330,7 @@ ${history.slice(-25).join("\n") || "None"}`
3354
4330
  } else if (toolName === "inspect_element_details") {
3355
4331
  const idx = toolInput.idx;
3356
4332
  const c = state.components.find((comp) => comp.idx === idx);
3357
- console.log(`>>ARCALITY_STATUS>> \u{1F50D} Investigando IDX:${idx}...`);
4333
+ console.log(`>>ARCALITY_STATUS>> ?? Investigando IDX:${idx}...`);
3358
4334
  let details = "Element not found or no longer present.";
3359
4335
  if (c) {
3360
4336
  try {
@@ -3379,11 +4355,11 @@ ${history.slice(-25).join("\n") || "None"}`
3379
4355
  }
3380
4356
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: details });
3381
4357
  } else if (toolName === "report_application_bug") {
3382
- console.error(`\u{1F41B} [BUG DETECTED] ${toolInput.bug_description}`);
4358
+ console.error(`?? [BUG DETECTED] ${toolInput.bug_description}`);
3383
4359
  try {
3384
4360
  const url = this.page.url();
3385
4361
  await this.knowledgeService.saveRule(
3386
- "Validaci\xF3n Detectada (Auto-Learning)",
4362
+ "Validaci\uFFFDn Detectada (Auto-Learning)",
3387
4363
  `Error: ${toolInput.bug_description}. Se esperaba: ${toolInput.expected_behavior}`,
3388
4364
  "WARNING",
3389
4365
  url
@@ -3395,9 +4371,9 @@ ${history.slice(-25).join("\n") || "None"}`
3395
4371
  const shortDesc = toolInput.bug_description.length > 80 ? toolInput.bug_description.substring(0, 80).trimEnd() + "..." : toolInput.bug_description;
3396
4372
  const bugTitle = `[QA Bug] ${shortDesc}`;
3397
4373
  const bugDescription = [
3398
- `**Bug detectado autom\xE1ticamente por Arcality QA**`,
4374
+ `**Bug detectado autom\uFFFDticamente por Arcality QA**`,
3399
4375
  ``,
3400
- `**Descripci\xF3n:** ${toolInput.bug_description}`,
4376
+ `**Descripci\uFFFDn:** ${toolInput.bug_description}`,
3401
4377
  `**Comportamiento esperado:** ${toolInput.expected_behavior}`,
3402
4378
  `**Comportamiento actual:** ${toolInput.actual_behavior}`,
3403
4379
  `**URL afectada:** ${this.page.url()}`,
@@ -3415,9 +4391,9 @@ ${history.slice(-25).join("\n") || "None"}`
3415
4391
  const shortDesc = toolInput.bug_description.length > 80 ? toolInput.bug_description.substring(0, 80).trimEnd() + "..." : toolInput.bug_description;
3416
4392
  const bugTitle = `[QA Bug] ${shortDesc}`;
3417
4393
  const bugDescription = [
3418
- `**Bug detectado autom\xE1ticamente por Arcality QA**`,
4394
+ `**Bug detectado autom\uFFFDticamente por Arcality QA**`,
3419
4395
  ``,
3420
- `**Descripci\xF3n:** ${toolInput.bug_description}`,
4396
+ `**Descripci\uFFFDn:** ${toolInput.bug_description}`,
3421
4397
  `**Comportamiento esperado:** ${toolInput.expected_behavior}`,
3422
4398
  `**Comportamiento actual:** ${toolInput.actual_behavior}`,
3423
4399
  `**URL afectada:** ${this.page.url()}`,
@@ -3430,7 +4406,7 @@ ${history.slice(-25).join("\n") || "None"}`
3430
4406
  return {
3431
4407
  thought: `DISCULPA: ${toolInput.confusion_reason}
3432
4408
 
3433
- \u{1F4A1} SUGERENCIA: ${toolInput.prompt_suggestion}`,
4409
+ ?? SUGERENCIA: ${toolInput.prompt_suggestion}`,
3434
4410
  actions: [],
3435
4411
  finish: true,
3436
4412
  usage: totalUsage
@@ -3496,7 +4472,7 @@ ${history.slice(-25).join("\n") || "None"}`
3496
4472
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result });
3497
4473
  } else if (toolName === "extract_table_data") {
3498
4474
  const { table_identifier, max_rows = 50 } = toolInput;
3499
- console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Extrayendo tabla: ${table_identifier}...`);
4475
+ console.log(`>>ARCALITY_STATUS>> ?? Extrayendo tabla: ${table_identifier}...`);
3500
4476
  try {
3501
4477
  const tableData = await this.page.evaluate(({ identifier, limit }) => {
3502
4478
  const findTable = () => {
@@ -3526,6 +4502,107 @@ ${history.slice(-25).join("\n") || "None"}`
3526
4502
  } catch (e) {
3527
4503
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error extracting: ${e.message}` });
3528
4504
  }
4505
+ } else if (toolName === "validate_database_state") {
4506
+ console.log(`>>ARCALITY_STATUS>> [DB] Validando estado en PostgreSQL: ${toolInput.query_id}...`);
4507
+ try {
4508
+ const report = await executeDatabaseValidation(toolInput);
4509
+ if (this.testInfo) {
4510
+ await this.testInfo.attach("database_validation_report", {
4511
+ body: Buffer.from(JSON.stringify(report, null, 2), "utf-8"),
4512
+ contentType: "application/json"
4513
+ });
4514
+ }
4515
+ const summary = report.passed ? `Database validation passed: ${report.name}` : `Database validation failed: ${report.name}`;
4516
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `${summary}
4517
+ ${JSON.stringify(report, null, 2)}` });
4518
+ } catch (e) {
4519
+ const report = {
4520
+ type: "database_validation_report",
4521
+ status: "failed",
4522
+ passed: false,
4523
+ summary: { total: 1, passed: 0, failed: 1 },
4524
+ validations: [{
4525
+ name: toolInput.name || toolInput.query_id || "database_validation",
4526
+ query_id: toolInput.query_id || "",
4527
+ status: "failed",
4528
+ passed: false,
4529
+ error: e.message
4530
+ }],
4531
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
4532
+ };
4533
+ if (this.testInfo) {
4534
+ await this.testInfo.attach("database_validation_report", {
4535
+ body: Buffer.from(JSON.stringify(report, null, 2), "utf-8"),
4536
+ contentType: "application/json"
4537
+ });
4538
+ }
4539
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Database validation error: ${e.message}` });
4540
+ }
4541
+ } else if (toolName === "preview_db_validation") {
4542
+ console.log(`>>ARCALITY_STATUS>> [DB] Preview de validacion PostgreSQL: ${toolInput.query_id}...`);
4543
+ try {
4544
+ const preview = await previewDatabaseValidation(toolInput);
4545
+ if (this.testInfo) {
4546
+ await this.testInfo.attach("database_validation_preview", {
4547
+ body: Buffer.from(JSON.stringify(preview, null, 2), "utf-8"),
4548
+ contentType: "application/json"
4549
+ });
4550
+ }
4551
+ toolResults.push({
4552
+ type: "tool_result",
4553
+ tool_use_id: toolUse.id,
4554
+ content: `Database validation preview ready: ${preview.name}
4555
+ ${JSON.stringify(preview, null, 2)}`
4556
+ });
4557
+ } catch (e) {
4558
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Database validation preview error: ${e.message}` });
4559
+ }
4560
+ } else if (toolName === "register_db_profile") {
4561
+ console.log(`>>ARCALITY_STATUS>> [DB] Registrando o validando perfil local: ${toolInput.profile || process.env.ARCALITY_DB_PROFILE || "qa_local"}...`);
4562
+ try {
4563
+ const profileReport = registerDatabaseProfile(toolInput);
4564
+ if (this.testInfo) {
4565
+ await this.testInfo.attach("database_profile_registration", {
4566
+ body: Buffer.from(JSON.stringify(profileReport, null, 2), "utf-8"),
4567
+ contentType: "application/json"
4568
+ });
4569
+ }
4570
+ toolResults.push({
4571
+ type: "tool_result",
4572
+ tool_use_id: toolUse.id,
4573
+ content: `Database profile ready: ${profileReport.profile}
4574
+ ${JSON.stringify(profileReport, null, 2)}`
4575
+ });
4576
+ } catch (e) {
4577
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Database profile error: ${e.message}` });
4578
+ }
4579
+ } else if (toolName === "summarize_execution_evidence") {
4580
+ try {
4581
+ const attachments = Array.isArray(this.testInfo?.attachments) ? this.testInfo.attachments.map((att) => ({
4582
+ name: att.name,
4583
+ contentType: att.contentType,
4584
+ path: att.path
4585
+ })) : [];
4586
+ const evidenceSummary = summarizeExecutionEvidence(toolInput, {
4587
+ logs: this.logs,
4588
+ attachments,
4589
+ current_url: this.page.url()
4590
+ });
4591
+ if (this.testInfo) {
4592
+ await this.testInfo.attach("execution_evidence_summary", {
4593
+ body: Buffer.from(JSON.stringify(evidenceSummary, null, 2), "utf-8"),
4594
+ contentType: "application/json"
4595
+ });
4596
+ }
4597
+ toolResults.push({
4598
+ type: "tool_result",
4599
+ tool_use_id: toolUse.id,
4600
+ content: `Execution evidence summary ready: ${evidenceSummary.final_status}
4601
+ ${JSON.stringify(evidenceSummary, null, 2)}`
4602
+ });
4603
+ } catch (e) {
4604
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Execution evidence summary error: ${e.message}` });
4605
+ }
3529
4606
  } else if (toolName === "capture_console_errors") {
3530
4607
  const logs = this.logs.length > 0 ? this.logs.join("\n") : "No console errors/warnings/network errors captured.";
3531
4608
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: logs });
@@ -3551,7 +4628,7 @@ ${history.slice(-25).join("\n") || "None"}`
3551
4628
  }
3552
4629
  } else if (toolName === "measure_page_performance") {
3553
4630
  const { thresholds = {} } = toolInput;
3554
- console.log(`>>ARCALITY_STATUS>> \u26A1 Midiendo rendimiento de p\xE1gina...`);
4631
+ console.log(`>>ARCALITY_STATUS>> ? Midiendo rendimiento de p\uFFFDgina...`);
3555
4632
  try {
3556
4633
  const perfData = await this.page.evaluate(() => {
3557
4634
  const nav = performance.getEntriesByType("navigation")[0];
@@ -3567,7 +4644,7 @@ ${history.slice(-25).join("\n") || "None"}`
3567
4644
  };
3568
4645
  });
3569
4646
  const results = [
3570
- `\u{1F4CA} PERFORMANCE METRICS:`,
4647
+ `?? PERFORMANCE METRICS:`,
3571
4648
  ` Load Time: ${perfData.load_time_ms ?? "N/A"}ms`,
3572
4649
  ` DOM Content Loaded: ${perfData.dom_content_loaded_ms ?? "N/A"}ms`,
3573
4650
  ` First Contentful Paint: ${perfData.first_contentful_paint_ms ?? "N/A"}ms`,
@@ -3580,7 +4657,7 @@ ${history.slice(-25).join("\n") || "None"}`
3580
4657
  }
3581
4658
  } else if (toolName === "save_navigation_checkpoint") {
3582
4659
  const { checkpoint_name, include_storage = true } = toolInput;
3583
- console.log(`>>ARCALITY_STATUS>> \u{1F4BE} Guardando punto de control: "${checkpoint_name}"...`);
4660
+ console.log(`>>ARCALITY_STATUS>> ?? Guardando punto de control: "${checkpoint_name}"...`);
3584
4661
  try {
3585
4662
  const url = this.page.url();
3586
4663
  const context = this.page.context();
@@ -3607,17 +4684,17 @@ ${history.slice(-25).join("\n") || "None"}`
3607
4684
  sessionStorage = storageData.sessionStorage;
3608
4685
  }
3609
4686
  this.checkpoints.set(checkpoint_name, { url, storageState, localStorage, sessionStorage });
3610
- const result = `\u2705 Checkpoint "${checkpoint_name}" saved successfully.`;
4687
+ const result = `? Checkpoint "${checkpoint_name}" saved successfully.`;
3611
4688
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result });
3612
4689
  } catch (e) {
3613
4690
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error saving checkpoint: ${e.message}` });
3614
4691
  }
3615
4692
  } else if (toolName === "restore_navigation_checkpoint") {
3616
4693
  const { checkpoint_name } = toolInput;
3617
- console.log(`>>ARCALITY_STATUS>> \u{1F504} Restaurando punto de control: "${checkpoint_name}"...`);
4694
+ console.log(`>>ARCALITY_STATUS>> ?? Restaurando punto de control: "${checkpoint_name}"...`);
3618
4695
  const checkpoint = this.checkpoints.get(checkpoint_name);
3619
4696
  if (!checkpoint) {
3620
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u274C Checkpoint "${checkpoint_name}" not found.` });
4697
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Checkpoint "${checkpoint_name}" not found.` });
3621
4698
  } else {
3622
4699
  try {
3623
4700
  const context = this.page.context();
@@ -3635,7 +4712,7 @@ ${history.slice(-25).join("\n") || "None"}`
3635
4712
  window.sessionStorage.setItem(k, v);
3636
4713
  }, { ls: checkpoint.localStorage, ss: checkpoint.sessionStorage });
3637
4714
  await this.page.reload({ waitUntil: "domcontentloaded", timeout: 15e3 });
3638
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Checkpoint "${checkpoint_name}" restored.` });
4715
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Checkpoint "${checkpoint_name}" restored.` });
3639
4716
  } catch (e) {
3640
4717
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error restoring checkpoint: ${e.message}` });
3641
4718
  }
@@ -3646,7 +4723,7 @@ ${history.slice(-25).join("\n") || "None"}`
3646
4723
  if (!el) {
3647
4724
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Element idx ${idx} not found.` });
3648
4725
  } else {
3649
- console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Probando inyecci\xF3n XSS en IDX ${idx}...`);
4726
+ console.log(`>>ARCALITY_STATUS>> ??? [Security] Probando inyecci\uFFFDn XSS en IDX ${idx}...`);
3650
4727
  let payload = "<script>alert(1)</script>";
3651
4728
  if (payload_type === "image")
3652
4729
  payload = "<img src=x onerror=alert(1)>";
@@ -3656,7 +4733,7 @@ ${history.slice(-25).join("\n") || "None"}`
3656
4733
  const frame = this.page.frames()[el.frameIdx] || this.page.mainFrame();
3657
4734
  const locator = frame.locator(el.selector).first();
3658
4735
  await locator.fill(payload);
3659
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Payload inyectado. Aseg\xFArate de probar si el payload fue ejecutado revisando el DOM, o realiza el sumbit y observa si el input regres\xF3 renderizado tal cual o sanitizado.` });
4736
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Payload inyectado. Aseg\uFFFDrate de probar si el payload fue ejecutado revisando el DOM, o realiza el sumbit y observa si el input regres\uFFFD renderizado tal cual o sanitizado.` });
3660
4737
  if (this.testInfo)
3661
4738
  this.testInfo.attach("security_tool_call", { body: JSON.stringify({ action: "test_xss_injection", target: idx, payload }), contentType: "application/json" });
3662
4739
  } catch (e) {
@@ -3665,7 +4742,7 @@ ${history.slice(-25).join("\n") || "None"}`
3665
4742
  }
3666
4743
  } else if (toolName === "test_auth_bypass") {
3667
4744
  const { target_url } = toolInput;
3668
- console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Intentando Auth Bypass en ${target_url}...`);
4745
+ console.log(`>>ARCALITY_STATUS>> ??? [Security] Intentando Auth Bypass en ${target_url}...`);
3669
4746
  try {
3670
4747
  const baseUrl = new URL(this.page.url()).origin;
3671
4748
  const fullUrl = new URL(target_url, baseUrl).toString();
@@ -3677,15 +4754,15 @@ ${history.slice(-25).join("\n") || "None"}`
3677
4754
  URL resultante: ${resultingUrl}.
3678
4755
  `;
3679
4756
  if (resultingUrl !== fullUrl)
3680
- resultData += `Posiblemente redirigido (el bypass fall\xF3 o te enviaron a Login).`;
4757
+ resultData += `Posiblemente redirigido (el bypass fall\uFFFD o te enviaron a Login).`;
3681
4758
  else
3682
- resultData += `URL se mantuvo igual. \xA1Esto podr\xEDa ser un IDOR o Auth Bypass si ves datos a los que no deber\xEDas acceder!`;
4759
+ resultData += `URL se mantuvo igual. \uFFFDEsto podr\uFFFDa ser un IDOR o Auth Bypass si ves datos a los que no deber\uFFFDas acceder!`;
3683
4760
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: resultData });
3684
4761
  } catch (e) {
3685
4762
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error test_auth_bypass: ${e.message}` });
3686
4763
  }
3687
4764
  } else if (toolName === "scan_sensitive_data_exposure") {
3688
- console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Escaneando datos sensibles...`);
4765
+ console.log(`>>ARCALITY_STATUS>> ??? [Security] Escaneando datos sensibles...`);
3689
4766
  try {
3690
4767
  const issues = await scan_sensitive_data_exposure(this.page);
3691
4768
  if (issues.length > 0) {
@@ -3700,7 +4777,7 @@ ${report}` });
3700
4777
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scan_sensitive_data_exposure: ${e.message}` });
3701
4778
  }
3702
4779
  } else if (toolName === "audit_http_headers") {
3703
- console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Auditando headers HTTP...`);
4780
+ console.log(`>>ARCALITY_STATUS>> ??? [Security] Auditando headers HTTP...`);
3704
4781
  try {
3705
4782
  const issues = await audit_http_headers(this.page);
3706
4783
  if (issues.length > 0) {
@@ -3708,14 +4785,14 @@ ${report}` });
3708
4785
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Vulnerabilidades de Headers detectadas:
3709
4786
  ${report}` });
3710
4787
  } else {
3711
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "No se detectaron problemas en los headers de seguridad b\xE1sicos." });
4788
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "No se detectaron problemas en los headers de seguridad b\uFFFDsicos." });
3712
4789
  }
3713
4790
  } catch (e) {
3714
4791
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error audit_http_headers: ${e.message}` });
3715
4792
  }
3716
4793
  } else if (toolName === "create_test_evidence") {
3717
4794
  const { evidence_type, annotations, save_as, description, finish_run = false } = toolInput;
3718
- console.log(`\u{1F4F8} [SKILL] Creating test evidence: "${save_as}"...`);
4795
+ console.log(`?? [SKILL] Creating test evidence: "${save_as}"...`);
3719
4796
  try {
3720
4797
  const filename = save_as.replace(/[^a-zA-Z0-9_-]/g, "_");
3721
4798
  if (annotations && annotations.length > 0) {
@@ -3764,20 +4841,20 @@ ${report}` });
3764
4841
  contentType: "text/plain"
3765
4842
  });
3766
4843
  }
3767
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Evidence "${filename}" created and attached.` });
4844
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Evidence "${filename}" created and attached.` });
3768
4845
  if (finish_run) {
3769
4846
  this.lastScreenshot = base64Img;
3770
4847
  return { thought: `Evidence "${description || filename}" created. Mission complete.`, actions: [], finish: true };
3771
4848
  }
3772
4849
  } else {
3773
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u26A0\uFE0F testInfo not available.` });
4850
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `?? testInfo not available.` });
3774
4851
  }
3775
4852
  } catch (e) {
3776
4853
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error creating evidence: ${e.message}` });
3777
4854
  }
3778
4855
  } else if (toolName === "send_keyboard_event") {
3779
4856
  const { key, idx, repeat = 1 } = toolInput;
3780
- console.log(`>>ARCALITY_STATUS>> \u2328\uFE0F Enviando tecla: "${key}" ${idx !== void 0 ? `(focus en IDX:${idx})` : "(focus actual)"} x${repeat}`);
4857
+ console.log(`>>ARCALITY_STATUS>> ?? Enviando tecla: "${key}" ${idx !== void 0 ? `(focus en IDX:${idx})` : "(focus actual)"} x${repeat}`);
3781
4858
  try {
3782
4859
  if (idx !== void 0) {
3783
4860
  const c = state.components.find((comp) => comp.idx === idx);
@@ -3793,13 +4870,13 @@ ${report}` });
3793
4870
  if (repeat > 1)
3794
4871
  await this.page.waitForTimeout(100);
3795
4872
  }
3796
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Key "${key}" pressed ${repeat} time(s).` });
4873
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Key "${key}" pressed ${repeat} time(s).` });
3797
4874
  } catch (e) {
3798
4875
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error sending key: ${e.message}` });
3799
4876
  }
3800
4877
  } else if (toolName === "type_sequentially") {
3801
4878
  const { idx, text, delay_ms = 80, clear_first = true } = toolInput;
3802
- console.log(`>>ARCALITY_STATUS>> \u2328\uFE0F\u270D\uFE0F Type sequentially IDX:${idx} \u2192 "${text}"`);
4879
+ console.log(`>>ARCALITY_STATUS>> ???? Type sequentially IDX:${idx} ? "${text}"`);
3803
4880
  const c = state.components.find((comp) => comp.idx === idx);
3804
4881
  if (!c) {
3805
4882
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "Element not found." });
@@ -3830,7 +4907,7 @@ ${report}` });
3830
4907
  toolResults.push({
3831
4908
  type: "tool_result",
3832
4909
  tool_use_id: toolUse.id,
3833
- content: `\u2705 Sequential typing completed. Current value: "${finalValue}".`
4910
+ content: `? Sequential typing completed. Current value: "${finalValue}".`
3834
4911
  });
3835
4912
  } catch (e) {
3836
4913
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error typing sequentially: ${e.message}` });
@@ -3839,7 +4916,7 @@ ${report}` });
3839
4916
  } else if (toolName === "interact_native_control") {
3840
4917
  const { idx, control_type, value } = toolInput;
3841
4918
  const c = state.components.find((comp) => comp.idx === idx);
3842
- console.log(`>>ARCALITY_STATUS>> \u{1F39B}\uFE0F Control nativo: ${control_type} IDX:${idx} \u2192 "${value}"`);
4919
+ console.log(`>>ARCALITY_STATUS>> ??? Control nativo: ${control_type} IDX:${idx} ? "${value}"`);
3843
4920
  if (!c) {
3844
4921
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "Element not found." });
3845
4922
  } else {
@@ -3854,7 +4931,7 @@ ${report}` });
3854
4931
  await loc.fill(value, { timeout: 3e3 });
3855
4932
  const actualValue = await loc.inputValue();
3856
4933
  if (actualValue && actualValue !== "") {
3857
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 ${control_type} set to "${actualValue}" via fill().` });
4934
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? ${control_type} set to "${actualValue}" via fill().` });
3858
4935
  break;
3859
4936
  }
3860
4937
  } catch {
@@ -3865,12 +4942,12 @@ ${report}` });
3865
4942
  const digits = value.replace(/[:-T]/g, "");
3866
4943
  await this.page.keyboard.type(digits, { delay: 50 });
3867
4944
  const finalValue = await loc.inputValue().catch(() => "");
3868
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 ${control_type} set via keyboard. Current value: "${finalValue}".` });
4945
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? ${control_type} set via keyboard. Current value: "${finalValue}".` });
3869
4946
  break;
3870
4947
  }
3871
4948
  case "color":
3872
4949
  await loc.fill(value, { timeout: 3e3 });
3873
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Color set to "${value}".` });
4950
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Color set to "${value}".` });
3874
4951
  break;
3875
4952
  case "range": {
3876
4953
  const numVal = parseFloat(value);
@@ -3884,7 +4961,7 @@ ${report}` });
3884
4961
  }
3885
4962
  }, { sel: c.selector, val: String(numVal) });
3886
4963
  });
3887
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Range set to ${numVal}.` });
4964
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Range set to ${numVal}.` });
3888
4965
  break;
3889
4966
  }
3890
4967
  case "select": {
@@ -3898,17 +4975,17 @@ ${report}` });
3898
4975
  }
3899
4976
  }
3900
4977
  const selected = await loc.inputValue().catch(() => "unknown");
3901
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Option selected. Value: "${selected}".` });
4978
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Option selected. Value: "${selected}".` });
3902
4979
  break;
3903
4980
  }
3904
4981
  case "contenteditable":
3905
4982
  await loc.click({ force: true });
3906
4983
  await this.page.keyboard.press("Control+a");
3907
4984
  await this.page.keyboard.type(value, { delay: 30 });
3908
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Content set to "${value}".` });
4985
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Content set to "${value}".` });
3909
4986
  break;
3910
4987
  default:
3911
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u274C Unknown control type: ${control_type}` });
4988
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Unknown control type: ${control_type}` });
3912
4989
  }
3913
4990
  } catch (e) {
3914
4991
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error interacting with native control: ${e.message}` });
@@ -3916,7 +4993,7 @@ ${report}` });
3916
4993
  }
3917
4994
  } else if (toolName === "scroll_page") {
3918
4995
  const { direction, amount = 500, idx } = toolInput;
3919
- console.log(`>>ARCALITY_STATUS>> \u{1F4DC} Scroll: ${direction} ${amount}px ${idx !== void 0 ? `(container IDX:${idx})` : "(page)"}`);
4996
+ console.log(`>>ARCALITY_STATUS>> ?? Scroll: ${direction} ${amount}px ${idx !== void 0 ? `(container IDX:${idx})` : "(page)"}`);
3920
4997
  try {
3921
4998
  if (idx !== void 0) {
3922
4999
  const c = state.components.find((comp) => comp.idx === idx);
@@ -3962,7 +5039,7 @@ ${report}` });
3962
5039
  }
3963
5040
  }
3964
5041
  await this.page.waitForTimeout(300);
3965
- toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Scrolled ${direction} ${direction === "top" || direction === "bottom" ? "completely" : `${amount}px`}. Page state will refresh on next turn.` });
5042
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Scrolled ${direction} ${direction === "top" || direction === "bottom" ? "completely" : `${amount}px`}. Page state will refresh on next turn.` });
3966
5043
  } catch (e) {
3967
5044
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scrolling: ${e.message}` });
3968
5045
  }
@@ -4001,7 +5078,7 @@ ${report}` });
4001
5078
  return { thought: "Max internal turns reached", actions: [] };
4002
5079
  }
4003
5080
  /**
4004
- * QA Skill: Genera una explicación "humana" y amigable de por qué la misión fue exitosa.
5081
+ * QA Skill: Genera una explicaci�n "humana" y amigable de por qu� la misi�n fue exitosa.
4005
5082
  */
4006
5083
  async humanizeMissionResult(prompt, history) {
4007
5084
  if (!process.env.ANTHROPIC_API_KEY)
@@ -4027,9 +5104,9 @@ ${report}` });
4027
5104
  body: JSON.stringify({
4028
5105
  model: process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest",
4029
5106
  max_tokens: 300,
4030
- system: "Eres un Senior QA personalizador de reportes. Tu tarea es resumir en una sola frase corta, profesional y amigable por qu\xE9 una misi\xF3n de testing fue exitosa bas\xE1ndote en el historial de pasos. Usa un tono de victoria. No menciones detalles t\xE9cnicos como 'selectores' o 'DOM'. El resumen debe empezar con algo como '\xA1Misi\xF3n cumplida! ...'",
5107
+ system: "Eres un Senior QA personalizador de reportes. Tu tarea es resumir en una sola frase corta, profesional y amigable por qu\uFFFD una misi\uFFFDn de testing fue exitosa bas\uFFFDndote en el historial de pasos. Usa un tono de victoria. No menciones detalles t\uFFFDcnicos como 'selectores' o 'DOM'. El resumen debe empezar con algo como '\uFFFDMisi\uFFFDn cumplida! ...'",
4031
5108
  messages: [
4032
- { role: "user", content: `Misi\xF3n original: ${prompt}
5109
+ { role: "user", content: `Misi\uFFFDn original: ${prompt}
4033
5110
 
4034
5111
  history of steps:
4035
5112
  ${history.join("\n")}` }
@@ -4218,8 +5295,8 @@ var SecurityScanner = class {
4218
5295
 
4219
5296
  // tests/_helpers/agentic-runner.spec.ts
4220
5297
  var import_config = require("dotenv/config");
4221
- var fs6 = __toESM(require("fs"));
4222
- var path6 = __toESM(require("path"));
5298
+ var fs7 = __toESM(require("fs"));
5299
+ var path8 = __toESM(require("path"));
4223
5300
  var crypto2 = __toESM(require("crypto"));
4224
5301
  init_asset_resolver();
4225
5302
  var _sessionRuleCache = /* @__PURE__ */ new Set();
@@ -4273,6 +5350,25 @@ async function readHiddenPortalResult(page) {
4273
5350
  }
4274
5351
  return null;
4275
5352
  }
5353
+ function tryResolveQueryParamValue(baseUrl, targetPath) {
5354
+ const normalizedTarget = targetPath.trim();
5355
+ if (!normalizedTarget || /[/?#&=]/.test(normalizedTarget))
5356
+ return null;
5357
+ try {
5358
+ const parsedBase = new URL(baseUrl);
5359
+ const emptyParams = [];
5360
+ for (const [key, value] of parsedBase.searchParams.entries()) {
5361
+ if (!String(value || "").trim())
5362
+ emptyParams.push(key);
5363
+ }
5364
+ if (emptyParams.length !== 1)
5365
+ return null;
5366
+ parsedBase.searchParams.set(emptyParams[0], normalizedTarget);
5367
+ return parsedBase.toString();
5368
+ } catch {
5369
+ return null;
5370
+ }
5371
+ }
4276
5372
  function resolveTargetUrl(baseUrl, currentUrl, targetPath) {
4277
5373
  if (!targetPath)
4278
5374
  return currentUrl || baseUrl;
@@ -4286,6 +5382,9 @@ function resolveTargetUrl(baseUrl, currentUrl, targetPath) {
4286
5382
  if (normalizedTarget.startsWith("?") || normalizedTarget.startsWith("#")) {
4287
5383
  return new URL(normalizedTarget, parsedReference).toString();
4288
5384
  }
5385
+ const queryParamUrl = tryResolveQueryParamValue(parsedBase.toString(), normalizedTarget);
5386
+ if (queryParamUrl)
5387
+ return queryParamUrl;
4289
5388
  if (normalizedTarget.startsWith("/")) {
4290
5389
  const targetAlreadyIncludesBase = basePath && (normalizedTarget === basePath || normalizedTarget.startsWith(`${basePath}/`));
4291
5390
  const combinedPath = targetAlreadyIncludesBase ? normalizedTarget : `${basePath}${normalizedTarget}`.replace(/\/{2,}/g, "/");
@@ -4302,11 +5401,11 @@ function writeBatchMissionResult(payload) {
4302
5401
  if (!runDomainDir)
4303
5402
  return;
4304
5403
  try {
4305
- if (!fs6.existsSync(runDomainDir)) {
4306
- fs6.mkdirSync(runDomainDir, { recursive: true });
5404
+ if (!fs7.existsSync(runDomainDir)) {
5405
+ fs7.mkdirSync(runDomainDir, { recursive: true });
4307
5406
  }
4308
- fs6.writeFileSync(
4309
- path6.join(runDomainDir, "mission-result.json"),
5407
+ fs7.writeFileSync(
5408
+ path8.join(runDomainDir, "mission-result.json"),
4310
5409
  JSON.stringify(payload, null, 2)
4311
5410
  );
4312
5411
  } catch {
@@ -4335,10 +5434,10 @@ function writeBatchMissionResult(payload) {
4335
5434
  } catch (e) {
4336
5435
  console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al sincronizar assets: ${e.message}`);
4337
5436
  }
4338
- const memoryFile = path6.join(contextDir, "memoria-arcality.json");
4339
- if (fs6.existsSync(memoryFile)) {
5437
+ const memoryFile = path8.join(contextDir, "memoria-arcality.json");
5438
+ if (fs7.existsSync(memoryFile)) {
4340
5439
  try {
4341
- fs6.unlinkSync(memoryFile);
5440
+ fs7.unlinkSync(memoryFile);
4342
5441
  } catch {
4343
5442
  }
4344
5443
  }
@@ -4393,33 +5492,64 @@ function writeBatchMissionResult(payload) {
4393
5492
  let accumulatedCost = 0;
4394
5493
  let totalMissionUsage = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
4395
5494
  let bugClassification = "";
5495
+ let databaseValidationReport = null;
4396
5496
  const missionSummaryPaths = /* @__PURE__ */ new Set();
4397
- const updateSavedMissionClassification = () => {
4398
- if (!bugClassification)
4399
- return;
5497
+ const updateSavedMissionSummaries = () => {
4400
5498
  for (const summaryPath of missionSummaryPaths) {
4401
5499
  try {
4402
- if (!fs6.existsSync(summaryPath))
5500
+ if (!fs7.existsSync(summaryPath))
4403
5501
  continue;
4404
- const data = JSON.parse(fs6.readFileSync(summaryPath, "utf8"));
4405
- data.bug_classification = bugClassification;
4406
- fs6.writeFileSync(summaryPath, JSON.stringify(data, null, 2));
5502
+ const data = JSON.parse(fs7.readFileSync(summaryPath, "utf8"));
5503
+ data.success = aiMarkedSuccess && !hasCriticalError;
5504
+ data.error = hasCriticalError;
5505
+ data.fail_reason = failReason;
5506
+ data.fail_reasoning = failReasoning;
5507
+ if (bugClassification)
5508
+ data.bug_classification = bugClassification;
5509
+ if (databaseValidationReport)
5510
+ data.database_validation = databaseValidationReport;
5511
+ fs7.writeFileSync(summaryPath, JSON.stringify(data, null, 2));
4407
5512
  } catch {
4408
5513
  }
4409
5514
  }
4410
5515
  };
5516
+ const updateSavedMissionClassification = updateSavedMissionSummaries;
5517
+ const runPostMissionDatabaseValidations = async () => {
5518
+ if (!aiMarkedSuccess || hasCriticalError || databaseValidationReport)
5519
+ return;
5520
+ const rawValidations = process.env.ARCALITY_DB_VALIDATIONS_JSON || process.env.ARCALITY_DB_VALIDATIONS || "";
5521
+ if (!rawValidations.trim())
5522
+ return;
5523
+ console.log(`>>ARCALITY_STATUS>> [DB] Ejecutando corroboracion post-mision...`);
5524
+ databaseValidationReport = await runDatabaseValidationsFromEnv();
5525
+ if (!databaseValidationReport)
5526
+ return;
5527
+ await testInfo.attach("database_validation_report", {
5528
+ body: Buffer.from(JSON.stringify(databaseValidationReport, null, 2), "utf-8"),
5529
+ contentType: "application/json"
5530
+ });
5531
+ const summary = databaseValidationReport.summary || { total: 0, passed: 0, failed: 0 };
5532
+ history.push(`Turno ${stepCount}: [DB_VALIDATION] ${summary.passed}/${summary.total} validaciones BD exitosas.`);
5533
+ if (!databaseValidationReport.passed) {
5534
+ aiMarkedSuccess = false;
5535
+ hasCriticalError = true;
5536
+ failReason = "database_validation_failed";
5537
+ failReasoning = `La UI parecia exitosa, pero la corroboracion en PostgreSQL fallo (${summary.failed}/${summary.total} validaciones fallidas). Revisa la seccion Validacion BD del reporte.`;
5538
+ }
5539
+ updateSavedMissionSummaries();
5540
+ };
4411
5541
  const saveMissionResults = () => {
4412
5542
  if (resultsSaved)
4413
5543
  return;
4414
5544
  resultsSaved = true;
4415
- if (!fs6.existsSync(contextDir))
4416
- fs6.mkdirSync(contextDir, { recursive: true });
5545
+ if (!fs7.existsSync(contextDir))
5546
+ fs7.mkdirSync(contextDir, { recursive: true });
4417
5547
  const finalSuccess = aiMarkedSuccess && !hasCriticalError;
4418
5548
  try {
4419
- const memoryFile2 = path6.join(contextDir, "memoria-arcality.json");
5549
+ const memoryFile2 = path8.join(contextDir, "memoria-arcality.json");
4420
5550
  let memories = [];
4421
- if (fs6.existsSync(memoryFile2)) {
4422
- const content = fs6.readFileSync(memoryFile2, "utf8").trim();
5551
+ if (fs7.existsSync(memoryFile2)) {
5552
+ const content = fs7.readFileSync(memoryFile2, "utf8").trim();
4423
5553
  if (content) {
4424
5554
  try {
4425
5555
  memories = JSON.parse(content);
@@ -4491,10 +5621,11 @@ function writeBatchMissionResult(payload) {
4491
5621
  fail_reason: failReason,
4492
5622
  fail_reasoning: failReasoning,
4493
5623
  bug_classification: bugClassification || null,
4494
- timestamp: Date.now()
5624
+ timestamp: Date.now(),
5625
+ database_validation: databaseValidationReport
4495
5626
  };
4496
5627
  memories.push(missionData);
4497
- fs6.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
5628
+ fs7.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
4498
5629
  if (process.env.DEBUG)
4499
5630
  console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${cleanSteps.length} pasos limpios (${rawSteps.length - cleanSteps.length} corruptos filtrados)`);
4500
5631
  } catch (err) {
@@ -4516,23 +5647,24 @@ function writeBatchMissionResult(payload) {
4516
5647
  target: process.env.BASE_URL || "",
4517
5648
  target_path: process.env.TARGET_PATH || "",
4518
5649
  report_dir: process.env.REPORTS_DIR || "",
4519
- timestamp: timestampIso
5650
+ timestamp: timestampIso,
5651
+ database_validation: databaseValidationReport
4520
5652
  };
4521
- const logPath = path6.join(contextDir, `agent-log-${timestampMs}.json`);
4522
- fs6.writeFileSync(logPath, JSON.stringify(summaryPayload, null, 2));
5653
+ const logPath = path8.join(contextDir, `agent-log-${timestampMs}.json`);
5654
+ fs7.writeFileSync(logPath, JSON.stringify(summaryPayload, null, 2));
4523
5655
  missionSummaryPaths.add(logPath);
4524
5656
  const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR;
4525
5657
  if (!runDomainDir) {
4526
5658
  throw new Error("Missing ARCALITY_RUN_DOMAIN_DIR. Arcality must run through the CLI wrapper so durable logs stay inside the user project .arcality directory.");
4527
5659
  }
4528
- const durableLogsDir = path6.join(runDomainDir, "logs");
4529
- fs6.mkdirSync(durableLogsDir, { recursive: true });
4530
- const durableLogPath = path6.join(durableLogsDir, `mission-log-${timestampMs}.json`);
4531
- fs6.writeFileSync(durableLogPath, JSON.stringify(summaryPayload, null, 2));
5660
+ const durableLogsDir = path8.join(runDomainDir, "logs");
5661
+ fs7.mkdirSync(durableLogsDir, { recursive: true });
5662
+ const durableLogPath = path8.join(durableLogsDir, `mission-log-${timestampMs}.json`);
5663
+ fs7.writeFileSync(durableLogPath, JSON.stringify(summaryPayload, null, 2));
4532
5664
  missionSummaryPaths.add(durableLogPath);
4533
5665
  writeBatchMissionResult(summaryPayload);
4534
5666
  if (runDomainDir) {
4535
- missionSummaryPaths.add(path6.join(runDomainDir, "mission-result.json"));
5667
+ missionSummaryPaths.add(path8.join(runDomainDir, "mission-result.json"));
4536
5668
  }
4537
5669
  try {
4538
5670
  const historyLine = JSON.stringify({
@@ -4543,12 +5675,12 @@ function writeBatchMissionResult(payload) {
4543
5675
  fail_reason: failReason,
4544
5676
  report_dir: process.env.REPORTS_DIR || ""
4545
5677
  }) + "\n";
4546
- fs6.appendFileSync(
4547
- path6.join(contextDir, "arcality-history.log"),
5678
+ fs7.appendFileSync(
5679
+ path8.join(contextDir, "arcality-history.log"),
4548
5680
  historyLine
4549
5681
  );
4550
- fs6.appendFileSync(
4551
- path6.join(durableLogsDir, "arcality-history.log"),
5682
+ fs7.appendFileSync(
5683
+ path8.join(durableLogsDir, "arcality-history.log"),
4552
5684
  historyLine
4553
5685
  );
4554
5686
  } catch (e) {
@@ -4653,7 +5785,7 @@ function writeBatchMissionResult(payload) {
4653
5785
  console.log(`
4654
5786
  \u{1F916} [ARCALITY] Iniciando misi\xF3n: "${prompt}"`);
4655
5787
  const base = process.env.BASE_URL || "";
4656
- const target = process.env.TARGET_PATH || "/";
5788
+ const target = process.env.TARGET_PATH ?? "/";
4657
5789
  if (process.env.LOGIN_USER && process.env.LOGIN_PASSWORD) {
4658
5790
  console.log(">>ARCALITY_STATUS>> \u{1F511} Realizando login autom\xE1tico...");
4659
5791
  const loginUrl = base + "/login";
@@ -4746,11 +5878,11 @@ function writeBatchMissionResult(payload) {
4746
5878
  timestamp: Date.now()
4747
5879
  };
4748
5880
  try {
4749
- const memoryFile2 = path6.join(contextDir, "memoria-agente.json");
5881
+ const memoryFile2 = path8.join(contextDir, "memoria-agente.json");
4750
5882
  let memories = [];
4751
- if (fs6.existsSync(memoryFile2)) {
5883
+ if (fs7.existsSync(memoryFile2)) {
4752
5884
  try {
4753
- memories = JSON.parse(fs6.readFileSync(memoryFile2, "utf8"));
5885
+ memories = JSON.parse(fs7.readFileSync(memoryFile2, "utf8"));
4754
5886
  } catch {
4755
5887
  memories = [];
4756
5888
  }
@@ -4762,14 +5894,14 @@ function writeBatchMissionResult(payload) {
4762
5894
  if (existingIdx < 0) {
4763
5895
  if (resolvedStepsData.length > 0) {
4764
5896
  memories.push(localMemoryData);
4765
- fs6.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
5897
+ fs7.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
4766
5898
  console.log(`>>ARCALITY_STATUS>> \u2728 Memoria sincronizada en memoria-agente.json (${resolvedStepsData.length} pasos). Arcality entrar\xE1 en MODO GU\xCDA.`);
4767
5899
  } else {
4768
5900
  console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Memoria NO sincronizada: el patr\xF3n del backend tiene 0 pasos t\xE9cnicos v\xE1lidos (steps_technical vac\xEDo).`);
4769
5901
  }
4770
5902
  } else if (!existingHasSteps && resolvedStepsData.length > 0) {
4771
5903
  memories[existingIdx] = localMemoryData;
4772
- fs6.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
5904
+ fs7.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
4773
5905
  console.log(`>>ARCALITY_STATUS>> \u{1F504} Patr\xF3n corrupto (0 pasos) sobreescrito en memoria-agente.json (${resolvedStepsData.length} pasos nuevos). MODO GU\xCDA activado.`);
4774
5906
  } else {
4775
5907
  console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F Patr\xF3n ya existe en memoria-agente.json con ${(memories[existingIdx]?.steps_data || []).length} pasos. MODO GU\xCDA activado sin re-escritura.`);
@@ -5151,7 +6283,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
5151
6283
  const lastSixHistory = history.slice(-6);
5152
6284
  const consecutiveWaits = lastSixHistory.filter((h) => h.includes('wait en "')).length;
5153
6285
  if (consecutiveWaits >= 3) {
5154
- const blankEvidencePath = path6.join(contextDir, `portal-blank-state-${Date.now()}.png`);
6286
+ const blankEvidencePath = path8.join(contextDir, `portal-blank-state-${Date.now()}.png`);
5155
6287
  await page.screenshot({ path: blankEvidencePath, fullPage: true }).catch(() => {
5156
6288
  });
5157
6289
  const blankUrl = page.url();
@@ -5428,7 +6560,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
5428
6560
  const panelCount = Math.min(panels.length, 6);
5429
6561
  if (panelCount >= 2 && emptyPanelCount >= panelCount * 0.6) {
5430
6562
  const blankUrl = page.url();
5431
- const blankEvidencePath = path6.join(contextDir, `portal-blank-dashboard-${Date.now()}.png`);
6563
+ const blankEvidencePath = path8.join(contextDir, `portal-blank-dashboard-${Date.now()}.png`);
5432
6564
  await page.screenshot({ path: blankEvidencePath, fullPage: true }).catch(() => {
5433
6565
  });
5434
6566
  console.error(`
@@ -5650,6 +6782,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
5650
6782
  }
5651
6783
  }
5652
6784
  }
6785
+ await runPostMissionDatabaseValidations();
5653
6786
  if (aiMarkedSuccess) {
5654
6787
  hasCriticalError = false;
5655
6788
  await persistCollectiveMemory();
@@ -5712,8 +6845,8 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
5712
6845
  console.log("\u{1F916} [SMART-YAML] Generating mission card...");
5713
6846
  const smartYaml = await agent.summarizeMissionToYaml(prompt, history, target);
5714
6847
  if (smartYaml) {
5715
- const yamlPath = path6.join(contextDir, "last-mission-smart.yaml");
5716
- fs6.writeFileSync(yamlPath, smartYaml);
6848
+ const yamlPath = path8.join(contextDir, "last-mission-smart.yaml");
6849
+ fs7.writeFileSync(yamlPath, smartYaml);
5717
6850
  console.log(`>>ARCALITY_STATUS>> \u{1F4DD} Smart Mission Card generated.`);
5718
6851
  await testInfo.attach("smart_mission_card", {
5719
6852
  body: Buffer.from(smartYaml, "utf-8"),
@@ -5815,15 +6948,15 @@ ${failReasoning}
5815
6948
  if (process.env.DEBUG)
5816
6949
  console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
5817
6950
  }
5818
- if (fs6.existsSync(contextDir)) {
6951
+ if (fs7.existsSync(contextDir)) {
5819
6952
  try {
5820
- const files = fs6.readdirSync(contextDir);
6953
+ const files = fs7.readdirSync(contextDir);
5821
6954
  for (const file of files) {
5822
6955
  if (file.endsWith(".json") || file.endsWith(".png")) {
5823
- fs6.unlinkSync(path6.join(contextDir, file));
6956
+ fs7.unlinkSync(path8.join(contextDir, file));
5824
6957
  }
5825
6958
  }
5826
- fs6.rmSync(contextDir, { recursive: true, force: true });
6959
+ fs7.rmSync(contextDir, { recursive: true, force: true });
5827
6960
  console.log(`>>ARCALITY_STATUS>> \u{1F9F9} Carpeta context eliminada (Memoria ef\xEDmera completada).`);
5828
6961
  } catch (err) {
5829
6962
  console.warn(`No se pudo limpiar la carpeta context: ${err}`);