@tea-agent/loop-agent 0.39.0-beta.12 → 0.39.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/build-stamp.json +3 -3
- package/dist/commands/client-recovery.js +8 -36
- package/dist/executors/dag-pi-executor.js +343 -40
- package/dist/executors/pi-executor.js +8 -4
- package/dist/executors/pi-sdk-executor.js +33 -5
- package/dist/executors/shell-executor.js +102 -32
- package/dist/governance/checks.js +1 -0
- package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
- package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
- package/dist/shared/pi-context-pressure/env.js +58 -0
- package/dist/shared/pi-context-pressure/extension.js +100 -0
- package/dist/shared/pi-context-pressure/index.js +7 -0
- package/dist/shared/pi-context-pressure/overflow.js +252 -0
- package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
- package/dist/shared/pi-context-pressure/telemetry.js +51 -0
- package/dist/task/frontend-project-capability.js +3 -1
- package/dist/task/source-prepare/fragment-inventory.js +4 -1
- package/dist/worker/console/chat/pi-runtime.js +146 -5
- package/dist/worker/console/chat/provider-error.js +2 -1
- package/dist/worker/console/chat/routes.js +3 -0
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +20 -2
- package/dist/worker/console/operator-actions.js +4 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CXj_GnSb.js → abnfDiagram-N423BO3Z-DC863mud.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BZp6JAp7.js → arc-CftC38G9.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DfpcEuYU.js → architectureDiagram-T3A2C74G-B3PAPQCw.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-_Gf0xadb.js → blockDiagram-VBNYF7ZC-C9UDJNRv.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct2QPCmv.js → c4Diagram-5PPSVZJV--BJ76fv_.js} +1 -1
- package/dist/worker/console/static/assets/channel-CUz-Bg86.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BfklkzKl.js → chunk-2GRJ4B5K--hiIqoGp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Dy25vZJV.js → chunk-2Q5K7J3B-DgTzpIa3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BFlCRZep.js → chunk-5RXB4S5H-DIwpJziP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-op3oVxIE.js → chunk-5VM5RSS4-BJzbdUi0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C0R7rzF2.js → chunk-6Q2QTUOP-BbGouI1Z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bt44TCGy.js → chunk-GF5L2VYU-B9247w69.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-_uEE_XFx.js → chunk-JWPE2WC7-CXob_wYy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-C3TOYGZ9.js → chunk-KBJHAD2P-C6FUel1g.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cz60oBPV.js → chunk-RYQCIY6F-DGKRYKHu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-8Bik0qis.js → chunk-XXDRQBXY-kNBqNPfx.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlUacmD.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlUacmD.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C2QIOA4a.js → cose-bilkent-JH36ORCC--xmkDwfD.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CSH_yUUd.js → cynefin-VYW2F7L2-Cw5FMMuG.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BDLw2XFM.js → cynefinDiagram-MW4NZA55-CuLslt2t.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CcD9ZtF4.js → dagre-VZM6K2ZE-D9ngqrs2.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DqwtkBsI.js → diagram-7IWD3JNH-RDRSbHQp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BWVEcqsC.js → diagram-B4RE2ZJO-BgQ1P9aV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-M-WAbtQi.js → diagram-LBJQPF4R-D3WWyPtn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DKW7SixP.js → diagram-Q27KOJAE-L2k28wTR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-PHHPPtrj.js → diagram-UB23O5K3-DDNrA5Qw.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BQD-B4RX.js → ebnfDiagram-BXEA7PRR-BaRFCOdM.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BFvULb51.js → erDiagram-JOGREHBK-CM33DVVM.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Bw-aXTCb.js → flowDiagram-UKHOOZJN-CVBSjOxe.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGKw04Qx.js → ganttDiagram-PKOTCBZU-BdscewE_.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-RqKaPR-I.js → gitGraphDiagram-DS77QQ5N-HRuSZb7g.js} +1 -1
- package/dist/worker/console/static/assets/{index-B_D8rbWc.js → index-B9JJQsVK.js} +102 -72
- package/dist/worker/console/static/assets/index-rWaGv4jz.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-YO5dnzrJ.js → infoDiagram-6WML65LV-cYfHvfAR.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js → ishikawaDiagram-WSZJBQD7-CZmaoOuy.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Bszls1DD.js → journeyDiagram-NVQOT4AX-ntYijh7g.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PhzeaZ09.js → kanban-definition-27J2QSJJ-Cz3b0DeD.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BsjbDoXi.js → linear-DvGonpsP.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-0B7NnWKk.js → mermaid.core-B-3vjfyW.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BJr4Fj-q.js → mindmap-definition-FAOFIHXS-5iKxlsX3.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-moC4fpGB.js → pegDiagram-VL7TDLO6-C8BUwapW.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BEw37-2c.js → pieDiagram-7S7Q4E2Y-B2rPoQQG.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq6LyasU.js → quadrantDiagram-CIZ2JOQS-jDeVUAy4.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DUCMcK0D.js → railroadDiagram-AXF67PYL-DV140Dwm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3upTZm7.js → requirementDiagram-LRYGKXZP-CnYgHtYC.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BI_gMsCW.js → sankeyDiagram-W5VNT64P-SIK3CdWw.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-YFOIRzfN.js → sequenceDiagram-SI44F4Z6-DW_vZix7.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-dOnB7UDD.js → sizeCapture-X5ZJPWSS-NBAtkg0C.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BXUniaIh.js → stateDiagram-OKZ733FA-Bi1bQxpi.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DjWKYjQ1.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-2tA4wTNu.js → swimlanes-SLNWSIFB-8PT_uP_i.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B4cdm7bH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DO0HkJXC.js → timeline-definition-Z64GVDOM-CD0ZZPk0.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DvIOzixv.js → vennDiagram-T6HMQDX7-BbEgWdhK.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DXDZ0cTj.js → wardleyDiagram-T6FBY63Y-DCwPKdLl.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B32Ark0D.js → xychartDiagram-ELKLHX3M-sfC5PcNg.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/static/styles.css +9 -0
- package/dist/worker/observe/static/views/dag-inspector.js +40 -0
- package/dist/worker/observe/static/views/session-timeline.js +135 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +157 -7
- package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
- package/dist/workflows/dag/backend-test-result-contract.js +4 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +118 -22
- package/dist/workflows/dag/frontend-shadow-dual-write.js +1 -1
- package/dist/workflows/dag/frontend-writer-admission.js +13 -0
- package/dist/workflows/dag/init-hybrid.js +7 -3
- package/dist/workflows/dag/node-execution.js +277 -3
- package/dist/workflows/dag/rerun-feedback.js +135 -3
- package/dist/workflows/dag/retry-policy.js +13 -122
- package/dist/workflows/dag/types.js +6 -1
- package/docs/architecture/runtime-boundaries.md +2 -1
- package/docs/templates/backend-test-dag.json +4 -3
- package/package.json +4 -3
- package/dist/worker/console/static/assets/channel-3TxJgYaH.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +0 -1
- package/dist/worker/console/static/assets/index-BdNx6fj0.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.js +0 -8
|
@@ -20,7 +20,8 @@ const factsSchema = z
|
|
|
20
20
|
tpId: z.string(),
|
|
21
21
|
field: z.string().optional(),
|
|
22
22
|
intent: z.string(),
|
|
23
|
-
observed: z.string(),
|
|
23
|
+
observed: z.string().max(200),
|
|
24
|
+
observedLength: z.number().int().min(0).optional(),
|
|
24
25
|
status: z.enum(["MATCH", "MISMATCH", "UNDETERMINED"]),
|
|
25
26
|
repairability: z.enum([
|
|
26
27
|
"repairable",
|
|
@@ -28,6 +29,12 @@ const factsSchema = z
|
|
|
28
29
|
"none",
|
|
29
30
|
"undetermined-no-repair",
|
|
30
31
|
]),
|
|
32
|
+
reasonCode: z.enum(["SCENARIO_PARAM_MATCH", "SCENARIO_PARAM_MISMATCH", "SCENARIO_PARAM_UNDETERMINED"]),
|
|
33
|
+
expectedRaw: z.string().max(512).optional(),
|
|
34
|
+
observedRaw: z.string().max(512),
|
|
35
|
+
expectedNormalized: z.string().max(512).optional(),
|
|
36
|
+
observedNormalized: z.string().max(512).optional(),
|
|
37
|
+
sourceLocation: z.string().max(512).optional(),
|
|
31
38
|
suggestedFix: z.string().optional(),
|
|
32
39
|
scriptPath: z.string().optional(),
|
|
33
40
|
bound: z.number().optional(),
|
|
@@ -435,8 +442,16 @@ function isWhitespacePaddedString(value) {
|
|
|
435
442
|
}
|
|
436
443
|
function normalizeScenarioLiteralText(value) {
|
|
437
444
|
// Writers sometimes emit visible-space placeholders (U+2420), NBSP variants,
|
|
438
|
-
//
|
|
445
|
+
// URL-encoded spaces, or explicit Python/JSON whitespace escapes in
|
|
446
|
+
// machine-readable Markdown. Decode only escapes that resolve to whitespace;
|
|
447
|
+
// ordinary business characters such as `\\u0041` remain literal and fail closed.
|
|
448
|
+
const decodeWhitespaceEscape = (token, hex) => {
|
|
449
|
+
const decoded = String.fromCodePoint(Number.parseInt(hex, 16));
|
|
450
|
+
return /^\s$/u.test(decoded) || /[ ]/u.test(decoded) ? decoded : token;
|
|
451
|
+
};
|
|
439
452
|
const normalized = value
|
|
453
|
+
.replace(/\\u([0-9a-f]{4})/gi, decodeWhitespaceEscape)
|
|
454
|
+
.replace(/\\x([0-9a-f]{2})/gi, decodeWhitespaceEscape)
|
|
440
455
|
.replace(/␠/g, " ")
|
|
441
456
|
.replace(/ /g, " ")
|
|
442
457
|
.replace(/ /g, " ")
|
|
@@ -677,6 +692,50 @@ export function extractPytestParamBlock(source, tpId) {
|
|
|
677
692
|
}
|
|
678
693
|
return undefined;
|
|
679
694
|
}
|
|
695
|
+
function extractPytestParamTopLevelArgs(block) {
|
|
696
|
+
if (!block)
|
|
697
|
+
return [];
|
|
698
|
+
const open = block.indexOf("(");
|
|
699
|
+
const close = block.lastIndexOf(")");
|
|
700
|
+
if (open < 0 || close <= open)
|
|
701
|
+
return [];
|
|
702
|
+
const input = block.slice(open + 1, close);
|
|
703
|
+
const args = [];
|
|
704
|
+
let start = 0;
|
|
705
|
+
let depth = 0;
|
|
706
|
+
let quote = "";
|
|
707
|
+
let escaped = false;
|
|
708
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
709
|
+
const ch = input[index];
|
|
710
|
+
if (escaped) {
|
|
711
|
+
escaped = false;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (ch === "\\") {
|
|
715
|
+
escaped = true;
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (quote) {
|
|
719
|
+
if (ch === quote)
|
|
720
|
+
quote = "";
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (ch === '"' || ch === "'") {
|
|
724
|
+
quote = ch;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (ch === "[" || ch === "{" || ch === "(")
|
|
728
|
+
depth += 1;
|
|
729
|
+
else if (ch === "]" || ch === "}" || ch === ")")
|
|
730
|
+
depth -= 1;
|
|
731
|
+
else if (ch === "," && depth === 0) {
|
|
732
|
+
args.push(input.slice(start, index).trim());
|
|
733
|
+
start = index + 1;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
args.push(input.slice(start).trim());
|
|
737
|
+
return args.filter((arg) => arg && !/^(?:id|marks)\s*=/.test(arg));
|
|
738
|
+
}
|
|
680
739
|
function extractPytestParamParameterNames(source, tpId) {
|
|
681
740
|
const block = extractPytestParamBlock(source, tpId);
|
|
682
741
|
if (!block)
|
|
@@ -688,8 +747,11 @@ function extractPytestParamParameterNames(source, tpId) {
|
|
|
688
747
|
if (decoratorIndex < 0)
|
|
689
748
|
return [];
|
|
690
749
|
const prefix = source.slice(decoratorIndex, blockIndex);
|
|
691
|
-
const
|
|
692
|
-
|
|
750
|
+
const scalar = /@pytest\.mark\.parametrize\s*\(\s*["']([^"']+)["']/s.exec(prefix)?.[1];
|
|
751
|
+
if (scalar)
|
|
752
|
+
return scalar.split(",").map((name) => name.trim()).filter(Boolean);
|
|
753
|
+
const tuple = /@pytest\.mark\.parametrize\s*\(\s*\(\s*((?:["'][^"']+["']\s*,?\s*)+)\)/s.exec(prefix)?.[1];
|
|
754
|
+
return tuple ? [...tuple.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]).filter(Boolean) : [];
|
|
693
755
|
}
|
|
694
756
|
export function observeParamFeatures(block, field, constants, parameterNames) {
|
|
695
757
|
if (!block)
|
|
@@ -749,48 +811,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
|
|
|
749
811
|
}
|
|
750
812
|
// Parse top-level positional args before the legacy token scanner so list
|
|
751
813
|
// cardinality and field/value rows remain statically visible.
|
|
752
|
-
const topLevelArgs = (
|
|
753
|
-
const open = block.indexOf("(");
|
|
754
|
-
const close = block.lastIndexOf(")");
|
|
755
|
-
if (open < 0 || close <= open)
|
|
756
|
-
return [];
|
|
757
|
-
const input = block.slice(open + 1, close);
|
|
758
|
-
const args = [];
|
|
759
|
-
let start = 0;
|
|
760
|
-
let depth = 0;
|
|
761
|
-
let quote = "";
|
|
762
|
-
let escaped = false;
|
|
763
|
-
for (let i = 0; i < input.length; i += 1) {
|
|
764
|
-
const ch = input[i];
|
|
765
|
-
if (escaped) {
|
|
766
|
-
escaped = false;
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
769
|
-
if (ch === "\\") {
|
|
770
|
-
escaped = true;
|
|
771
|
-
continue;
|
|
772
|
-
}
|
|
773
|
-
if (quote) {
|
|
774
|
-
if (ch === quote)
|
|
775
|
-
quote = "";
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
if (ch === '"' || ch === "'") {
|
|
779
|
-
quote = ch;
|
|
780
|
-
continue;
|
|
781
|
-
}
|
|
782
|
-
if (ch === "[" || ch === "{" || ch === "(")
|
|
783
|
-
depth += 1;
|
|
784
|
-
else if (ch === "]" || ch === "}" || ch === ")")
|
|
785
|
-
depth -= 1;
|
|
786
|
-
else if (ch === "," && depth === 0) {
|
|
787
|
-
args.push(input.slice(start, i).trim());
|
|
788
|
-
start = i + 1;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
args.push(input.slice(start).trim());
|
|
792
|
-
return args.filter((arg) => arg && !/^(?:id|marks)\s*=/.test(arg));
|
|
793
|
-
})();
|
|
814
|
+
const topLevelArgs = extractPytestParamTopLevelArgs(block);
|
|
794
815
|
if (topLevelArgs.length > 0) {
|
|
795
816
|
const idTp = /\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/i.exec(block)?.[1]?.toUpperCase();
|
|
796
817
|
const meaningfulArgs = topLevelArgs.filter((arg) => {
|
|
@@ -1161,11 +1182,13 @@ function suggestedFixFor(intent, field, bound, example) {
|
|
|
1161
1182
|
: undefined;
|
|
1162
1183
|
default:
|
|
1163
1184
|
if (intent.startsWith("custom-literal:")) {
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1185
|
+
const expectedRaw = intent.slice("custom-literal:".length);
|
|
1186
|
+
const expected = normalizeCustomLiteralExpected(expectedRaw);
|
|
1187
|
+
if (isWhitespaceSemanticLiteral(expectedRaw) || isWhitespaceOnlyString(expected)) {
|
|
1188
|
+
const length = isWhitespaceOnlyString(expected) ? expected.length : 3;
|
|
1189
|
+
return field ? `set ${field} to ${length} literal spaces` : `set value to ${length} literal spaces`;
|
|
1167
1190
|
}
|
|
1168
|
-
if (isWhitespacePaddedSemanticLiteral(
|
|
1191
|
+
if (isWhitespacePaddedSemanticLiteral(expectedRaw)) {
|
|
1169
1192
|
return field
|
|
1170
1193
|
? `set ${field}=" ${field}-value "`
|
|
1171
1194
|
: 'set value=" value "';
|
|
@@ -1177,6 +1200,200 @@ function suggestedFixFor(intent, field, bound, example) {
|
|
|
1177
1200
|
return undefined;
|
|
1178
1201
|
}
|
|
1179
1202
|
}
|
|
1203
|
+
const MAX_PERSISTED_SCENARIO_TEXT = 192;
|
|
1204
|
+
function boundedScenarioText(value, logicalLength) {
|
|
1205
|
+
if (value.length <= MAX_PERSISTED_SCENARIO_TEXT)
|
|
1206
|
+
return value;
|
|
1207
|
+
const suffix = `<truncated length=${logicalLength ?? value.length}>`;
|
|
1208
|
+
return `${value.slice(0, Math.max(0, MAX_PERSISTED_SCENARIO_TEXT - suffix.length - 3))}...${suffix}`;
|
|
1209
|
+
}
|
|
1210
|
+
function localPythonFunctionRegion(source, functionName) {
|
|
1211
|
+
const escaped = functionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1212
|
+
const startMatch = new RegExp(`^def\\s+${escaped}\\s*\\(`, "m").exec(source);
|
|
1213
|
+
if (!startMatch || startMatch.index === undefined)
|
|
1214
|
+
return undefined;
|
|
1215
|
+
const start = startMatch.index;
|
|
1216
|
+
const remainder = source.slice(start + startMatch[0].length);
|
|
1217
|
+
const nextTopLevel = /^(?:def|class|@pytest\b|@(?:[A-Za-z_][A-Za-z0-9_.]*))\s*/m.exec(remainder);
|
|
1218
|
+
return source.slice(start, nextTopLevel?.index === undefined ? source.length : start + startMatch[0].length + nextTopLevel.index);
|
|
1219
|
+
}
|
|
1220
|
+
function splitPythonTopLevelArguments(input) {
|
|
1221
|
+
const args = [];
|
|
1222
|
+
let start = 0;
|
|
1223
|
+
let depth = 0;
|
|
1224
|
+
let quote = "";
|
|
1225
|
+
let escaped = false;
|
|
1226
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
1227
|
+
const ch = input[index];
|
|
1228
|
+
if (escaped) {
|
|
1229
|
+
escaped = false;
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (ch === "\\") {
|
|
1233
|
+
escaped = true;
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (quote) {
|
|
1237
|
+
if (ch === quote)
|
|
1238
|
+
quote = "";
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (ch === '"' || ch === "'") {
|
|
1242
|
+
quote = ch;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (ch === "[" || ch === "{" || ch === "(")
|
|
1246
|
+
depth += 1;
|
|
1247
|
+
else if (ch === "]" || ch === "}" || ch === ")")
|
|
1248
|
+
depth -= 1;
|
|
1249
|
+
else if (ch === "," && depth === 0) {
|
|
1250
|
+
args.push(input.slice(start, index).trim());
|
|
1251
|
+
start = index + 1;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
args.push(input.slice(start).trim());
|
|
1255
|
+
return args.filter(Boolean);
|
|
1256
|
+
}
|
|
1257
|
+
function parseSimplePythonCall(expression) {
|
|
1258
|
+
const call = /^([A-Za-z_][A-Za-z0-9_]*)\s*\(([\s\S]*)\)$/.exec(expression.trim());
|
|
1259
|
+
if (!call)
|
|
1260
|
+
return undefined;
|
|
1261
|
+
const positional = [];
|
|
1262
|
+
const keywords = new Map();
|
|
1263
|
+
for (const argument of splitPythonTopLevelArguments(call[2] ?? "")) {
|
|
1264
|
+
const keyword = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]+)$/.exec(argument);
|
|
1265
|
+
if (keyword) {
|
|
1266
|
+
if (keywords.has(keyword[1]))
|
|
1267
|
+
return undefined;
|
|
1268
|
+
keywords.set(keyword[1], keyword[2].trim());
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
if (keywords.size > 0 || /^\*{1,2}/.test(argument))
|
|
1272
|
+
return undefined;
|
|
1273
|
+
positional.push(argument);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return { name: call[1], positional, keywords };
|
|
1277
|
+
}
|
|
1278
|
+
function safeSameModuleDictHelper(source, helperName) {
|
|
1279
|
+
if (!helperName.startsWith("_"))
|
|
1280
|
+
return undefined;
|
|
1281
|
+
const region = localPythonFunctionRegion(source, helperName);
|
|
1282
|
+
if (!region)
|
|
1283
|
+
return undefined;
|
|
1284
|
+
const functionBody = region.slice(region.indexOf(":") + 1);
|
|
1285
|
+
const escapedName = helperName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1286
|
+
if (new RegExp(`\\b${escapedName}\\s*\\(`).test(functionBody))
|
|
1287
|
+
return undefined;
|
|
1288
|
+
if (/\b(?:Faker|fake|random|randint|uuid4|environ|getenv|requests|httpx|urlopen|subprocess|Popen|socket|open)\b/i.test(region))
|
|
1289
|
+
return undefined;
|
|
1290
|
+
const signature = new RegExp(`^def\\s+${escapedName}\\s*\\(([\\s\\S]*?)\\)\\s*(?:->[^:]+)?\\s*:`, "m").exec(region);
|
|
1291
|
+
if (!signature)
|
|
1292
|
+
return undefined;
|
|
1293
|
+
const parameters = [];
|
|
1294
|
+
for (const rawParameter of splitPythonTopLevelArguments(signature[1] ?? "")) {
|
|
1295
|
+
if (rawParameter === "*" || rawParameter === "/")
|
|
1296
|
+
continue;
|
|
1297
|
+
if (/^\*{1,2}/.test(rawParameter))
|
|
1298
|
+
return undefined;
|
|
1299
|
+
const parameter = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s*:[^=]+)?(?:\s*=\s*([\s\S]+))?$/.exec(rawParameter);
|
|
1300
|
+
if (!parameter)
|
|
1301
|
+
return undefined;
|
|
1302
|
+
parameters.push({ name: parameter[1], defaultExpression: parameter[2]?.trim() });
|
|
1303
|
+
}
|
|
1304
|
+
const directReturn = /\breturn\s*(\{[\s\S]*?\})/.exec(functionBody)?.[1];
|
|
1305
|
+
const assignment = /\b([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=\n]+)?=\s*(\{[\s\S]*?\})/.exec(functionBody);
|
|
1306
|
+
const assignedReturn = assignment && new RegExp(`\\breturn\\s+${assignment[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(functionBody)
|
|
1307
|
+
? assignment[2]
|
|
1308
|
+
: undefined;
|
|
1309
|
+
const dictExpression = directReturn ?? assignedReturn;
|
|
1310
|
+
if (!dictExpression)
|
|
1311
|
+
return undefined;
|
|
1312
|
+
const fieldBindings = new Map();
|
|
1313
|
+
for (const item of splitPythonTopLevelArguments(dictExpression.slice(1, -1))) {
|
|
1314
|
+
const binding = /^(?:["']([^"']+)["'])\s*:\s*([A-Za-z_][A-Za-z0-9_]*)$/.exec(item);
|
|
1315
|
+
if (!binding || !parameters.some((parameter) => parameter.name === binding[2]))
|
|
1316
|
+
return undefined;
|
|
1317
|
+
fieldBindings.set(binding[1], binding[2]);
|
|
1318
|
+
}
|
|
1319
|
+
return { parameters, fieldBindings };
|
|
1320
|
+
}
|
|
1321
|
+
function observeSameModulePayloadHelperField(input) {
|
|
1322
|
+
const payloadIndex = input.parameterNames.findIndex((name) => /^(?:payload|body|data|json)$/i.test(name));
|
|
1323
|
+
if (payloadIndex < 0)
|
|
1324
|
+
return undefined;
|
|
1325
|
+
let expression = extractPytestParamTopLevelArgs(input.block)[payloadIndex]?.trim() ?? "";
|
|
1326
|
+
const wrapper = parseSimplePythonCall(expression);
|
|
1327
|
+
if (wrapper?.name === "_without") {
|
|
1328
|
+
if (wrapper.positional.length !== 2 || wrapper.keywords.size > 0)
|
|
1329
|
+
return undefined;
|
|
1330
|
+
const removed = observeLiteralToken(wrapper.positional[1], input.field, input.constants);
|
|
1331
|
+
if (removed?.kind !== "string" || removed.literal !== input.field)
|
|
1332
|
+
return undefined;
|
|
1333
|
+
expression = wrapper.positional[0];
|
|
1334
|
+
const inner = parseSimplePythonCall(expression);
|
|
1335
|
+
if (!inner || !safeSameModuleDictHelper(input.source, inner.name))
|
|
1336
|
+
return undefined;
|
|
1337
|
+
return { kind: "missing-key", text: `missing:${input.field}` };
|
|
1338
|
+
}
|
|
1339
|
+
const call = parseSimplePythonCall(expression);
|
|
1340
|
+
if (!call)
|
|
1341
|
+
return undefined;
|
|
1342
|
+
const helper = safeSameModuleDictHelper(input.source, call.name);
|
|
1343
|
+
if (!helper)
|
|
1344
|
+
return undefined;
|
|
1345
|
+
const parameterName = helper.fieldBindings.get(input.field);
|
|
1346
|
+
if (!parameterName)
|
|
1347
|
+
return { kind: "missing-key", text: `missing:${input.field}` };
|
|
1348
|
+
const parameterIndex = helper.parameters.findIndex((parameter) => parameter.name === parameterName);
|
|
1349
|
+
if (parameterIndex < 0 || call.positional.length > helper.parameters.length)
|
|
1350
|
+
return undefined;
|
|
1351
|
+
const expressionForField = call.keywords.get(parameterName) ?? call.positional[parameterIndex] ?? helper.parameters[parameterIndex]?.defaultExpression;
|
|
1352
|
+
return expressionForField
|
|
1353
|
+
? observeLiteralToken(expressionForField, input.field, input.constants)
|
|
1354
|
+
: undefined;
|
|
1355
|
+
}
|
|
1356
|
+
function hasSafeSameModulePayloadHelper(source, block, parameterNames) {
|
|
1357
|
+
const payloadIndex = parameterNames.findIndex((name) => /^(?:payload|body|data|json)$/i.test(name));
|
|
1358
|
+
if (payloadIndex < 0)
|
|
1359
|
+
return false;
|
|
1360
|
+
const expression = extractPytestParamTopLevelArgs(block)[payloadIndex]?.trim() ?? "";
|
|
1361
|
+
const call = parseSimplePythonCall(expression);
|
|
1362
|
+
if (!call)
|
|
1363
|
+
return false;
|
|
1364
|
+
if (/\b(?:Faker|fake|random|randint|uuid4|environ|getenv|requests|httpx|urlopen|subprocess|Popen|socket|open)\b/i.test(expression))
|
|
1365
|
+
return false;
|
|
1366
|
+
return Boolean(safeSameModuleDictHelper(source, call.name));
|
|
1367
|
+
}
|
|
1368
|
+
function observeDeterministicFunctionFieldOverride(input) {
|
|
1369
|
+
const region = scenarioFunctionRegion(input.source, input.tpId);
|
|
1370
|
+
if (!region)
|
|
1371
|
+
return undefined;
|
|
1372
|
+
const escapedField = input.field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1373
|
+
const assignment = new RegExp(`^([ \\t]*)([A-Za-z_][A-Za-z0-9_]*)\\s*\\[\\s*["']${escapedField}["']\\s*\\]\\s*=\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*$`, "m").exec(region);
|
|
1374
|
+
if (!assignment)
|
|
1375
|
+
return undefined;
|
|
1376
|
+
const indentation = assignment[1]?.length ?? 0;
|
|
1377
|
+
const valueParameter = assignment[3];
|
|
1378
|
+
const valueIndex = input.parameterNames.indexOf(valueParameter);
|
|
1379
|
+
if (valueIndex < 0)
|
|
1380
|
+
return undefined;
|
|
1381
|
+
const beforeAssignment = region.slice(0, assignment.index);
|
|
1382
|
+
const guardMatches = [...beforeAssignment.matchAll(/^([ \\t]*)if\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/gm)];
|
|
1383
|
+
const nearestGuard = guardMatches.at(-1);
|
|
1384
|
+
if (nearestGuard && (nearestGuard[1]?.length ?? 0) < indentation) {
|
|
1385
|
+
const guardIndex = input.parameterNames.indexOf(nearestGuard[2]);
|
|
1386
|
+
if (guardIndex < 0)
|
|
1387
|
+
return undefined;
|
|
1388
|
+
const guardExpression = extractPytestParamTopLevelArgs(input.block)[guardIndex]?.trim();
|
|
1389
|
+
if (guardExpression !== "True")
|
|
1390
|
+
return undefined;
|
|
1391
|
+
}
|
|
1392
|
+
const valueExpression = extractPytestParamTopLevelArgs(input.block)[valueIndex]?.trim();
|
|
1393
|
+
return valueExpression
|
|
1394
|
+
? observeLiteralToken(valueExpression, input.field, input.constants)
|
|
1395
|
+
: undefined;
|
|
1396
|
+
}
|
|
1180
1397
|
function scenarioFunctionRegion(source, tpId) {
|
|
1181
1398
|
const idIndex = source.indexOf(`id="${tpId}"`) >= 0
|
|
1182
1399
|
? source.indexOf(`id="${tpId}"`)
|
|
@@ -1202,6 +1419,43 @@ function hasCreateDeleteDerivedJourney(source, tpId) {
|
|
|
1202
1419
|
(/\b_create[A-Za-z0-9_]*\s*\(/.test(region) &&
|
|
1203
1420
|
(/\b_delete[A-Za-z0-9_]*\s*\(/.test(region) || /\.delete\s*\(/.test(region) || /\.request\s*\(\s*["']DELETE["']/i.test(region)));
|
|
1204
1421
|
}
|
|
1422
|
+
function hasConditionalFieldOmission(source, tpId, field, parameterNames) {
|
|
1423
|
+
if (!field)
|
|
1424
|
+
return false;
|
|
1425
|
+
const region = scenarioFunctionRegion(source, tpId);
|
|
1426
|
+
const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1427
|
+
const candidateNames = parameterNames.filter((name) => {
|
|
1428
|
+
const normalized = name.toLowerCase();
|
|
1429
|
+
const normalizedField = field.toLowerCase();
|
|
1430
|
+
return normalized === normalizedField || normalized.startsWith(`${normalizedField}_`) || normalized.endsWith(`_${normalizedField}`);
|
|
1431
|
+
});
|
|
1432
|
+
return candidateNames.some((name) => {
|
|
1433
|
+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1434
|
+
return new RegExp(`\\{\\s*\\}\\s*if\\s+${escapedName}\\s+is\\s+None\\s+else\\s+\\{[\\s\\S]{0,200}?["']${escapedField}["']\\s*:\\s*${escapedName}\\b`).test(region) ||
|
|
1435
|
+
new RegExp(`\\{[\\s\\S]{0,200}?["']${escapedField}["']\\s*:\\s*${escapedName}\\b[\\s\\S]{0,200}?\\}\\s*if\\s+${escapedName}\\s+is\\s+not\\s+None\\s+else\\s+\\{\\s*\\}`).test(region) ||
|
|
1436
|
+
new RegExp(`if\\s+${escapedName}\\s+is\\s+not\\s+None\\s*:[\\s\\S]{0,200}?["']${escapedField}["']\\s*\\]\\s*=\\s*${escapedName}\\b`).test(region);
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
function scenarioParamSourceLocation(scriptPath, source, tpId) {
|
|
1440
|
+
const block = extractPytestParamBlock(source, tpId);
|
|
1441
|
+
if (!block)
|
|
1442
|
+
return undefined;
|
|
1443
|
+
const index = source.indexOf(block);
|
|
1444
|
+
if (index < 0)
|
|
1445
|
+
return undefined;
|
|
1446
|
+
return `${scriptPath}:${source.slice(0, index).split("\n").length}`;
|
|
1447
|
+
}
|
|
1448
|
+
function scenarioParamDiagnosticValues(intent, observed) {
|
|
1449
|
+
const expectedRaw = intent.startsWith("custom-literal:")
|
|
1450
|
+
? intent.slice("custom-literal:".length)
|
|
1451
|
+
: intent === "unknown" ? undefined : intent;
|
|
1452
|
+
return {
|
|
1453
|
+
expectedRaw,
|
|
1454
|
+
observedRaw: boundedScenarioText(observed.text, observed.length),
|
|
1455
|
+
expectedNormalized: expectedRaw === undefined ? undefined : normalizeCustomLiteralExpected(expectedRaw).slice(0, 512),
|
|
1456
|
+
observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal), observed.length),
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1205
1459
|
export async function assessBackendScenarioParamConsistency(input) {
|
|
1206
1460
|
const cases = await listMarkdownCases(input.workspaceRoot, input.layout);
|
|
1207
1461
|
const entries = [];
|
|
@@ -1223,7 +1477,26 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
1223
1477
|
const constants = source ? collectPythonStringConstants(source) : undefined;
|
|
1224
1478
|
const block = source ? extractPytestParamBlock(source, tpId) : undefined;
|
|
1225
1479
|
const parameterNames = source ? extractPytestParamParameterNames(source, tpId) : [];
|
|
1226
|
-
const
|
|
1480
|
+
const functionFieldOverride = source && inferred.field
|
|
1481
|
+
? observeDeterministicFunctionFieldOverride({
|
|
1482
|
+
source,
|
|
1483
|
+
tpId,
|
|
1484
|
+
block,
|
|
1485
|
+
parameterNames,
|
|
1486
|
+
field: inferred.field,
|
|
1487
|
+
constants,
|
|
1488
|
+
})
|
|
1489
|
+
: undefined;
|
|
1490
|
+
const helperFieldObserved = source && inferred.field
|
|
1491
|
+
? observeSameModulePayloadHelperField({
|
|
1492
|
+
source,
|
|
1493
|
+
block,
|
|
1494
|
+
parameterNames,
|
|
1495
|
+
field: inferred.field,
|
|
1496
|
+
constants,
|
|
1497
|
+
})
|
|
1498
|
+
: undefined;
|
|
1499
|
+
const observed = functionFieldOverride ?? helperFieldObserved ?? observeParamFeatures(block, inferred.field, constants, parameterNames);
|
|
1227
1500
|
// Request-level / health checks often have no pytest.param payload row,
|
|
1228
1501
|
// or only a TP-id label positional (no field value).
|
|
1229
1502
|
const requestLevelNoParam = Boolean(source) &&
|
|
@@ -1242,9 +1515,17 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
1242
1515
|
hasCreateDeleteDerivedJourney(source, tpId)) ||
|
|
1243
1516
|
(/^custom-literal:(?:create-derived(?:-active-id|-id)?|active-existing-derived)$/i.test(inferred.intent) &&
|
|
1244
1517
|
hasCreateDerivedJourney(source, tpId)));
|
|
1518
|
+
const conditionalOmissionMatch = Boolean(source) &&
|
|
1519
|
+
inferred.intent === "missing" &&
|
|
1520
|
+
observed.kind === "none" &&
|
|
1521
|
+
hasConditionalFieldOmission(source, tpId, inferred.field, parameterNames);
|
|
1522
|
+
const localPayloadHelperMatch = Boolean(source) &&
|
|
1523
|
+
inferred.intent === "nominal" &&
|
|
1524
|
+
!inferred.field &&
|
|
1525
|
+
hasSafeSameModulePayloadHelper(source, block, parameterNames);
|
|
1245
1526
|
const status = !source
|
|
1246
1527
|
? "UNDETERMINED"
|
|
1247
|
-
: derivedJourneyMatch
|
|
1528
|
+
: derivedJourneyMatch || conditionalOmissionMatch || localPayloadHelperMatch
|
|
1248
1529
|
? "MATCH"
|
|
1249
1530
|
: requestLevelNoParam
|
|
1250
1531
|
? "MATCH"
|
|
@@ -1252,14 +1533,19 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
1252
1533
|
? "UNDETERMINED"
|
|
1253
1534
|
: compareIntent(inferred.intent, observed, inferred.bound, inferred.example, inferred.field);
|
|
1254
1535
|
const repairability = repairabilityFor(status, inferred.intent, observed, inferred.example);
|
|
1536
|
+
const diagnostics = scenarioParamDiagnosticValues(inferred.intent, observed);
|
|
1255
1537
|
entries.push({
|
|
1256
1538
|
caseId: testCase.caseId,
|
|
1257
1539
|
tpId,
|
|
1258
1540
|
field: inferred.field,
|
|
1259
1541
|
intent: inferred.intent,
|
|
1260
|
-
observed: observed.text,
|
|
1542
|
+
observed: boundedScenarioText(observed.text, observed.length),
|
|
1543
|
+
observedLength: observed.length,
|
|
1261
1544
|
status,
|
|
1262
1545
|
repairability,
|
|
1546
|
+
reasonCode: status === "MATCH" ? "SCENARIO_PARAM_MATCH" : status === "MISMATCH" ? "SCENARIO_PARAM_MISMATCH" : "SCENARIO_PARAM_UNDETERMINED",
|
|
1547
|
+
...diagnostics,
|
|
1548
|
+
sourceLocation: source ? scenarioParamSourceLocation(testCase.scriptPath, source, tpId) : undefined,
|
|
1263
1549
|
suggestedFix: fallbackSourced
|
|
1264
1550
|
? undefined
|
|
1265
1551
|
: suggestedFixFor(inferred.intent, inferred.field, inferred.bound, inferred.example),
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { BACKEND_TEST_MAX_MODULE_COUNT, isOpaqueHashBackendTestModuleStem, isPriorityOnlyBackendTestModuleStem, } from "./backend-test-module-stem.js";
|
|
6
6
|
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
7
|
+
import { inspectBackendTestMarkdownTestPointBindings } from "./backend-test-case-coverage-analysis.js";
|
|
7
8
|
/**
|
|
8
9
|
* Maps a backend-test generation writer task id to its writer-progress role.
|
|
9
10
|
* Returns undefined for nodes that are not completeness-gated generators.
|
|
@@ -276,6 +277,16 @@ function moduleStructurallyComplete(markdown) {
|
|
|
276
277
|
reasons: [`missing required sections: [${missing.join(", ")}]`],
|
|
277
278
|
};
|
|
278
279
|
}
|
|
280
|
+
const bindingReasons = inspectBackendTestMarkdownTestPointBindings(markdown).flatMap((entry) => [
|
|
281
|
+
...(entry.unclassifiedTestPoints.length > 0
|
|
282
|
+
? [`${entry.caseId} has unclassified Test Points: ${entry.unclassifiedTestPoints.join(", ")}`]
|
|
283
|
+
: []),
|
|
284
|
+
...(entry.duplicateBindingTestPoints.length > 0
|
|
285
|
+
? [`${entry.caseId} has duplicate Test Point bindings: ${entry.duplicateBindingTestPoints.join(", ")}`]
|
|
286
|
+
: []),
|
|
287
|
+
]);
|
|
288
|
+
if (bindingReasons.length > 0)
|
|
289
|
+
return { ok: false, reasons: bindingReasons };
|
|
279
290
|
return { ok: true, reasons: [] };
|
|
280
291
|
}
|
|
281
292
|
function moduleStructuralDetail(result) {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Leaf module for the retry-policy schema surface: failure-category constants,
|
|
4
|
+
* the zod retry-policy schema, and the target-template transient retry profile
|
|
5
|
+
* literal.
|
|
6
|
+
*
|
|
7
|
+
* `types.ts` reads `dagRetryPolicySchema` / `TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE`
|
|
8
|
+
* during its own module initialization, and the retry-policy module transitively
|
|
9
|
+
* reaches `types.ts` (retry-policy → frontend-recovery-capsule →
|
|
10
|
+
* frontend-recovery-lineage → frontend-implementation-contract → types). With
|
|
11
|
+
* these definitions living in retry-policy itself, a run whose entry module is
|
|
12
|
+
* retry-policy (e.g. test/dag-node-retry.test.ts) evaluates `types.ts` while
|
|
13
|
+
* retry-policy is still initializing and reads an undefined
|
|
14
|
+
* `dagRetryPolicySchema`. Keeping this module a leaf (zod is its only import)
|
|
15
|
+
* makes the cycle resolve order-independently; retry-policy re-exports
|
|
16
|
+
* everything for source compatibility.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Failure categories that are safe to auto-retry for read-only Pi nodes.
|
|
20
|
+
*
|
|
21
|
+
* These are failures with no repository write side-effects: model connection
|
|
22
|
+
* interruption, provider rate-limiting, temporary provider unavailability,
|
|
23
|
+
* request timeout, or a settled read-only turn with no assistant text.
|
|
24
|
+
*
|
|
25
|
+
* `quota` is intentionally NOT included: a quota exhaustion is an account
|
|
26
|
+
* billing/plan state, not a transient rate limit, and retrying immediately
|
|
27
|
+
* will not help and may waste budget.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_DAG_RETRY_CATEGORIES = [
|
|
30
|
+
"timeout",
|
|
31
|
+
"network",
|
|
32
|
+
"rate-limit",
|
|
33
|
+
"unavailable",
|
|
34
|
+
"empty-output",
|
|
35
|
+
];
|
|
36
|
+
export const STRUCTURED_OUTPUT_RETRY_CATEGORY = "output-too-large";
|
|
37
|
+
export const PROTOCOL_INVALID_RETRY_CATEGORY = "protocol-invalid";
|
|
38
|
+
/** Recoverable model artifact/schema formatting failure on read-only structured nodes. */
|
|
39
|
+
export const STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY = "invalid-output";
|
|
40
|
+
/** Provider stopReason=length truncated the response before the JSON contract completed. */
|
|
41
|
+
export const STRUCTURED_OUTPUT_TRUNCATED_RETRY_CATEGORY = "structured-output-truncated";
|
|
42
|
+
/**
|
|
43
|
+
* The review node emitted a verdict in response text (e.g. "VERDICT: pass")
|
|
44
|
+
* but never committed the authoritative typed terminal tool call
|
|
45
|
+
* (approve_review / request_review_changes). Read-only and safe to retry with
|
|
46
|
+
* a corrected instruction; the typed terminal is the only authority.
|
|
47
|
+
*/
|
|
48
|
+
export const REVIEW_TERMINAL_MISSING_RETRY_CATEGORY = "review-terminal-missing";
|
|
49
|
+
/**
|
|
50
|
+
* The plan node issued too many read-only tool calls (read/grep/ls/find),
|
|
51
|
+
* blowing up the context window and eventually a 400 request-too-large. The
|
|
52
|
+
* plan should trust upstream typed facts instead of re-reading contract/scout
|
|
53
|
+
* outputs and source files repeatedly. Read-only and safe to retry with a
|
|
54
|
+
* reduced-reading instruction.
|
|
55
|
+
*/
|
|
56
|
+
export const READ_BURST_RETRY_CATEGORY = "read-burst";
|
|
57
|
+
/**
|
|
58
|
+
* Provider returned a context-overflow error (400 request-too-large / context
|
|
59
|
+
* window exceeded). The SDK compacts and auto-retries once internally; when it
|
|
60
|
+
* still surfaces, the executor classifies this so retry policies and reports
|
|
61
|
+
* treat it as overflow (not empty-output) and can retry with a compacted
|
|
62
|
+
* session / fresh context.
|
|
63
|
+
*/
|
|
64
|
+
export const CONTEXT_OVERFLOW_RETRY_CATEGORY = "context-overflow";
|
|
65
|
+
/** Retry only a proven no-op from an explicitly opt-in bounded Pi writer. */
|
|
66
|
+
export const WRITER_EMPTY_DIFF_RETRY_CATEGORY = "writer-empty-diff";
|
|
67
|
+
/** Retry when a backend-test writer finished but Completeness Gate found missing/broken targets. */
|
|
68
|
+
export const INCOMPLETE_WRITE_SET_RETRY_CATEGORY = "incomplete-write-set";
|
|
69
|
+
/**
|
|
70
|
+
* Provider/transport stall or absolute timeout on a Pi exclusive writer that
|
|
71
|
+
* left zero attributed workspace writes and zero write-tool calls. Safe to
|
|
72
|
+
* retry once because the attempt was a pure provider flake (no side effects).
|
|
73
|
+
* Partial writes keep the original `timeout` category and do NOT retry.
|
|
74
|
+
*/
|
|
75
|
+
export const WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY = "writer-clean-timeout";
|
|
76
|
+
export const STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES = [
|
|
77
|
+
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
78
|
+
STRUCTURED_OUTPUT_RETRY_CATEGORY,
|
|
79
|
+
STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
|
|
80
|
+
STRUCTURED_OUTPUT_TRUNCATED_RETRY_CATEGORY,
|
|
81
|
+
];
|
|
82
|
+
/** Categories allowed on nodes that declare a machine-readable outputProtocol. */
|
|
83
|
+
export const PROTOCOL_AWARE_DAG_RETRY_CATEGORIES = [
|
|
84
|
+
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
85
|
+
PROTOCOL_INVALID_RETRY_CATEGORY,
|
|
86
|
+
STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
|
|
87
|
+
];
|
|
88
|
+
export const ALL_DAG_RETRY_CATEGORIES = [
|
|
89
|
+
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
90
|
+
STRUCTURED_OUTPUT_RETRY_CATEGORY,
|
|
91
|
+
PROTOCOL_INVALID_RETRY_CATEGORY,
|
|
92
|
+
STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
|
|
93
|
+
STRUCTURED_OUTPUT_TRUNCATED_RETRY_CATEGORY,
|
|
94
|
+
WRITER_EMPTY_DIFF_RETRY_CATEGORY,
|
|
95
|
+
INCOMPLETE_WRITE_SET_RETRY_CATEGORY,
|
|
96
|
+
WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
|
|
97
|
+
REVIEW_TERMINAL_MISSING_RETRY_CATEGORY,
|
|
98
|
+
READ_BURST_RETRY_CATEGORY,
|
|
99
|
+
CONTEXT_OVERFLOW_RETRY_CATEGORY,
|
|
100
|
+
];
|
|
101
|
+
export const dagRetryCategorySchema = z.enum(ALL_DAG_RETRY_CATEGORIES);
|
|
102
|
+
export const dagRetryBackoffSchema = z.enum(["exponential"]);
|
|
103
|
+
/**
|
|
104
|
+
* Opt-in retry policy for a DagTask. Generated for safe read-only Pi nodes,
|
|
105
|
+
* except the single validation-gated writer-empty-diff policy. Validation
|
|
106
|
+
* rejects all other writers, dynamic, shell, static, and decision-gate nodes.
|
|
107
|
+
*/
|
|
108
|
+
export const dagRetryPolicySchema = z
|
|
109
|
+
.object({
|
|
110
|
+
/**
|
|
111
|
+
* Total number of attempts (including the first try). Must be 1..5.
|
|
112
|
+
* A value of 1 disables retry (one attempt, no retry).
|
|
113
|
+
*/
|
|
114
|
+
maxAttempts: z.number().int().positive().max(5),
|
|
115
|
+
backoff: dagRetryBackoffSchema.default("exponential"),
|
|
116
|
+
/** Initial delay before the second attempt, in milliseconds. */
|
|
117
|
+
initialDelayMs: z.number().int().nonnegative().default(2000),
|
|
118
|
+
/** Upper bound on a single backoff delay, in milliseconds. */
|
|
119
|
+
maxDelayMs: z.number().int().nonnegative().default(30000),
|
|
120
|
+
/**
|
|
121
|
+
* Explicit set of failure categories eligible for retry. Defaults to
|
|
122
|
+
* {@link DEFAULT_DAG_RETRY_CATEGORIES}. `quota` and other categories
|
|
123
|
+
* are never retried.
|
|
124
|
+
*/
|
|
125
|
+
retryCategories: z
|
|
126
|
+
.array(dagRetryCategorySchema)
|
|
127
|
+
.default([...DEFAULT_DAG_RETRY_CATEGORIES]),
|
|
128
|
+
})
|
|
129
|
+
.superRefine((policy, ctx) => {
|
|
130
|
+
if (policy.maxDelayMs < policy.initialDelayMs) {
|
|
131
|
+
ctx.addIssue({
|
|
132
|
+
code: z.ZodIssueCode.custom,
|
|
133
|
+
message: "retryPolicy.maxDelayMs must be >= retryPolicy.initialDelayMs",
|
|
134
|
+
path: ["maxDelayMs"],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
export const TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE = "standard-reviewed-supervised-v1";
|