@tea-agent/loop-agent 0.42.0-next.1 → 0.42.0-next.10
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/AGENTS.md +1 -1
- package/CHANGELOG.md +319 -1
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/dag/run-dag.js +41 -0
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +6 -1
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3976 -76
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +978 -188
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/infrastructure/console/operation-store.js +226 -6
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +105 -5
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +64 -25
- package/dist/task/source-prepare/prepare.js +126 -21
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/model-resolver.js +7 -4
- package/dist/worker/console/chat/pi-runtime.js +258 -11
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +165 -71
- package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
- package/dist/worker/console/chat/session-catalog.js +2 -0
- package/dist/worker/console/chat/session-store.js +159 -70
- package/dist/worker/console/chat/shortcuts.js +19 -3
- package/dist/worker/console/chat/turn-process.js +111 -56
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operator-actions.js +169 -7
- package/dist/worker/console/pi-readiness.js +3 -2
- package/dist/worker/console/server.js +23 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
- package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-COptKq2S.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
- package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
- package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
- package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-D59YJ9kB.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-Bl13LpNa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CJj6VTog.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
- 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/console/static-src/shell/console-update-reload.js +36 -9
- package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
- package/dist/worker/console/workspace-context.js +115 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/dag-helpers.js +4 -2
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/worker/observe/static/inspect-workspace.js +23 -0
- package/dist/worker/observe/static/kpi.js +2 -2
- package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
- package/dist/worker/observe/static/operator-chrome.js +37 -23
- package/dist/worker/observe/static/relations.js +2 -2
- package/dist/worker/observe/static/router.d.ts +12 -1
- package/dist/worker/observe/static/router.js +48 -6
- package/dist/worker/observe/static/run-processing.js +4 -2
- package/dist/worker/observe/static/shell-chrome.js +2 -2
- package/dist/worker/observe/static/state.js +5 -0
- package/dist/worker/observe/static/styles.css +95 -0
- package/dist/worker/observe/static/views/dag-inspector.js +14 -7
- package/dist/worker/observe/static/views/dag.js +10 -2
- package/dist/worker/observe/static/views/dags.js +3 -1
- package/dist/worker/observe/static/views/dashboard.js +14 -6
- package/dist/worker/observe/static/views/pool.js +7 -2
- package/dist/worker/observe/static/views/run.js +12 -3
- package/dist/worker/observe/static/views/session-timeline.js +69 -10
- package/dist/worker/observe/static/views/task.js +7 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
- package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
- package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
- package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
- package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +1237 -192
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +226 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
- package/dist/workflows/dag/frontend-recovery-run.js +166 -34
- package/dist/workflows/dag/frontend-repair.js +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +941 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +419 -0
- package/dist/workflows/dag/frontend-shape.js +435 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +252 -25
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +319 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +1028 -562
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +828 -4
- package/dist/workflows/dag/prompt.js +70 -3
- package/dist/workflows/dag/recovery-lease.js +170 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +315 -1
- package/dist/workflows/dag/rerun-plan.js +17 -6
- package/dist/workflows/dag/rerun-run.js +44 -6
- package/dist/workflows/dag/rerun-task.js +251 -13
- package/dist/workflows/dag/retry-policy.js +219 -104
- package/dist/workflows/dag/runner.js +580 -124
- package/dist/workflows/dag/scheduler.js +133 -20
- package/dist/workflows/dag/types.js +244 -16
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/examples/README.md +5 -0
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/backend-test-dag.json +23 -20
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +1 -1
- package/package.json +4 -3
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
- package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -7,12 +7,72 @@ import { fetchJson } from "../api.js";
|
|
|
7
7
|
import {
|
|
8
8
|
uiState,
|
|
9
9
|
updateDagTimelineViewportState,
|
|
10
|
-
computeFollowLatest,
|
|
11
10
|
} from "../state.js";
|
|
12
11
|
|
|
13
12
|
/** Default visible lines for tool result preview before expand. */
|
|
14
13
|
export const TOOL_RESULT_PREVIEW_LINES = 20;
|
|
15
14
|
|
|
15
|
+
function isTimelineAutoScrollEnabled() {
|
|
16
|
+
return uiState.dagTimelineAutoScrollEnabled !== false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function scrollTimelineToLatest(content) {
|
|
20
|
+
content.scrollLeft = 0;
|
|
21
|
+
content.scrollTop = content.scrollHeight;
|
|
22
|
+
uiState.dagTimelineViewportState = updateDagTimelineViewportState(
|
|
23
|
+
uiState.dagTimelineViewportState,
|
|
24
|
+
uiState.currentDagRunId,
|
|
25
|
+
content.dataset.nodeId,
|
|
26
|
+
content.scrollLeft,
|
|
27
|
+
content.scrollTop,
|
|
28
|
+
true,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Floating, explicit live-follow control for the execution-record viewport. */
|
|
33
|
+
export function createTimelineAutoScrollControl(content) {
|
|
34
|
+
const dock = el("div", "process-timeline-autoscroll-dock");
|
|
35
|
+
const button = document.createElement("button");
|
|
36
|
+
button.type = "button";
|
|
37
|
+
const signal = el("span", "process-timeline-autoscroll-signal");
|
|
38
|
+
signal.setAttribute("aria-hidden", "true");
|
|
39
|
+
const icon = el("i", "ri-arrow-down-line");
|
|
40
|
+
icon.setAttribute("aria-hidden", "true");
|
|
41
|
+
button.appendChild(signal);
|
|
42
|
+
button.appendChild(icon);
|
|
43
|
+
|
|
44
|
+
const sync = () => {
|
|
45
|
+
const enabled = isTimelineAutoScrollEnabled();
|
|
46
|
+
const actionLabel = enabled ? "关闭自动滚动" : "自动滚动";
|
|
47
|
+
button.className = `process-timeline-autoscroll${enabled ? " is-active" : ""}`;
|
|
48
|
+
button.setAttribute("aria-pressed", String(enabled));
|
|
49
|
+
button.setAttribute("aria-label", actionLabel);
|
|
50
|
+
button.setAttribute("data-tooltip", actionLabel);
|
|
51
|
+
icon.className = enabled ? "ri-arrow-down-line" : "ri-pause-mini-fill";
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
button.addEventListener("click", () => {
|
|
55
|
+
const enabled = !isTimelineAutoScrollEnabled();
|
|
56
|
+
uiState.dagTimelineAutoScrollEnabled = enabled;
|
|
57
|
+
if (enabled) scrollTimelineToLatest(content);
|
|
58
|
+
else {
|
|
59
|
+
uiState.dagTimelineViewportState = updateDagTimelineViewportState(
|
|
60
|
+
uiState.dagTimelineViewportState,
|
|
61
|
+
uiState.currentDagRunId,
|
|
62
|
+
content.dataset.nodeId,
|
|
63
|
+
content.scrollLeft,
|
|
64
|
+
content.scrollTop,
|
|
65
|
+
false,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
sync();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
sync();
|
|
72
|
+
dock.appendChild(button);
|
|
73
|
+
return dock;
|
|
74
|
+
}
|
|
75
|
+
|
|
16
76
|
export function normalizeSessionEventFields(event) {
|
|
17
77
|
if (!event || typeof event !== "object") return {};
|
|
18
78
|
const args = event.args ?? event.input ?? event.toolInput;
|
|
@@ -951,6 +1011,7 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
|
|
|
951
1011
|
});
|
|
952
1012
|
toolbar.appendChild(exportBtn);
|
|
953
1013
|
content.appendChild(toolbar);
|
|
1014
|
+
content.appendChild(createTimelineAutoScrollControl(content));
|
|
954
1015
|
|
|
955
1016
|
const timelineItems = filterExecutionRecordItems(
|
|
956
1017
|
buildRichTimelineItems(visible.events, { showProtocol }),
|
|
@@ -1030,23 +1091,18 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
|
|
|
1030
1091
|
content.addEventListener(
|
|
1031
1092
|
"scroll",
|
|
1032
1093
|
() => {
|
|
1033
|
-
const followLatest = computeFollowLatest(
|
|
1034
|
-
content.scrollTop,
|
|
1035
|
-
content.scrollHeight,
|
|
1036
|
-
content.clientHeight,
|
|
1037
|
-
);
|
|
1038
1094
|
uiState.dagTimelineViewportState = updateDagTimelineViewportState(
|
|
1039
1095
|
uiState.dagTimelineViewportState,
|
|
1040
1096
|
uiState.currentDagRunId,
|
|
1041
1097
|
nodeId,
|
|
1042
1098
|
content.scrollLeft,
|
|
1043
1099
|
content.scrollTop,
|
|
1044
|
-
|
|
1100
|
+
isTimelineAutoScrollEnabled(),
|
|
1045
1101
|
);
|
|
1046
1102
|
},
|
|
1047
1103
|
{ passive: true },
|
|
1048
1104
|
);
|
|
1049
|
-
if (
|
|
1105
|
+
if (isTimelineAutoScrollEnabled()) {
|
|
1050
1106
|
content.scrollLeft = timelineState?.scrollLeft ?? 0;
|
|
1051
1107
|
content.scrollTop = content.scrollHeight;
|
|
1052
1108
|
} else {
|
|
@@ -1058,6 +1114,7 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
|
|
|
1058
1114
|
function rerenderSessionTimeline(content, nodeId, options) {
|
|
1059
1115
|
const scrollTop = content.scrollTop;
|
|
1060
1116
|
const scrollLeft = content.scrollLeft;
|
|
1117
|
+
const autoScrollEnabled = isTimelineAutoScrollEnabled();
|
|
1061
1118
|
const preserved = [...content.children].filter((child) =>
|
|
1062
1119
|
String(child.className ?? "")
|
|
1063
1120
|
.split(/\s+/)
|
|
@@ -1066,6 +1123,8 @@ function rerenderSessionTimeline(content, nodeId, options) {
|
|
|
1066
1123
|
while (content.firstChild) content.removeChild(content.firstChild);
|
|
1067
1124
|
for (const child of preserved) content.appendChild(child);
|
|
1068
1125
|
renderSessionTimeline(content, nodeId, options);
|
|
1069
|
-
|
|
1070
|
-
|
|
1126
|
+
if (!autoScrollEnabled) {
|
|
1127
|
+
content.scrollTop = scrollTop;
|
|
1128
|
+
content.scrollLeft = scrollLeft;
|
|
1129
|
+
}
|
|
1071
1130
|
}
|
|
@@ -36,7 +36,12 @@ import {
|
|
|
36
36
|
markdownLinkHref,
|
|
37
37
|
formatLedgerSummary,
|
|
38
38
|
} from "../format.js";
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
parseRoute,
|
|
41
|
+
navigate,
|
|
42
|
+
buildHashPath,
|
|
43
|
+
inspectPathToCanonicalHref,
|
|
44
|
+
} from "../router.js";
|
|
40
45
|
import {
|
|
41
46
|
fetchJson,
|
|
42
47
|
fetchJsonResult,
|
|
@@ -133,7 +138,7 @@ export async function renderTaskDetail(featureId, taskId) {
|
|
|
133
138
|
if (!c?.featureId || !c?.taskId) continue;
|
|
134
139
|
const path = `/feature/${encodeURIComponent(c.featureId)}/task/${encodeURIComponent(c.taskId)}`;
|
|
135
140
|
const a = el("a", "object-link", `${c.featureId}::${c.taskId}`);
|
|
136
|
-
a.href =
|
|
141
|
+
a.href = inspectPathToCanonicalHref(path);
|
|
137
142
|
a.addEventListener("click", (e) => {
|
|
138
143
|
e.preventDefault();
|
|
139
144
|
navigate(path);
|
|
@@ -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));
|
|
@@ -668,6 +675,92 @@ function automationLabelBody(body, labels) {
|
|
|
668
675
|
const match = new RegExp(`^\\s*[-*+]\\s*(?:${escaped})\\s*[::]\\s*(.*)$`, "mi").exec(automation);
|
|
669
676
|
return match?.[1]?.replaceAll("`", "").trim() ?? "";
|
|
670
677
|
}
|
|
678
|
+
const BINDING_NEGATION_OR_AMBIGUITY = /(?:不是|并非|非变体|非断言|非横切|可能|待定|或者|\b(?:TBD|unknown)\b)/i;
|
|
679
|
+
function explicitBindingModeFromLine(line) {
|
|
680
|
+
if (BINDING_NEGATION_OR_AMBIGUITY.test(line))
|
|
681
|
+
return undefined;
|
|
682
|
+
const modes = TEST_POINT_BINDING_MODES.filter((mode) => AUTOMATION_BINDING_LABELS[mode].some((label) => line.includes(label)));
|
|
683
|
+
return modes.length === 1 ? modes[0] : undefined;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Canonicalize only explicit, unambiguous Test Point classification marks.
|
|
687
|
+
* The model occasionally writes `TP-X(变体测试点)`, `[场景断言测试点] TP-X`
|
|
688
|
+
* or a pipe-delimited equivalent. Those forms carry deterministic semantics,
|
|
689
|
+
* but are not the machine contract consumed by automationLabelBody(). Keep the
|
|
690
|
+
* original prose and materialize the same binding under `### 自动化映射` as the
|
|
691
|
+
* canonical single-line labels. Never infer a mode from ordinary prose.
|
|
692
|
+
*/
|
|
693
|
+
export function canonicalizeBackendTestMarkdownTestPointBindings(markdown) {
|
|
694
|
+
const headings = [...markdown.matchAll(CASE_HEADING)];
|
|
695
|
+
let output = "";
|
|
696
|
+
let cursor = 0;
|
|
697
|
+
const unsafeTestPoints = [];
|
|
698
|
+
const canonicalizedTestPoints = [];
|
|
699
|
+
for (let index = 0; index < headings.length; index += 1) {
|
|
700
|
+
const heading = headings[index];
|
|
701
|
+
const start = heading.index;
|
|
702
|
+
const end = headings[index + 1]?.index ?? markdown.length;
|
|
703
|
+
const body = markdown.slice(start, end);
|
|
704
|
+
output += markdown.slice(cursor, start);
|
|
705
|
+
cursor = end;
|
|
706
|
+
const declared = listTokens(body, CASE_SECTION_ALIASES.testPoints, TEST_POINT);
|
|
707
|
+
const current = testPointBindingFacts(body, declared);
|
|
708
|
+
const currentByMode = new Map(TEST_POINT_BINDING_MODES.map((mode) => [
|
|
709
|
+
mode,
|
|
710
|
+
current.testPointBindings.filter((item) => item.mode === mode).map((item) => item.testPoint),
|
|
711
|
+
]));
|
|
712
|
+
const unresolved = new Set(current.unclassifiedTestPoints);
|
|
713
|
+
for (const line of body.split(/\r?\n/)) {
|
|
714
|
+
const mode = explicitBindingModeFromLine(line);
|
|
715
|
+
if (!mode)
|
|
716
|
+
continue;
|
|
717
|
+
const tokens = orderedUnique(line.match(/\bTP-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g) ?? []);
|
|
718
|
+
for (const token of tokens) {
|
|
719
|
+
if (!unresolved.has(token))
|
|
720
|
+
continue;
|
|
721
|
+
const existingModes = TEST_POINT_BINDING_MODES.filter((candidate) => (currentByMode.get(candidate) ?? []).includes(token));
|
|
722
|
+
if (existingModes.length > 0)
|
|
723
|
+
continue;
|
|
724
|
+
currentByMode.set(mode, orderedUnique([...(currentByMode.get(mode) ?? []), token]));
|
|
725
|
+
unresolved.delete(token);
|
|
726
|
+
canonicalizedTestPoints.push(token);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
unsafeTestPoints.push(...unresolved);
|
|
730
|
+
if (unresolved.size === current.unclassifiedTestPoints.length) {
|
|
731
|
+
output += body;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const automationMarker = /^###\s+(?:自动化映射|Automation Mapping)\s*$/mi.exec(body);
|
|
735
|
+
if (!automationMarker) {
|
|
736
|
+
output += body;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const automationStart = automationMarker.index + automationMarker[0].length;
|
|
740
|
+
const rest = body.slice(automationStart);
|
|
741
|
+
const nextSection = /^###\s+/m.exec(rest);
|
|
742
|
+
const automationEnd = automationStart + (nextSection?.index ?? rest.length);
|
|
743
|
+
let automation = body.slice(automationStart, automationEnd);
|
|
744
|
+
for (const mode of TEST_POINT_BINDING_MODES) {
|
|
745
|
+
const labels = AUTOMATION_BINDING_LABELS[mode];
|
|
746
|
+
const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
747
|
+
const canonicalLine = `- ${AUTOMATION_BINDING_LABELS[mode][0]}:${(currentByMode.get(mode) ?? []).join("; ") || "无"}`;
|
|
748
|
+
const linePattern = new RegExp(`^\\s*[-*+]\\s*(?:${escaped})\\s*[::].*$`, "mi");
|
|
749
|
+
if (linePattern.test(automation))
|
|
750
|
+
automation = automation.replace(linePattern, canonicalLine);
|
|
751
|
+
else
|
|
752
|
+
automation = `${automation.replace(/\s*$/, "")}\n${canonicalLine}\n`;
|
|
753
|
+
}
|
|
754
|
+
output += body.slice(0, automationStart) + automation + body.slice(automationEnd);
|
|
755
|
+
}
|
|
756
|
+
output += markdown.slice(cursor);
|
|
757
|
+
return {
|
|
758
|
+
markdown: output,
|
|
759
|
+
changed: output !== markdown,
|
|
760
|
+
unsafeTestPoints: orderedUnique(unsafeTestPoints),
|
|
761
|
+
canonicalizedTestPoints: orderedUnique(canonicalizedTestPoints),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
671
764
|
export function inspectBackendTestMarkdownTestPointBindings(markdown) {
|
|
672
765
|
const headings = [...markdown.matchAll(CASE_HEADING)];
|
|
673
766
|
return headings.map((heading, index) => {
|
|
@@ -710,6 +803,16 @@ function isEvidenceOnlyMarkdownCase(testCase) {
|
|
|
710
803
|
!hasVariant &&
|
|
711
804
|
hasNonVariant);
|
|
712
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
|
+
}
|
|
713
816
|
function payloadContract(body) {
|
|
714
817
|
const contractValue = automationLabelBody(body, ["Payload Contract", "载荷合同"]);
|
|
715
818
|
const requiredValue = automationLabelBody(body, ["Payload Required Paths", "载荷必需路径"]);
|
|
@@ -718,7 +821,7 @@ function payloadContract(body) {
|
|
|
718
821
|
const tokens = (value) => orderedUnique(value
|
|
719
822
|
.split(/[,;,;]/)
|
|
720
823
|
.map((item) => item.trim())
|
|
721
|
-
.filter((item) => !/^(?:none|无|not-applicable|n\/a)$/i.test(item))
|
|
824
|
+
.filter((item) => !/^(?:none|无|not-applicable|n\/a|true|false)$/i.test(item))
|
|
722
825
|
.filter((item) => /^[A-Za-z_][A-Za-z0-9_.]*$/.test(item)));
|
|
723
826
|
const enumValues = {};
|
|
724
827
|
for (const assignment of enumValue.split(/[,;,;]/).map((item) => item.trim()).filter(Boolean)) {
|
|
@@ -1312,8 +1415,16 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
1312
1415
|
function normalizeModuleStem(markdownPath) {
|
|
1313
1416
|
return path.posix.basename(markdownPath).replace(/\.md$/i, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
|
|
1314
1417
|
}
|
|
1315
|
-
function expectedScript(markdownPath, layout) {
|
|
1418
|
+
function expectedScript(markdownPath, layout, declaredScripts = []) {
|
|
1316
1419
|
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
1420
|
+
if (declaredScripts.length === 1) {
|
|
1421
|
+
const candidate = declaredScripts[0].replaceAll("\\", "/").replace(/^\.\//, "");
|
|
1422
|
+
const prefix = resolved.scriptDir === "." ? "" : `${resolved.scriptDir}/`;
|
|
1423
|
+
const relative = prefix && candidate.startsWith(prefix) ? candidate.slice(prefix.length) : candidate;
|
|
1424
|
+
if ((!prefix || candidate.startsWith(prefix)) && !relative.includes("..") && /^test_[A-Za-z0-9_.-]+\.py$/.test(path.posix.basename(candidate))) {
|
|
1425
|
+
return candidate;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1317
1428
|
return `${resolved.scriptDir}/test_${normalizeModuleStem(markdownPath)}.py`;
|
|
1318
1429
|
}
|
|
1319
1430
|
function symbolCaseId(symbol) {
|
|
@@ -1369,8 +1480,15 @@ for top in ast.walk(tree):
|
|
|
1369
1480
|
returned_names.add(node.value.id)
|
|
1370
1481
|
if isinstance(node,ast.Call):
|
|
1371
1482
|
for keyword in node.keywords:
|
|
1372
|
-
if keyword.arg in {'payload','body','request_body','request_json','dto','json','data','json_body'} and isinstance(keyword.value,ast.Name) and keyword.value.id in positional:
|
|
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:
|
|
1373
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))
|
|
1374
1492
|
if isinstance(node.func,ast.Attribute) and node.func.attr.lower() in {'post','put','patch'} and len(node.args) >= 2:
|
|
1375
1493
|
candidate=node.args[1]
|
|
1376
1494
|
if isinstance(candidate,ast.Name) and candidate.id in positional:
|
|
@@ -1382,6 +1500,17 @@ for top in ast.walk(tree):
|
|
|
1382
1500
|
matched=node.args[nested_position] if nested_position >= 0 and nested_position < len(node.args) else None
|
|
1383
1501
|
if isinstance(matched,ast.Name) and matched.id in positional:
|
|
1384
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))
|
|
1385
1514
|
for returned_name in returned_names:
|
|
1386
1515
|
entries=mutated.get(returned_name,[])
|
|
1387
1516
|
if entries:
|
|
@@ -1390,7 +1519,7 @@ for top in ast.walk(tree):
|
|
|
1390
1519
|
for node in ast.walk(top):
|
|
1391
1520
|
if not isinstance(node,ast.Call): continue
|
|
1392
1521
|
for keyword in node.keywords:
|
|
1393
|
-
if keyword.arg in {'payload','body','request_body','request_json','dto','json','data','json_body'} and isinstance(keyword.value,ast.Name) and keyword.value.id in assigned:
|
|
1522
|
+
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 assigned:
|
|
1394
1523
|
transport_shapes.append(assigned[keyword.value.id])
|
|
1395
1524
|
if candidates: FUNCTION_PAYLOADS[top.name]=candidates
|
|
1396
1525
|
if transport_shapes: FUNCTION_TRANSPORT_SHAPES[top.name]=transport_shapes
|
|
@@ -1446,8 +1575,9 @@ def literal(node):
|
|
|
1446
1575
|
return repr(node.value)
|
|
1447
1576
|
return None
|
|
1448
1577
|
|
|
1449
|
-
def collect_dict(node, prefix, paths, values):
|
|
1578
|
+
def collect_dict(node, prefix, paths, values, parameters=None):
|
|
1450
1579
|
if not isinstance(node, ast.Dict): return
|
|
1580
|
+
parameters = parameters or {}
|
|
1451
1581
|
for key,value in zip(node.keys,node.values):
|
|
1452
1582
|
if not isinstance(key, ast.Constant) or not isinstance(key.value,str): continue
|
|
1453
1583
|
# Ignore writer helper/meta keys that are not DTO request fields.
|
|
@@ -1457,12 +1587,16 @@ def collect_dict(node, prefix, paths, values):
|
|
|
1457
1587
|
paths.add(current)
|
|
1458
1588
|
lv=literal(value)
|
|
1459
1589
|
if lv is not None: values.setdefault(current,set()).add(lv)
|
|
1460
|
-
|
|
1590
|
+
elif isinstance(value, ast.Name) and value.id in parameters:
|
|
1591
|
+
for bound in parameters[value.id]:
|
|
1592
|
+
bound_lv=literal(bound)
|
|
1593
|
+
if bound_lv is not None: values.setdefault(current,set()).add(bound_lv)
|
|
1594
|
+
if isinstance(value, ast.Dict): collect_dict(value,current,paths,values,parameters)
|
|
1461
1595
|
elif isinstance(value,(ast.List,ast.Tuple)):
|
|
1462
1596
|
for item in value.elts:
|
|
1463
|
-
if isinstance(item,ast.Dict): collect_dict(item,current,paths,values)
|
|
1597
|
+
if isinstance(item,ast.Dict): collect_dict(item,current,paths,values,parameters)
|
|
1464
1598
|
|
|
1465
|
-
PAYLOAD_NAMES={'payload','body','request_body','request_json','dto','json','data','json_body'}
|
|
1599
|
+
PAYLOAD_NAMES={'payload','body','request_body','request_json','dto','json','data','json_body','params','query','query_params'}
|
|
1466
1600
|
|
|
1467
1601
|
def shape_only(node):
|
|
1468
1602
|
if isinstance(node,ast.Dict):
|
|
@@ -1543,6 +1677,7 @@ def decorator_payload_nodes(parameters,assigned):
|
|
|
1543
1677
|
|
|
1544
1678
|
def decorator_payload_nodes_by_tp(fn,assigned):
|
|
1545
1679
|
result={}
|
|
1680
|
+
function_payload_nodes=payload_nodes(fn)
|
|
1546
1681
|
for dec in fn.decorator_list:
|
|
1547
1682
|
if not isinstance(dec,ast.Call) or not isinstance(dec.func,ast.Attribute) or dec.func.attr != 'parametrize' or len(dec.args) < 2: continue
|
|
1548
1683
|
if not isinstance(dec.args[0],ast.Constant) or not isinstance(dec.args[0].value,str): continue
|
|
@@ -1559,9 +1694,11 @@ def decorator_payload_nodes_by_tp(fn,assigned):
|
|
|
1559
1694
|
parameters={}
|
|
1560
1695
|
for index,name in enumerate(names):
|
|
1561
1696
|
if index < len(node.args): parameters.setdefault(name,[]).append(node.args[index])
|
|
1562
|
-
|
|
1697
|
+
payload_nodes_for_tp=decorator_payload_nodes(parameters,assigned)
|
|
1698
|
+
if not payload_nodes_for_tp:
|
|
1699
|
+
payload_nodes_for_tp=function_payload_nodes
|
|
1563
1700
|
paths=set(); values={}
|
|
1564
|
-
for payload_node in
|
|
1701
|
+
for payload_node in payload_nodes_for_tp: collect_dict(payload_node,'',paths,values,parameters)
|
|
1565
1702
|
result[tp]={key:sorted(items) for key,items in sorted(values.items())}
|
|
1566
1703
|
return result
|
|
1567
1704
|
|
|
@@ -1603,6 +1740,20 @@ def is_direct_transport_call(node):
|
|
|
1603
1740
|
if isinstance(current,ast.Name): receiver_tokens.append(current.id.lower())
|
|
1604
1741
|
return any(token in {'requests','httpx','client','session','api_client','http_client'} or token.endswith('_client') for token in receiver_tokens)
|
|
1605
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
|
+
|
|
1606
1757
|
def payload_nodes(fn):
|
|
1607
1758
|
result=[]
|
|
1608
1759
|
assigned={}
|
|
@@ -1625,7 +1776,7 @@ def payload_nodes(fn):
|
|
|
1625
1776
|
# Observe local payload/body variables only when they flow into a direct
|
|
1626
1777
|
# HTTP transport call. Arbitrary setup variables are not target requests.
|
|
1627
1778
|
for node in ast.walk(fn):
|
|
1628
|
-
if not is_direct_transport_call(node): continue
|
|
1779
|
+
if not (is_direct_transport_call(node) or is_fixture_http_call(node)): continue
|
|
1629
1780
|
for keyword in node.keywords:
|
|
1630
1781
|
if keyword.arg in PAYLOAD_NAMES: result.extend(payload_expr_nodes(keyword.value,assigned,parameters))
|
|
1631
1782
|
return result
|
|
@@ -1635,8 +1786,8 @@ for fn in ast.walk(tree):
|
|
|
1635
1786
|
if isinstance(fn,(ast.FunctionDef,ast.AsyncFunctionDef)) and fn.name.startswith('test_'):
|
|
1636
1787
|
paths=set(); values={}
|
|
1637
1788
|
fallback_paths=set(); fallback_values={}
|
|
1638
|
-
for node in payload_nodes(fn): collect_dict(node,'',paths,values)
|
|
1639
|
-
for node in fallback_payload_nodes(fn): collect_dict(node,'',fallback_paths,fallback_values)
|
|
1789
|
+
for node in payload_nodes(fn): collect_dict(node,'',paths,values,{})
|
|
1790
|
+
for node in fallback_payload_nodes(fn): collect_dict(node,'',fallback_paths,fallback_values,{})
|
|
1640
1791
|
out[fn.name]={
|
|
1641
1792
|
'paths':sorted(paths),
|
|
1642
1793
|
'values':{key:sorted(items) for key,items in sorted(values.items())},
|
|
@@ -1688,14 +1839,39 @@ function metadataTestPoints(region, label) {
|
|
|
1688
1839
|
const match = new RegExp(`^\\s*${escaped}\\s*:\\s*(.*)$`, "mi").exec(region);
|
|
1689
1840
|
return orderedUnique(match?.[1]?.match(/\bTP-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g) ?? []);
|
|
1690
1841
|
}
|
|
1842
|
+
function isPytestDecoratorOrContinuationLine(line) {
|
|
1843
|
+
const trimmed = line.trim();
|
|
1844
|
+
if (!trimmed)
|
|
1845
|
+
return false;
|
|
1846
|
+
if (trimmed.startsWith("#"))
|
|
1847
|
+
return true;
|
|
1848
|
+
if (trimmed.startsWith("@"))
|
|
1849
|
+
return true;
|
|
1850
|
+
// Multi-line decorator argument lists: @pytest.mark.parametrize(\n "x", [\n pytest.param(..., id="TP-...")\n ]\n)
|
|
1851
|
+
return /^[\]\),]/.test(trimmed) || /[\(\[{,]$/.test(trimmed) || /\bpytest\.param\s*\(/.test(trimmed);
|
|
1852
|
+
}
|
|
1691
1853
|
function pytestFunctionRegionStart(source, functionIndex) {
|
|
1692
1854
|
let regionStart = source.lastIndexOf("\n", functionIndex - 1) + 1;
|
|
1693
1855
|
let cursor = regionStart;
|
|
1856
|
+
let blankRun = 0;
|
|
1694
1857
|
while (cursor > 0) {
|
|
1695
1858
|
const previousEnd = cursor - 1;
|
|
1696
1859
|
const previousStart = source.lastIndexOf("\n", previousEnd - 1) + 1;
|
|
1697
1860
|
const previous = source.slice(previousStart, previousEnd).replace(/\r$/, "");
|
|
1698
|
-
|
|
1861
|
+
const trimmed = previous.trim();
|
|
1862
|
+
if (!trimmed) {
|
|
1863
|
+
blankRun += 1;
|
|
1864
|
+
// Keep walking across a small blank gap so a decorator is still bound to
|
|
1865
|
+
// the following test when writers insert one empty line before `def`.
|
|
1866
|
+
if (blankRun > 2)
|
|
1867
|
+
break;
|
|
1868
|
+
cursor = previousStart;
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
blankRun = 0;
|
|
1872
|
+
if (/^(?:async\s+)?def\s+/.test(trimmed) || /^(?:async\s+)?class\s+/.test(trimmed))
|
|
1873
|
+
break;
|
|
1874
|
+
if (!isPytestDecoratorOrContinuationLine(previous))
|
|
1699
1875
|
break;
|
|
1700
1876
|
regionStart = previousStart;
|
|
1701
1877
|
cursor = previousStart;
|
|
@@ -1725,12 +1901,14 @@ function pytestFunctionBodyEnd(source, signatureEnd, functionIndent) {
|
|
|
1725
1901
|
}
|
|
1726
1902
|
function pytestParameterCollections(source) {
|
|
1727
1903
|
const collections = new Map();
|
|
1728
|
-
for (const match of source.matchAll(/^(
|
|
1904
|
+
for (const match of source.matchAll(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\[(?![^\r\n]*\])([\s\S]*?)^\]/gm)) {
|
|
1729
1905
|
const explicitIds = [...match[2].matchAll(/\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/g)].map((item) => item[1]);
|
|
1730
1906
|
const factoryIds = explicitIds.length === 0
|
|
1731
1907
|
? [...match[2].matchAll(/["'](TP-[A-Z0-9-]+)["']/g)].map((item) => item[1])
|
|
1732
1908
|
: [];
|
|
1733
|
-
|
|
1909
|
+
const ids = orderedUnique([...explicitIds, ...factoryIds]);
|
|
1910
|
+
if (ids.length > 0)
|
|
1911
|
+
collections.set(match[1], ids);
|
|
1734
1912
|
}
|
|
1735
1913
|
return collections;
|
|
1736
1914
|
}
|
|
@@ -1792,6 +1970,12 @@ function normalizePythonLiteralValue(value) {
|
|
|
1792
1970
|
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
1793
1971
|
return trimmed.slice(1, -1);
|
|
1794
1972
|
}
|
|
1973
|
+
if (trimmed === "True")
|
|
1974
|
+
return "true";
|
|
1975
|
+
if (trimmed === "False")
|
|
1976
|
+
return "false";
|
|
1977
|
+
if (trimmed === "None")
|
|
1978
|
+
return "null";
|
|
1795
1979
|
return trimmed;
|
|
1796
1980
|
}
|
|
1797
1981
|
function intentionalInvalidEnumVariant(input) {
|
|
@@ -1836,13 +2020,15 @@ function assessPayloadContract(testCase, refs) {
|
|
|
1836
2020
|
: [];
|
|
1837
2021
|
const enumMismatches = [];
|
|
1838
2022
|
for (const [field, allowed] of Object.entries(testCase.payloadContract.enumValues)) {
|
|
1839
|
-
const
|
|
2023
|
+
const aggregated = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? []));
|
|
2024
|
+
const byTestPoint = orderedUnique(refs.flatMap((item) => Object.values(item.payloadValuesByTestPoint).flatMap((values) => values[field] ?? [])));
|
|
2025
|
+
const observed = orderedUnique([...aggregated, ...byTestPoint]);
|
|
1840
2026
|
for (const rawValue of observed) {
|
|
1841
2027
|
const value = normalizePythonLiteralValue(rawValue);
|
|
1842
2028
|
if (allowed.includes(value))
|
|
1843
2029
|
continue;
|
|
1844
2030
|
const associatedTestPoints = orderedUnique(refs.flatMap((item) => Object.entries(item.payloadValuesByTestPoint)
|
|
1845
|
-
.filter(([, values]) => (values[field] ?? []).
|
|
2031
|
+
.filter(([, values]) => (values[field] ?? []).some((itemValue) => itemValue === rawValue || normalizePythonLiteralValue(itemValue) === value))
|
|
1846
2032
|
.map(([testPoint]) => testPoint)));
|
|
1847
2033
|
const intentionallyInvalid = associatedTestPoints.length > 0 && associatedTestPoints.every((testPoint) => intentionalInvalidEnumVariant({ testCase, testPoint, field, observedValue: rawValue, allowed }));
|
|
1848
2034
|
if (!intentionallyInvalid)
|
|
@@ -1863,7 +2049,7 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
|
|
|
1863
2049
|
const layout = input.layout ?? resolveBackendTestLayout(undefined);
|
|
1864
2050
|
const cases = await parseMarkdownCases(input.workspaceRoot, layout);
|
|
1865
2051
|
const modulePaths = orderedUnique(cases.map((item) => item.markdownPath));
|
|
1866
|
-
const scripts = orderedUnique(cases.flatMap((item) => [
|
|
2052
|
+
const scripts = orderedUnique(cases.flatMap((item) => [expectedScript(item.markdownPath, layout, item.declaredScripts)]));
|
|
1867
2053
|
const allSymbols = [];
|
|
1868
2054
|
const scriptFiles = [];
|
|
1869
2055
|
for (const script of scripts) {
|
|
@@ -1876,7 +2062,7 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
|
|
|
1876
2062
|
const findings = [];
|
|
1877
2063
|
const entries = [];
|
|
1878
2064
|
for (const testCase of cases) {
|
|
1879
|
-
const expected = expectedScript(testCase.markdownPath, layout);
|
|
2065
|
+
const expected = expectedScript(testCase.markdownPath, layout, testCase.declaredScripts);
|
|
1880
2066
|
const declared = testCase.declaredScripts[0];
|
|
1881
2067
|
const refs = allSymbols.filter((item) => item.caseIds.includes(testCase.caseId));
|
|
1882
2068
|
const actualScripts = orderedUnique(refs.map((item) => item.script));
|
|
@@ -2034,8 +2220,9 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
|
|
|
2034
2220
|
for (const [signature, owners] of duplicateSignatureGroups)
|
|
2035
2221
|
findings.push(`duplicate execution signature ${signature}: ${orderedUnique(owners).join(", ")}`);
|
|
2036
2222
|
const exactModuleCount = modulePaths.filter((modulePath) => {
|
|
2037
|
-
const expected = expectedScript(modulePath, layout);
|
|
2038
2223
|
const moduleCases = cases.filter((item) => item.markdownPath === modulePath);
|
|
2224
|
+
const moduleDeclaredScripts = orderedUnique(moduleCases.flatMap((item) => item.declaredScripts));
|
|
2225
|
+
const expected = expectedScript(modulePath, layout, moduleDeclaredScripts);
|
|
2039
2226
|
return moduleCases.every((item) => item.declaredScripts.length === 1 && item.declaredScripts[0] === expected) && scriptFiles.some((file) => path.relative(input.workspaceRoot, file).replaceAll(path.sep, "/") === expected);
|
|
2040
2227
|
}).length;
|
|
2041
2228
|
const caseEntries = entries.filter((entry) => entry.cardinality !== "0:1");
|
|
@@ -285,6 +285,7 @@ export async function resolveBackendTestManifestModules(workspaceRoot, layout) {
|
|
|
285
285
|
moduleIndexHeadingCount,
|
|
286
286
|
declaredStems: [],
|
|
287
287
|
declaredMdFiles: [],
|
|
288
|
+
declaredScriptByMarkdown: {},
|
|
288
289
|
declaredMissing: [],
|
|
289
290
|
unmanifested: [],
|
|
290
291
|
});
|
|
@@ -306,6 +307,15 @@ export async function resolveBackendTestManifestModules(workspaceRoot, layout) {
|
|
|
306
307
|
return invalid("duplicate", moduleIndexHeadingCount);
|
|
307
308
|
}
|
|
308
309
|
const declaredStems = extractDeclaredModuleIndexFilenames(readme);
|
|
310
|
+
const declaredScriptByMarkdown = {};
|
|
311
|
+
const mdPrefix = escapeRegExp(`${resolved.markdownDir}/`);
|
|
312
|
+
const scriptPrefix = escapeRegExp(`${resolved.scriptDir}/`);
|
|
313
|
+
for (const line of readme.split(/\r?\n/)) {
|
|
314
|
+
const markdown = new RegExp(`(${mdPrefix}[A-Za-z0-9_.-]+\\.md)`).exec(line)?.[1];
|
|
315
|
+
const pytest = new RegExp(`(${scriptPrefix}[A-Za-z0-9_./-]+\\.py)`).exec(line)?.[1];
|
|
316
|
+
if (markdown && pytest)
|
|
317
|
+
declaredScriptByMarkdown[markdown] = pytest;
|
|
318
|
+
}
|
|
309
319
|
if (declaredStems.length === 0) {
|
|
310
320
|
return invalid("empty-unparsable", moduleIndexHeadingCount);
|
|
311
321
|
}
|
|
@@ -345,6 +355,7 @@ export async function resolveBackendTestManifestModules(workspaceRoot, layout) {
|
|
|
345
355
|
moduleIndexHeadingCount,
|
|
346
356
|
declaredStems,
|
|
347
357
|
declaredMdFiles: declaredMdFiles.sort(),
|
|
358
|
+
declaredScriptByMarkdown,
|
|
348
359
|
declaredMissing: declaredMissing.sort(),
|
|
349
360
|
unmanifested: unmanifested.sort(),
|
|
350
361
|
};
|
|
@@ -694,7 +705,8 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
694
705
|
if (!hasAssertableExpectedResult(expected)) {
|
|
695
706
|
findings.push(`${testCase.id} has no structured expected result`);
|
|
696
707
|
}
|
|
697
|
-
const expectedScript =
|
|
708
|
+
const expectedScript = manifest.declaredScriptByMarkdown[relativeFile] ??
|
|
709
|
+
expectedBackendTestPytestScriptForMarkdownModule(relativeFile, layout);
|
|
698
710
|
const mappedScripts = extractMappedPytestScripts(testCase.body);
|
|
699
711
|
if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
|
|
700
712
|
// Evidence-only meta cases intentionally declare 脚本/primary symbol as 无 and keep only assertion/cross-cutting TPs.
|