@tea-agent/loop-agent 0.42.0-next.7 → 0.42.0-next.9
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/run-dag.js +40 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +313 -56
- package/dist/executors/shell-executor.js +50 -20
- package/dist/worker/console/chat/pi-runtime.js +20 -4
- package/dist/worker/console/chat/routes.js +44 -22
- package/dist/worker/console/chat/session-store.js +16 -0
- package/dist/worker/console/server.js +19 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BQBHX31n.js → abnfDiagram-N423BO3Z-C6n9_m0P.js} +1 -1
- package/dist/worker/console/static/assets/{arc-MSJ199eR.js → arc-DQh-IfZ1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DiRq2qMX.js → architectureDiagram-T3A2C74G-54NnrwUC.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-rlaxFOvi.js → blockDiagram-VBNYF7ZC-pivRALGK.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BSpi9ump.js → c4Diagram-5PPSVZJV-BR7OV2NJ.js} +1 -1
- package/dist/worker/console/static/assets/channel-DsZxrgqe.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BN8VNeh5.js → chunk-2GRJ4B5K-B1Aq6BcK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDa1OVRA.js → chunk-2Q5K7J3B-CbdU0rjo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-zxMQ5b8E.js → chunk-5RXB4S5H-Dqi2GJeD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XTqNlzH4.js → chunk-5VM5RSS4-BYn1Hu4R.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DpZQfFTq.js → chunk-6Q2QTUOP-DRLjDL0k.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DPv743zu.js → chunk-GF5L2VYU-CY9Xx2jV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CFsCFoF4.js → chunk-JWPE2WC7-BNCZs7_z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CV__aJTt.js → chunk-KBJHAD2P-DZ8AStLL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BshKlNir.js → chunk-RYQCIY6F-DsxjYrzz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-B-A3ErBP.js → chunk-XXDRQBXY-Ddx1KC1l.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-25xj8928.js → cose-bilkent-JH36ORCC-W9TveCnK.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DFmv38cl.js → cynefin-VYW2F7L2-l9j_ztZH.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-hReuWDQP.js → cynefinDiagram-MW4NZA55-BMJMKi4G.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C9X3YPUI.js → dagre-VZM6K2ZE-Bb4mG9pH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CAVmqjg9.js → diagram-7IWD3JNH-BMgL_Qv0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYNlLsIm.js → diagram-B4RE2ZJO-Bvo2T4OQ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BP5mlbfb.js → diagram-LBJQPF4R-_5kWRN9c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KWOHK5ZM.js → diagram-Q27KOJAE-DqdMrltM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-D25Ro5y-.js → diagram-UB23O5K3-CDDYsNkp.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kLaahtbh.js → ebnfDiagram-BXEA7PRR-c4nfnZUV.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-1tgKuLez.js → erDiagram-JOGREHBK-DnkUcNMs.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DARr-Cvn.js → flowDiagram-UKHOOZJN-Dz0KGLZE.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BgwjBv_V.js → ganttDiagram-PKOTCBZU-Cj1t1uka.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DneIlYSI.js → gitGraphDiagram-DS77QQ5N-tp2FrBHd.js} +1 -1
- package/dist/worker/console/static/assets/{index-Dkv0ezuE.js → index-D9Sc0f0y.js} +84 -84
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DA7ATqY6.js → infoDiagram-6WML65LV-DQwJS8DH.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DnOS-E1Z.js → ishikawaDiagram-WSZJBQD7-eimCQWD4.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C8JnwJ_s.js → journeyDiagram-NVQOT4AX-Dd9Gbwgv.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Cw0_OjEC.js → kanban-definition-27J2QSJJ-Qve3cyft.js} +1 -1
- package/dist/worker/console/static/assets/{linear-mrxq3fyn.js → linear-UXKSe36Z.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Bagus6zF.js → mermaid.core-CWhj4JXN.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-Bf95bLuW.js → mindmap-definition-FAOFIHXS-yTcwf4SJ.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BvtUW6w2.js → pegDiagram-VL7TDLO6-_7qToaZP.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CbWtT-Ry.js → pieDiagram-7S7Q4E2Y-CXmrKmDm.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-ctnxxIdD.js → quadrantDiagram-CIZ2JOQS-hMmrJyaS.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BZ7Z3X0-.js → railroadDiagram-AXF67PYL-BXw8tCBe.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BMX1xnIy.js → requirementDiagram-LRYGKXZP-BmjgnLZl.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-MShOQznz.js → sankeyDiagram-W5VNT64P-Bj77SPpN.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CTGOazaB.js → sequenceDiagram-SI44F4Z6-B2FDVysL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-zGbfM4nV.js → sizeCapture-X5ZJPWSS-UChNY9ZZ.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Brf8Wwzr.js → stateDiagram-OKZ733FA-wbAEkbd7.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CvNw3ZqF.js → swimlanes-SLNWSIFB-DCEkU8HR.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-z2LyZpDt.js → timeline-definition-Z64GVDOM-BGp7H06U.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Bc_coprH.js → vennDiagram-T6HMQDX7-DsCIQuWR.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-M1NSmWq-.js → wardleyDiagram-T6FBY63Y-cRZLP4Xe.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Ct0rjGXW.js → xychartDiagram-ELKLHX3M-CP0xLR9Y.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +31 -2
- package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -3
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
- package/dist/worker/observe/routes.js +4 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +58 -9
- package/dist/workflows/dag/backend-test-scenario-param.js +156 -20
- package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
- package/dist/workflows/dag/backend-test-writer-completeness.js +7 -3
- package/dist/workflows/dag/frontend-recovery-plan.js +2 -1
- package/dist/workflows/dag/frontend-recovery-run.js +33 -5
- package/dist/workflows/dag/frontend-writer-admission.js +44 -10
- package/dist/workflows/dag/init-hybrid.js +26 -5
- package/dist/workflows/dag/node-execution.js +76 -0
- package/dist/workflows/dag/rerun-feedback.js +59 -0
- package/dist/workflows/dag/rerun-task.js +8 -2
- package/dist/workflows/dag/runner.js +18 -11
- package/dist/workflows/dag/scheduler.js +21 -6
- package/docs/templates/backend-test-dag.json +1 -1
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-BdTaMnYP.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cid7CSRK.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cid7CSRK.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CIkoculx.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-pJ-V5EdT.js +0 -8
|
@@ -307,14 +307,18 @@ export function classifyBackendTestCorrespondenceRepair(facts, layout) {
|
|
|
307
307
|
const markdownReasons = [];
|
|
308
308
|
const pytestReasons = [];
|
|
309
309
|
const counts = bindingCounts(entry);
|
|
310
|
-
if (entry.
|
|
311
|
-
|
|
310
|
+
if (entry.cardinality !== "0:1" &&
|
|
311
|
+
entry.testPoints.some((point) => (counts.get(point) ?? 0) !== 1)) {
|
|
312
|
+
// Only ### 测试点 inventory must be bound exactly once. Extra
|
|
313
|
+
// automation-mapping TPs (typical: 横切 listed only under 跨切证据)
|
|
314
|
+
// are pytest-owned docstring/repair defects, not Markdown BLOCKED.
|
|
312
315
|
markdownReasons.push("markdown-test-point-binding-invalid");
|
|
313
316
|
}
|
|
314
317
|
if (entry.declaredScript && entry.declaredScript !== entry.expectedScript) {
|
|
315
318
|
markdownReasons.push("markdown-script-declaration-invalid");
|
|
316
319
|
}
|
|
317
|
-
|
|
320
|
+
const evidenceOnly = isEvidenceOnlyCorrespondenceEntry(entry);
|
|
321
|
+
if (!evidenceOnly && entry.cardinality === "1:0")
|
|
318
322
|
pytestReasons.push("missing-pytest-symbol");
|
|
319
323
|
if (entry.cardinality === "1:N")
|
|
320
324
|
pytestReasons.push("multiple-pytest-symbols");
|
|
@@ -325,12 +329,15 @@ export function classifyBackendTestCorrespondenceRepair(facts, layout) {
|
|
|
325
329
|
if (entry.declaredPrimarySymbol && entry.pytestSymbols.some((symbol) => symbol !== entry.declaredPrimarySymbol)) {
|
|
326
330
|
pytestReasons.push("pytest-primary-symbol-mismatch");
|
|
327
331
|
}
|
|
328
|
-
if (entry.variantTestPoints.some((point) => !entry.parameterIds.includes(point)))
|
|
332
|
+
if (!evidenceOnly && entry.variantTestPoints.some((point) => !entry.parameterIds.includes(point))) {
|
|
329
333
|
pytestReasons.push("variant-parameter-missing");
|
|
330
|
-
|
|
334
|
+
}
|
|
335
|
+
if (!evidenceOnly && entry.assertionTestPoints.some((point) => !entry.mappedTestPoints.includes(point))) {
|
|
331
336
|
pytestReasons.push("assertion-binding-missing");
|
|
332
|
-
|
|
337
|
+
}
|
|
338
|
+
if (!evidenceOnly && entry.crossCuttingTestPoints.some((point) => !entry.mappedTestPoints.includes(point))) {
|
|
333
339
|
pytestReasons.push("cross-cutting-binding-missing");
|
|
340
|
+
}
|
|
334
341
|
if (entry.parameterIds.some((point) => !entry.testPoints.includes(point)))
|
|
335
342
|
pytestReasons.push("pytest-test-point-binding-extra");
|
|
336
343
|
if (entry.payloadAssessment.status === "UNSAFE")
|
|
@@ -339,7 +346,7 @@ export function classifyBackendTestCorrespondenceRepair(facts, layout) {
|
|
|
339
346
|
!["TEST_POINT_BINDING_DUPLICATE", "PAYLOAD_CONTRACT_UNAVAILABLE"].includes(entry.status)) {
|
|
340
347
|
pytestReasons.push(`correspondence-${entry.status.toLowerCase().replaceAll("_", "-")}`);
|
|
341
348
|
}
|
|
342
|
-
if (entry.status === "PAYLOAD_CONTRACT_UNAVAILABLE") {
|
|
349
|
+
if (entry.status === "PAYLOAD_CONTRACT_UNAVAILABLE" && entry.cardinality !== "0:1") {
|
|
343
350
|
markdownReasons.push("markdown-payload-contract-unavailable");
|
|
344
351
|
}
|
|
345
352
|
const paths = orderedUnique([entry.expectedScript, ...entry.actualScripts].filter(safePytestPath));
|
|
@@ -796,6 +803,16 @@ function isEvidenceOnlyMarkdownCase(testCase) {
|
|
|
796
803
|
!hasVariant &&
|
|
797
804
|
hasNonVariant);
|
|
798
805
|
}
|
|
806
|
+
function isEvidenceOnlyCorrespondenceEntry(entry) {
|
|
807
|
+
return (entry.status === "EXACT_1_TO_1" &&
|
|
808
|
+
entry.cardinality === "1:0" &&
|
|
809
|
+
!entry.declaredScript &&
|
|
810
|
+
!entry.declaredPrimarySymbol &&
|
|
811
|
+
entry.variantTestPoints.length === 0 &&
|
|
812
|
+
(entry.assertionTestPoints.length > 0 || entry.crossCuttingTestPoints.length > 0) &&
|
|
813
|
+
entry.pytestSymbols.length === 0 &&
|
|
814
|
+
entry.actualScripts.length === 0);
|
|
815
|
+
}
|
|
799
816
|
function payloadContract(body) {
|
|
800
817
|
const contractValue = automationLabelBody(body, ["Payload Contract", "载荷合同"]);
|
|
801
818
|
const requiredValue = automationLabelBody(body, ["Payload Required Paths", "载荷必需路径"]);
|
|
@@ -804,7 +821,7 @@ function payloadContract(body) {
|
|
|
804
821
|
const tokens = (value) => orderedUnique(value
|
|
805
822
|
.split(/[,;,;]/)
|
|
806
823
|
.map((item) => item.trim())
|
|
807
|
-
.filter((item) => !/^(?:none|无|not-applicable|n\/a)$/i.test(item))
|
|
824
|
+
.filter((item) => !/^(?:none|无|not-applicable|n\/a|true|false)$/i.test(item))
|
|
808
825
|
.filter((item) => /^[A-Za-z_][A-Za-z0-9_.]*$/.test(item)));
|
|
809
826
|
const enumValues = {};
|
|
810
827
|
for (const assignment of enumValue.split(/[,;,;]/).map((item) => item.trim()).filter(Boolean)) {
|
|
@@ -1465,6 +1482,13 @@ for top in ast.walk(tree):
|
|
|
1465
1482
|
for keyword in node.keywords:
|
|
1466
1483
|
if keyword.arg in {'payload','body','request_body','request_json','dto','json','data','json_body','params','query','query_params'} and isinstance(keyword.value,ast.Name) and keyword.value.id in positional:
|
|
1467
1484
|
transport.append((keyword.value.id,positional.index(keyword.value.id)-call_offset))
|
|
1485
|
+
if keyword.arg is None and isinstance(keyword.value,ast.Name):
|
|
1486
|
+
alias=keyword.value.id
|
|
1487
|
+
assigned_value=assigned.get(alias)
|
|
1488
|
+
if isinstance(assigned_value,ast.Name) and assigned_value.id in positional:
|
|
1489
|
+
transport.append((assigned_value.id,positional.index(assigned_value.id)-call_offset))
|
|
1490
|
+
elif isinstance(assigned_value,ast.Call) and isinstance(assigned_value.func,ast.Name) and assigned_value.func.id == 'dict' and assigned_value.args and isinstance(assigned_value.args[0],ast.Name) and assigned_value.args[0].id in positional:
|
|
1491
|
+
transport.append((assigned_value.args[0].id,positional.index(assigned_value.args[0].id)-call_offset))
|
|
1468
1492
|
if isinstance(node.func,ast.Attribute) and node.func.attr.lower() in {'post','put','patch'} and len(node.args) >= 2:
|
|
1469
1493
|
candidate=node.args[1]
|
|
1470
1494
|
if isinstance(candidate,ast.Name) and candidate.id in positional:
|
|
@@ -1476,6 +1500,17 @@ for top in ast.walk(tree):
|
|
|
1476
1500
|
matched=node.args[nested_position] if nested_position >= 0 and nested_position < len(node.args) else None
|
|
1477
1501
|
if isinstance(matched,ast.Name) and matched.id in positional:
|
|
1478
1502
|
transport.append((matched.id,positional.index(matched.id)-call_offset))
|
|
1503
|
+
# Assignment order in ast.walk is not source order. Re-scan **kwargs
|
|
1504
|
+
# aliases once the complete local assignment table is available.
|
|
1505
|
+
for node in ast.walk(top):
|
|
1506
|
+
if not isinstance(node,ast.Call): continue
|
|
1507
|
+
for keyword in node.keywords:
|
|
1508
|
+
if keyword.arg is not None or not isinstance(keyword.value,ast.Name): continue
|
|
1509
|
+
assigned_value=assigned.get(keyword.value.id)
|
|
1510
|
+
if isinstance(assigned_value,ast.Name) and assigned_value.id in positional:
|
|
1511
|
+
transport.append((assigned_value.id,positional.index(assigned_value.id)-call_offset))
|
|
1512
|
+
elif isinstance(assigned_value,ast.Call) and isinstance(assigned_value.func,ast.Name) and assigned_value.func.id == 'dict' and assigned_value.args and isinstance(assigned_value.args[0],ast.Name) and assigned_value.args[0].id in positional:
|
|
1513
|
+
transport.append((assigned_value.args[0].id,positional.index(assigned_value.args[0].id)-call_offset))
|
|
1479
1514
|
for returned_name in returned_names:
|
|
1480
1515
|
entries=mutated.get(returned_name,[])
|
|
1481
1516
|
if entries:
|
|
@@ -1705,6 +1740,20 @@ def is_direct_transport_call(node):
|
|
|
1705
1740
|
if isinstance(current,ast.Name): receiver_tokens.append(current.id.lower())
|
|
1706
1741
|
return any(token in {'requests','httpx','client','session','api_client','http_client'} or token.endswith('_client') for token in receiver_tokens)
|
|
1707
1742
|
|
|
1743
|
+
HTTP_METHODS={'GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'}
|
|
1744
|
+
HTTP_FIXTURE_NAMES={'http','http_client','client','api_client','session'}
|
|
1745
|
+
|
|
1746
|
+
def is_fixture_http_call(node):
|
|
1747
|
+
# R12: http("POST", path, json_body={"title": title}) is a Name-call fixture, not client.post.
|
|
1748
|
+
# R15: http_client(operation, path, json_body={...}) still transports when operation is a parametrized method.
|
|
1749
|
+
if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Name) or not node.args: return False
|
|
1750
|
+
first=node.args[0]
|
|
1751
|
+
if isinstance(first,ast.Constant) and isinstance(first.value,str) and first.value.upper() in HTTP_METHODS:
|
|
1752
|
+
return True
|
|
1753
|
+
func=node.func.id.lower()
|
|
1754
|
+
httpish=func in HTTP_FIXTURE_NAMES or func.endswith('_client')
|
|
1755
|
+
return httpish and isinstance(first,ast.Name)
|
|
1756
|
+
|
|
1708
1757
|
def payload_nodes(fn):
|
|
1709
1758
|
result=[]
|
|
1710
1759
|
assigned={}
|
|
@@ -1727,7 +1776,7 @@ def payload_nodes(fn):
|
|
|
1727
1776
|
# Observe local payload/body variables only when they flow into a direct
|
|
1728
1777
|
# HTTP transport call. Arbitrary setup variables are not target requests.
|
|
1729
1778
|
for node in ast.walk(fn):
|
|
1730
|
-
if not is_direct_transport_call(node): continue
|
|
1779
|
+
if not (is_direct_transport_call(node) or is_fixture_http_call(node)): continue
|
|
1731
1780
|
for keyword in node.keywords:
|
|
1732
1781
|
if keyword.arg in PAYLOAD_NAMES: result.extend(payload_expr_nodes(keyword.value,assigned,parameters))
|
|
1733
1782
|
return result
|
|
@@ -59,6 +59,7 @@ const CLOSED_SET = new Set([
|
|
|
59
59
|
"null",
|
|
60
60
|
"min-1",
|
|
61
61
|
"min",
|
|
62
|
+
"max-1",
|
|
62
63
|
"max",
|
|
63
64
|
"max+1",
|
|
64
65
|
"pattern-invalid",
|
|
@@ -155,14 +156,19 @@ function parseScenarioBoundToken(raw) {
|
|
|
155
156
|
function parseScenarioIntentLine(line) {
|
|
156
157
|
// Target may contain commas (body.title,body.status). Intent may contain `|` / spaces.
|
|
157
158
|
// Stop field values only at known trailing keys or end-of-line.
|
|
158
|
-
const
|
|
159
|
+
const unfenced = line
|
|
160
|
+
.trim()
|
|
161
|
+
.replace(/^[-*+]\s+/, "")
|
|
162
|
+
.replace(/^`(.+)`$/, "$1")
|
|
163
|
+
.trim();
|
|
164
|
+
const precise = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;]\s*operation\s*=\s*([^;\n]+?)\s*[|;]\s*target\s*=\s*([^;\n]+?)\s*[|;]\s*intent\s*=\s*([^;\n]+?)(?:\s*[|;]\s*bound\s*=\s*([^\s;`|\n]+))?(?:\s*[|;]\s*example\s*=\s*([^;\n]*))?(?:\s*[|;]\s*expectedCode\s*=\s*[^;\n]+)?\s*$/i.exec(unfenced);
|
|
159
165
|
if (precise) {
|
|
160
166
|
return {
|
|
161
167
|
tpId: precise[1],
|
|
162
168
|
rawIntent: (precise[4] ?? "unknown").trim(),
|
|
163
169
|
target: precise[3]?.trim(),
|
|
164
170
|
bound: parseScenarioBoundToken(precise[5]),
|
|
165
|
-
example: precise[6]
|
|
171
|
+
example: precise[6] === undefined ? undefined : precise[6].trim(),
|
|
166
172
|
legacy: false,
|
|
167
173
|
};
|
|
168
174
|
}
|
|
@@ -192,6 +198,53 @@ function resolveScenarioIntentField(target, field) {
|
|
|
192
198
|
target.split(/\s*,\s*/)[0];
|
|
193
199
|
return preferred?.split(".").at(-1);
|
|
194
200
|
}
|
|
201
|
+
function normalizeLengthIntentAlias(rawIntent) {
|
|
202
|
+
const lower = rawIntent.trim().toLowerCase();
|
|
203
|
+
if (lower === "max-1" || lower === "maxminus1" || lower === "max_minus_1") {
|
|
204
|
+
return "max-1";
|
|
205
|
+
}
|
|
206
|
+
if (lower === "min-1" || lower === "minminus1" || lower === "min_minus_1") {
|
|
207
|
+
return "min-1";
|
|
208
|
+
}
|
|
209
|
+
return lower;
|
|
210
|
+
}
|
|
211
|
+
function proseLengthBound(haystack, field) {
|
|
212
|
+
const fieldAlt = field ? field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : undefined;
|
|
213
|
+
const scoped = fieldAlt ? new RegExp(`${fieldAlt}[^\\n]{0,160}`, "ig") : undefined;
|
|
214
|
+
const windows = [];
|
|
215
|
+
if (scoped) {
|
|
216
|
+
for (const match of haystack.matchAll(scoped)) {
|
|
217
|
+
windows.push(match[0] ?? "");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
windows.push(haystack);
|
|
221
|
+
let max;
|
|
222
|
+
let min;
|
|
223
|
+
const proseMaxRe = /(\d{1,4})\s*个字符的最大/i;
|
|
224
|
+
for (const window of windows) {
|
|
225
|
+
const proseMax = proseMaxRe.exec(window);
|
|
226
|
+
if (proseMax && max === undefined)
|
|
227
|
+
max = Number(proseMax[1]);
|
|
228
|
+
const nums = [...window.matchAll(/(?<![+\-])\d{1,4}/g)]
|
|
229
|
+
.map((item) => Number(item[0]))
|
|
230
|
+
.filter((item) => Number.isFinite(item));
|
|
231
|
+
for (let i = 0; i + 2 < nums.length; i += 1) {
|
|
232
|
+
const a = nums[i];
|
|
233
|
+
const b = nums[i + 1];
|
|
234
|
+
const c = nums[i + 2];
|
|
235
|
+
// Prefer n-1,n,n+1 around a documented max (100/2000), not 0,1,99 or 99,100,101's first two plus leftover.
|
|
236
|
+
if (b === a + 1 && c === b + 1 && b >= 10) {
|
|
237
|
+
max ??= b;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (nums.includes(0) && nums.includes(1))
|
|
242
|
+
min ??= 1;
|
|
243
|
+
if (max !== undefined && min !== undefined)
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
return { max, min };
|
|
247
|
+
}
|
|
195
248
|
function resolveCustomLiteralIntent(rawIntent, field) {
|
|
196
249
|
const lower = rawIntent.toLowerCase();
|
|
197
250
|
if (!lower.startsWith("custom-literal:")) {
|
|
@@ -250,7 +303,7 @@ export function inferScenarioParamIntent(input) {
|
|
|
250
303
|
const field = resolveScenarioIntentField(machine.target, machine.field);
|
|
251
304
|
const rawIntent = machine.rawIntent.trim();
|
|
252
305
|
const example = machine.example;
|
|
253
|
-
const lower = rawIntent
|
|
306
|
+
const lower = normalizeLengthIntentAlias(rawIntent);
|
|
254
307
|
let normalizedIntent = lower === "nominal-operation" ? "nominal" : lower;
|
|
255
308
|
// Writers sometimes mark a short "typical" length sample as min/max with bound=LEN<nominal>.
|
|
256
309
|
// Treat that as nominal so a short helper/string is not forced to equal maxLength.
|
|
@@ -273,16 +326,16 @@ export function inferScenarioParamIntent(input) {
|
|
|
273
326
|
new RegExp(`(?:minLength|min(?:imum)?|下限|最小长度)\\s*[=::]?\\s*(\\d{1,4})[^\\n]{0,80}?${fieldEsc}`, "i").exec(haystack) ??
|
|
274
327
|
/(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack)
|
|
275
328
|
: /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack);
|
|
329
|
+
const proseBound = proseLengthBound(haystack, field);
|
|
276
330
|
const prefersMinBound = normalizedIntent === "min" ||
|
|
277
331
|
normalizedIntent === "min-1" ||
|
|
278
332
|
/(?:MIN|LEN1|LENGTH-?1|最短|最小)/i.test(tpId);
|
|
279
|
-
const boundFromBody = prefersMinBound
|
|
280
|
-
? minBoundFromBody ?? maxBoundFromBody
|
|
281
|
-
: maxBoundFromBody ?? minBoundFromBody;
|
|
333
|
+
const boundFromBody = prefersMinBound ? minBoundFromBody : maxBoundFromBody ?? minBoundFromBody;
|
|
282
334
|
const boundFromTp = /(?:LEN(?:GTH)?|长度)[_-]?(\d{1,5})(?:[_-]|$)/i.exec(tpId) ??
|
|
283
|
-
/(?:MIN|MAX)[_-]
|
|
335
|
+
/(?:MIN|MAX)[_-](\d{2,5})(?:[_-]|$)/i.exec(tpId);
|
|
284
336
|
const lengthIntent = normalizedIntent === "min" ||
|
|
285
337
|
normalizedIntent === "min-1" ||
|
|
338
|
+
normalizedIntent === "max-1" ||
|
|
286
339
|
normalizedIntent === "max" ||
|
|
287
340
|
normalizedIntent === "max+1";
|
|
288
341
|
// Length intents: explicit machine bound >0, then TP-encoded LEN1/LEN100,
|
|
@@ -294,13 +347,35 @@ export function inferScenarioParamIntent(input) {
|
|
|
294
347
|
: normalizedIntent === "max+1"
|
|
295
348
|
? Math.max(0, boundFromTpNumber - 1)
|
|
296
349
|
: boundFromTpNumber;
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
350
|
+
const boundFromProse = prefersMinBound ? proseBound.min : proseBound.max;
|
|
351
|
+
const offsetIntent = normalizedIntent === "max-1" ||
|
|
352
|
+
normalizedIntent === "max+1" ||
|
|
353
|
+
normalizedIntent === "min-1";
|
|
354
|
+
const contractBound = boundFromBody
|
|
355
|
+
? Number(boundFromBody[1])
|
|
356
|
+
: boundFromProse !== undefined && boundFromProse >= 0
|
|
357
|
+
? boundFromProse
|
|
358
|
+
: undefined;
|
|
359
|
+
const machineBoundPositive = typeof machine.bound === "number" && Number.isFinite(machine.bound) && machine.bound > 0;
|
|
360
|
+
const minLengthFromBody = prefersMinBound && minBoundFromBody ? Number(minBoundFromBody[1]) : undefined;
|
|
361
|
+
const machineBoundZeroMin = prefersMinBound &&
|
|
362
|
+
machine.bound === 0 &&
|
|
363
|
+
minLengthFromBody === undefined &&
|
|
364
|
+
boundFromTpNormalized === undefined;
|
|
365
|
+
const bound = offsetIntent && contractBound !== undefined && contractBound > 0
|
|
366
|
+
? contractBound
|
|
367
|
+
: machineBoundPositive && !offsetIntent
|
|
368
|
+
? machine.bound
|
|
369
|
+
: lengthIntent && boundFromTpNormalized !== undefined
|
|
370
|
+
? boundFromTpNormalized
|
|
371
|
+
: minLengthFromBody !== undefined && minLengthFromBody >= 0
|
|
372
|
+
? minLengthFromBody
|
|
373
|
+
: machineBoundZeroMin
|
|
374
|
+
? 0
|
|
375
|
+
: contractBound !== undefined &&
|
|
376
|
+
(prefersMinBound ? contractBound >= 0 : contractBound > 0)
|
|
377
|
+
? contractBound
|
|
378
|
+
: undefined;
|
|
304
379
|
if (rawIntent.toLowerCase().startsWith("custom-literal:")) {
|
|
305
380
|
const resolved = resolveCustomLiteralIntent(rawIntent, field);
|
|
306
381
|
const rawLiteral = rawIntent.slice(rawIntent.indexOf(":") + 1).trim();
|
|
@@ -387,13 +462,15 @@ export function inferScenarioParamIntent(input) {
|
|
|
387
462
|
const boundMatch = fieldScopedBound ??
|
|
388
463
|
/(?:maxLength|max(?:imum)?|上限|最大长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`) ??
|
|
389
464
|
/(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`);
|
|
390
|
-
const
|
|
465
|
+
const proseBound = proseLengthBound(`${caseBody}
|
|
466
|
+
${dataSection}`, field);
|
|
467
|
+
const bound = boundMatch ? Number(boundMatch[1]) : proseBound.max ?? proseBound.min;
|
|
391
468
|
const exampleMatch = /(?:反例|invalid example|example)\s*[=::]\s*[`"]?([^`"\n]+)[`"]?/i.exec(caseBody);
|
|
392
469
|
const example = exampleMatch?.[1]?.trim();
|
|
393
470
|
if (/EMPTY|空白|空串|空字符串/.test(upper) || /空字符串|空串|empty string/i.test(caseBody)) {
|
|
394
471
|
return { intent: "empty", field, bound, example, intentSource: "tp-fallback" };
|
|
395
472
|
}
|
|
396
|
-
if (/MISSING|缺省|缺失|省略/.test(upper) || /缺少字段|缺失字段|missing field/i.test(caseBody)) {
|
|
473
|
+
if (/MISSING|OMITTED|\bOMIT\b|缺省|缺失|省略/.test(upper) || /缺少字段|缺失字段|missing field/i.test(caseBody)) {
|
|
397
474
|
return { intent: "missing", field, bound, example, intentSource: "tp-fallback" };
|
|
398
475
|
}
|
|
399
476
|
if (/\bNULL\b|空值/.test(upper) || /\bnull\b/i.test(caseBody)) {
|
|
@@ -402,6 +479,9 @@ export function inferScenarioParamIntent(input) {
|
|
|
402
479
|
if (/MAX[-_]?PLUS[-_]?1|MAX\+1|超长|越界|LENGTH[-_]?OVER|OVER[-_]?LENGTH|OVER/.test(upper)) {
|
|
403
480
|
return { intent: "max+1", field, bound, example, intentSource: "tp-fallback" };
|
|
404
481
|
}
|
|
482
|
+
if (/MAX[-_]1(?:$|[-_])|max-1/i.test(upper)) {
|
|
483
|
+
return { intent: "max-1", field, bound, example, intentSource: "tp-fallback" };
|
|
484
|
+
}
|
|
405
485
|
if (/\bMAX\b|最大/.test(upper)) {
|
|
406
486
|
return { intent: "max", field, bound, example, intentSource: "tp-fallback" };
|
|
407
487
|
}
|
|
@@ -425,7 +505,7 @@ export function inferScenarioParamIntent(input) {
|
|
|
425
505
|
}
|
|
426
506
|
return { intent: "unknown", field, bound, example, intentSource: "tp-fallback" };
|
|
427
507
|
}
|
|
428
|
-
const OMIT_SENTINEL_RE = /^(?:_OMIT|OMIT|MISSING|Ellipsis|\.\.\.)\b/;
|
|
508
|
+
const OMIT_SENTINEL_RE = /^(?:_OMIT|_MISSING|OMIT|MISSING|Ellipsis|\.\.\.)\b/;
|
|
429
509
|
const HTTP_EXPECTED_CODE_RE = /^(?:2xx|3xx|4xx|5xx|[1-5]\d{2}|HTTP_[1-5]\d{2})$/i;
|
|
430
510
|
/** Semantic custom-literal tokens that describe a value class, not a raw string. */
|
|
431
511
|
function isWhitespaceSemanticLiteral(token) {
|
|
@@ -774,6 +854,10 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
|
|
|
774
854
|
(/["'][A-Za-z_][A-Za-z0-9_]*["']\s*:/.test(dictText) ||
|
|
775
855
|
/(?:payload|body|data|json)\s*=/.test(block));
|
|
776
856
|
if (dictLooksLikePayload && !field) {
|
|
857
|
+
const emptyField = /["']([A-Za-z_][A-Za-z0-9_.]*)["']\s*:\s*(?:""|'')/.exec(dictText);
|
|
858
|
+
if (emptyField?.[1]) {
|
|
859
|
+
return { kind: "empty-string", text: '""', length: 0, literal: "" };
|
|
860
|
+
}
|
|
777
861
|
return { kind: "dict", text: dictText.replace(/\s+/g, " ").slice(0, 160) };
|
|
778
862
|
}
|
|
779
863
|
if (dictLooksLikePayload && field) {
|
|
@@ -803,7 +887,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
|
|
|
803
887
|
};
|
|
804
888
|
}
|
|
805
889
|
// Bare omit sentinel before id=...
|
|
806
|
-
if (/pytest\.param\s*\(\s*(?:_OMIT|OMIT|MISSING|\.\.\.|Ellipsis)\s*(?:,|\)|$)/.test(block)) {
|
|
890
|
+
if (/pytest\.param\s*\(\s*(?:_OMIT|_MISSING|OMIT|MISSING|\.\.\.|Ellipsis)\s*(?:,|\)|$)/.test(block)) {
|
|
807
891
|
return {
|
|
808
892
|
kind: "missing-key",
|
|
809
893
|
text: field ? `missing:${field}` : "missing-key",
|
|
@@ -865,7 +949,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
|
|
|
865
949
|
if (paramOpen) {
|
|
866
950
|
const afterOpen = block.slice((paramOpen.index ?? 0) + paramOpen[0].length);
|
|
867
951
|
const positionalTokens = [];
|
|
868
|
-
const tokenRe = /\s*(_OMIT|OMIT|MISSING|Ellipsis|\.\.\.|None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|(?:["'][^"'\\]["']\s*\*\s*(?:\d{1,5}|[A-Za-z_][A-Za-z0-9_]*))|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,|\)|$)/g;
|
|
952
|
+
const tokenRe = /\s*(_OMIT|_MISSING|OMIT|MISSING|Ellipsis|\.\.\.|None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|(?:["'][^"'\\]["']\s*\*\s*(?:\d{1,5}|[A-Za-z_][A-Za-z0-9_]*))|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,|\)|$)/g;
|
|
869
953
|
let match;
|
|
870
954
|
while ((match = tokenRe.exec(afterOpen)) !== null) {
|
|
871
955
|
const between = afterOpen.slice(0, match.index);
|
|
@@ -1008,12 +1092,25 @@ function compareIntent(intent, observed, bound, example, field) {
|
|
|
1008
1092
|
return "MATCH";
|
|
1009
1093
|
return len !== undefined ? "MISMATCH" : "UNDETERMINED";
|
|
1010
1094
|
}
|
|
1095
|
+
case "max-1":
|
|
1096
|
+
if (bound === undefined)
|
|
1097
|
+
return "UNDETERMINED";
|
|
1098
|
+
{
|
|
1099
|
+
const expectedMax1 = Math.max(0, bound - 1);
|
|
1100
|
+
const len = observedLengthValue(observed);
|
|
1101
|
+
if (len === expectedMax1)
|
|
1102
|
+
return "MATCH";
|
|
1103
|
+
return len !== undefined ? "MISMATCH" : "UNDETERMINED";
|
|
1104
|
+
}
|
|
1011
1105
|
case "min":
|
|
1012
1106
|
if (bound === undefined)
|
|
1013
1107
|
return "UNDETERMINED";
|
|
1014
1108
|
{
|
|
1015
1109
|
if (isMinLengthHelper(observed))
|
|
1016
1110
|
return "MATCH";
|
|
1111
|
+
// Optional min=0 may omit the key instead of sending "".
|
|
1112
|
+
if (bound === 0 && observed.kind === "missing-key")
|
|
1113
|
+
return "MATCH";
|
|
1017
1114
|
const len = observedLengthValue(observed);
|
|
1018
1115
|
if (len === bound)
|
|
1019
1116
|
return "MATCH";
|
|
@@ -1167,6 +1264,10 @@ function suggestedFixFor(intent, field, bound, example) {
|
|
|
1167
1264
|
return field && bound !== undefined
|
|
1168
1265
|
? `set ${field}="x"*${bound + 1}`
|
|
1169
1266
|
: undefined;
|
|
1267
|
+
case "max-1":
|
|
1268
|
+
return field && bound !== undefined
|
|
1269
|
+
? `set ${field}="x"*${Math.max(0, bound - 1)}`
|
|
1270
|
+
: undefined;
|
|
1170
1271
|
case "min":
|
|
1171
1272
|
return field && bound !== undefined
|
|
1172
1273
|
? `set ${field}="x"*${bound}`
|
|
@@ -1375,6 +1476,32 @@ function hasSafeSameModulePayloadHelper(source, block, parameterNames) {
|
|
|
1375
1476
|
return false;
|
|
1376
1477
|
return Boolean(safeSameModuleDictHelper(source, call.name));
|
|
1377
1478
|
}
|
|
1479
|
+
function observeBranchPayloadField(input) {
|
|
1480
|
+
const region = scenarioFunctionRegion(input.source, input.tpId);
|
|
1481
|
+
if (!region || !input.block)
|
|
1482
|
+
return undefined;
|
|
1483
|
+
const args = extractPytestParamTopLevelArgs(input.block);
|
|
1484
|
+
const escapedField = input.field.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
|
|
1485
|
+
for (let index = 0; index < input.parameterNames.length; index += 1) {
|
|
1486
|
+
const parameter = input.parameterNames[index];
|
|
1487
|
+
const raw = args[index]?.trim();
|
|
1488
|
+
if (!raw || !/^['\"](?:[^'\"\\\\]|\\\\.)*['\"]$/.test(raw))
|
|
1489
|
+
continue;
|
|
1490
|
+
const branchValue = raw.slice(1, -1).replace(/\\\\([\\\"'])/g, "$1");
|
|
1491
|
+
const escapedParameter = parameter.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
|
|
1492
|
+
const escapedBranchValue = branchValue.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
|
|
1493
|
+
const branch = new RegExp(`(?:^|\\n)([ \\t]*)if\\s+${escapedParameter}\\s*==\\s*['\"]${escapedBranchValue}['\"]\\s*:\\s*([\\s\\S]*?)(?=\\n\\1(?:elif\\b|else\\s*:)|\\n\\1(?:if\\b|def\\b)|$)`, "m").exec(region)?.[2];
|
|
1494
|
+
if (!branch)
|
|
1495
|
+
continue;
|
|
1496
|
+
const value = new RegExp(`['\"]${escapedField}['\"]\\s*:\\s*([^,}\\n]+)`).exec(branch)?.[1];
|
|
1497
|
+
if (!value)
|
|
1498
|
+
continue;
|
|
1499
|
+
const observed = observeLiteralToken(value.trim(), input.field, input.constants);
|
|
1500
|
+
if (observed)
|
|
1501
|
+
return observed;
|
|
1502
|
+
}
|
|
1503
|
+
return undefined;
|
|
1504
|
+
}
|
|
1378
1505
|
function observeDeterministicFunctionFieldOverride(input) {
|
|
1379
1506
|
const region = scenarioFunctionRegion(input.source, input.tpId);
|
|
1380
1507
|
if (!region)
|
|
@@ -1507,7 +1634,10 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
1507
1634
|
constants,
|
|
1508
1635
|
})
|
|
1509
1636
|
: undefined;
|
|
1510
|
-
const
|
|
1637
|
+
const branchFieldObserved = source && inferred.field
|
|
1638
|
+
? observeBranchPayloadField({ source, tpId, block, parameterNames, field: inferred.field, constants })
|
|
1639
|
+
: undefined;
|
|
1640
|
+
const observed = functionFieldOverride ?? helperFieldObserved ?? branchFieldObserved ?? observeParamFeatures(block, inferred.field, constants, parameterNames);
|
|
1511
1641
|
// Request-level / health checks often have no pytest.param payload row,
|
|
1512
1642
|
// or only a TP-id label positional (no field value).
|
|
1513
1643
|
const requestLevelNoParam = Boolean(source) &&
|
|
@@ -1720,6 +1850,12 @@ export function deterministicRewriteScenarioParam(input) {
|
|
|
1720
1850
|
}
|
|
1721
1851
|
valueLiteral = `"x" * ${bound + 1}`;
|
|
1722
1852
|
break;
|
|
1853
|
+
case "max-1":
|
|
1854
|
+
if (bound === undefined) {
|
|
1855
|
+
return { ok: false, source: input.source, detail: "missing bound for max-1" };
|
|
1856
|
+
}
|
|
1857
|
+
valueLiteral = `"x" * ${Math.max(0, bound - 1)}`;
|
|
1858
|
+
break;
|
|
1723
1859
|
case "min-1":
|
|
1724
1860
|
if (bound === undefined) {
|
|
1725
1861
|
return { ok: false, source: input.source, detail: "missing bound for min-1" };
|
|
@@ -43,6 +43,67 @@ function splitCells(raw) {
|
|
|
43
43
|
.map((cell) => cell.trim())
|
|
44
44
|
.filter((cell, index, cells) => !(cell === "" && (index === 0 || index === cells.length - 1)));
|
|
45
45
|
}
|
|
46
|
+
function isExactSlotInventory(extras, partitionId) {
|
|
47
|
+
const tokens = extras
|
|
48
|
+
.join(";")
|
|
49
|
+
.split(/[;,,;]/)
|
|
50
|
+
.map((token) => token.trim().replace(/`/g, ""))
|
|
51
|
+
.filter((token) => token.length > 0);
|
|
52
|
+
if (tokens.length === 0)
|
|
53
|
+
return false;
|
|
54
|
+
const prefix = `TP-${partitionId.trim().toUpperCase()}-`;
|
|
55
|
+
return tokens.every((token) => /^TP-[A-Z0-9-]+$/i.test(token) &&
|
|
56
|
+
(token.toUpperCase().startsWith(prefix) || token.toUpperCase().startsWith("TP-SP-")));
|
|
57
|
+
}
|
|
58
|
+
function trimExactSlotInventoryCells(cells) {
|
|
59
|
+
if (cells.length <= 7)
|
|
60
|
+
return { cells, trimmed: false };
|
|
61
|
+
const partitionId = cells[0] ?? "";
|
|
62
|
+
if (!isExactSlotInventory(cells.slice(7), partitionId)) {
|
|
63
|
+
return { cells, trimmed: false };
|
|
64
|
+
}
|
|
65
|
+
return { cells: cells.slice(0, 7), trimmed: true };
|
|
66
|
+
}
|
|
67
|
+
export function canonicalizeScenarioPartitionTable(markdown) {
|
|
68
|
+
const lines = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
|
|
69
|
+
const headingIndex = lines.findIndex((line) => PARTITION_HEADING.test(line.trim()));
|
|
70
|
+
if (headingIndex === -1) {
|
|
71
|
+
return { markdown: lines.join("\n"), changed: false, trimmedRows: 0 };
|
|
72
|
+
}
|
|
73
|
+
let headerIndex = -1;
|
|
74
|
+
let sectionEnd = lines.length;
|
|
75
|
+
for (let i = headingIndex + 1; i < lines.length; i++) {
|
|
76
|
+
if (/^##\s+/.test(lines[i].trim()) && !PARTITION_HEADING.test(lines[i].trim())) {
|
|
77
|
+
sectionEnd = i;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (PARTITION_TABLE_HEADER.test(lines[i])) {
|
|
81
|
+
headerIndex = i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (headerIndex === -1) {
|
|
85
|
+
return { markdown: lines.join("\n"), changed: false, trimmedRows: 0 };
|
|
86
|
+
}
|
|
87
|
+
let trimmedRows = 0;
|
|
88
|
+
for (let i = headerIndex + 2; i < sectionEnd; i++) {
|
|
89
|
+
const line = lines[i];
|
|
90
|
+
if (!TABLE_ROW.test(line)) {
|
|
91
|
+
if (/^##\s+/.test(line) || /^#\s+/.test(line))
|
|
92
|
+
break;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const { cells, trimmed } = trimExactSlotInventoryCells(splitCells(line));
|
|
96
|
+
if (!trimmed)
|
|
97
|
+
continue;
|
|
98
|
+
lines[i] = `| ${cells.join(" | ")} |`;
|
|
99
|
+
trimmedRows += 1;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
markdown: lines.join("\n"),
|
|
103
|
+
changed: trimmedRows > 0,
|
|
104
|
+
trimmedRows,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
46
107
|
/**
|
|
47
108
|
* Parse the `## Scenario Partitions` section of the README into declared
|
|
48
109
|
* partitions. Returns { partitions, issues }; issues are fail-closed parse
|
|
@@ -77,7 +138,7 @@ export function parseScenarioPartitions(readmeMarkdown) {
|
|
|
77
138
|
break;
|
|
78
139
|
continue;
|
|
79
140
|
}
|
|
80
|
-
const cells = splitCells(line);
|
|
141
|
+
const { cells } = trimExactSlotInventoryCells(splitCells(line));
|
|
81
142
|
if (cells.length !== 7) {
|
|
82
143
|
issues.push(`MISSING_PARTITION: row has ${cells.length} cells, expected 7: ${line.trim()}`);
|
|
83
144
|
continue;
|
|
@@ -369,8 +369,6 @@ function pythonParseable(source) {
|
|
|
369
369
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
370
370
|
if (!normalized.trim())
|
|
371
371
|
return false;
|
|
372
|
-
if (/^\s*(?:def|class|import|from)\b/m.test(normalized) === false)
|
|
373
|
-
return false;
|
|
374
372
|
const structural = pythonStructureOutsideStrings(normalized);
|
|
375
373
|
if (!structural.stringsClosed)
|
|
376
374
|
return false;
|
|
@@ -387,7 +385,13 @@ function pythonParseable(source) {
|
|
|
387
385
|
return false;
|
|
388
386
|
if (openBraces !== closeBraces)
|
|
389
387
|
return false;
|
|
390
|
-
|
|
388
|
+
// Business modules still need a statement. Evidence-only mapped scripts may
|
|
389
|
+
// be a closed module docstring + Case-ID with no `def test_` (R10 health).
|
|
390
|
+
if (/^\s*(?:def|class|import|from|__all__\s*=)/m.test(normalized))
|
|
391
|
+
return true;
|
|
392
|
+
return (/^\s*(?:'''|""")/.test(normalized) &&
|
|
393
|
+
/\bCase-ID\s*:/i.test(normalized) &&
|
|
394
|
+
!/^\s*(?:async\s+)?def\s+test_/m.test(normalized));
|
|
391
395
|
}
|
|
392
396
|
/**
|
|
393
397
|
* Forbidden generated shared helper/factory namespaces for the self-contained
|
|
@@ -26,7 +26,8 @@ export function deriveFrontendRecoveryFailureSource(result) {
|
|
|
26
26
|
* Compute the frontend candidate-continuation reset partition:
|
|
27
27
|
* - `frontend-plan-pi` failure → reset plan + design-review + policy/admission (+ descendants);
|
|
28
28
|
* - `frontend-design-policy-shell` failure → reset policy/admission (+ descendants);
|
|
29
|
-
* -
|
|
29
|
+
* - design-review rejection → reset plan + design review + admission (+ descendants);
|
|
30
|
+
* - other admission failures → reset writer admission (+ descendants);
|
|
30
31
|
*
|
|
31
32
|
* Everything upstream of the failure source is imported as verified parent facts.
|
|
32
33
|
*/
|
|
@@ -275,7 +275,35 @@ async function casUpdateFrontendRecoveryState(parentRunDir, expectedRevision, de
|
|
|
275
275
|
async function materializeChildStaging(input) {
|
|
276
276
|
const { cwd, parentRunDir, parentRunId, parentSpec, parentState, recovery, requestId, childRunId, stagingDir, createdAt, } = input;
|
|
277
277
|
await prepareRunDir(stagingDir);
|
|
278
|
-
|
|
278
|
+
// Design-review rejection must be visible to the reset planner. Recovery
|
|
279
|
+
// children otherwise copy the old spec and only rerun design review, which
|
|
280
|
+
// deterministically re-rejects the unchanged plan. Bind the parent's typed
|
|
281
|
+
// findings into the child spec as ordinary rerun feedback.
|
|
282
|
+
let childSpec = parentSpec;
|
|
283
|
+
if (recovery.failureSource === "frontend-plan-pi") {
|
|
284
|
+
try {
|
|
285
|
+
const { deriveDagRerunFeedback } = await import("./rerun-feedback.js");
|
|
286
|
+
const taskId = parentSpec.taskContractBinding?.taskId ??
|
|
287
|
+
parentSpec.sourceBinding?.taskId ??
|
|
288
|
+
parentRunId;
|
|
289
|
+
const feedback = await deriveDagRerunFeedback({
|
|
290
|
+
parentRunId,
|
|
291
|
+
taskId,
|
|
292
|
+
runDir: parentRunDir,
|
|
293
|
+
spec: parentSpec,
|
|
294
|
+
state: parentState,
|
|
295
|
+
});
|
|
296
|
+
if (feedback)
|
|
297
|
+
childSpec = { ...parentSpec, rerunFeedback: feedback };
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
// A design rejection cannot be safely retried without its typed
|
|
301
|
+
// findings. Fail closed instead of deterministically re-running the
|
|
302
|
+
// unchanged plan and producing another rejection.
|
|
303
|
+
throw new Error(`frontend recovery feedback unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
await writeJsonVerified(stagingDir, "run.json", childSpec);
|
|
279
307
|
// Phase 5: writer transient partial-write recovery resets the writer subtree
|
|
280
308
|
// (frontend-implement-pi) instead of the candidate producer; everything
|
|
281
309
|
// upstream is imported as verified parent facts.
|
|
@@ -300,10 +328,10 @@ async function materializeChildStaging(input) {
|
|
|
300
328
|
result: admissionRead.result,
|
|
301
329
|
});
|
|
302
330
|
}
|
|
303
|
-
const { ranks } = topoSortToRanks(
|
|
331
|
+
const { ranks } = topoSortToRanks(childSpec);
|
|
304
332
|
const resetSet = new Set(plan.resetNodeIds);
|
|
305
333
|
const importedSet = new Set(plan.importedNodeIds);
|
|
306
|
-
const tasksById = new Map(
|
|
334
|
+
const tasksById = new Map(childSpec.tasks.map((task) => [task.id, task]));
|
|
307
335
|
const nodes = {};
|
|
308
336
|
const importedFacts = [];
|
|
309
337
|
for (const nodeId of plan.importedNodeIds) {
|
|
@@ -328,7 +356,7 @@ async function materializeChildStaging(input) {
|
|
|
328
356
|
}
|
|
329
357
|
nodes[nodeId] = buildPendingNodeRecord(task);
|
|
330
358
|
}
|
|
331
|
-
for (const task of
|
|
359
|
+
for (const task of childSpec.tasks) {
|
|
332
360
|
if (!resetSet.has(task.id) && !importedSet.has(task.id)) {
|
|
333
361
|
throw new Error(`node ${task.id} missing from frontend recovery plan partition`);
|
|
334
362
|
}
|
|
@@ -338,7 +366,7 @@ async function materializeChildStaging(input) {
|
|
|
338
366
|
const canonicalContract = await copyArtifactVerified({
|
|
339
367
|
parentRunDir,
|
|
340
368
|
newRunDir: stagingDir,
|
|
341
|
-
relativePath: resolveCanonicalContractRelPath(
|
|
369
|
+
relativePath: resolveCanonicalContractRelPath(childSpec),
|
|
342
370
|
});
|
|
343
371
|
const writerAdmissionResult = await copyArtifactVerified({
|
|
344
372
|
parentRunDir,
|