@tea-agent/loop-agent 0.42.0-next.1 → 0.42.0-next.11
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 +321 -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 +4010 -78
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-extension-resolver.js +14 -2
- 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/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-helpers.js +4 -2
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
- package/dist/worker/observe/static/dag-node-purpose.js +13 -8
- package/dist/worker/observe/static/index.html +4 -4
- 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/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +7 -7
- 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 +231 -0
- package/dist/worker/observe/static/views/dag-graph.js +1 -1
- package/dist/worker/observe/static/views/dag-inspector.js +336 -181
- package/dist/worker/observe/static/views/dag.js +26 -14
- 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 +74 -13
- 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 +210 -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 +1031 -564
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +854 -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 +230 -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 +25 -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
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic backend-test plan protocol helpers.
|
|
3
|
+
*
|
|
4
|
+
* English canonical `##` headings remain the parse authority. Known Chinese
|
|
5
|
+
* aliases are rewritten only as an exact whole-line `##` heading, never as
|
|
6
|
+
* prose. Partition slot coverage follows Scheme A: Case count may be smaller
|
|
7
|
+
* than the enum count, but every exact `TP-SP-...` slot must appear as an
|
|
8
|
+
* independent variant Test Point.
|
|
9
|
+
*/
|
|
10
|
+
export const BACKEND_TEST_PLAN_HEADING_ALIASES = {
|
|
11
|
+
"模块索引": "Module Index",
|
|
12
|
+
"覆盖范围": "Coverage Scope",
|
|
13
|
+
"覆盖矩阵": "Coverage Matrix",
|
|
14
|
+
"场景分区": "Scenario Partitions",
|
|
15
|
+
};
|
|
16
|
+
export const BACKEND_TEST_SCENARIO_PARTITION_SLOTS_SCHEMA_ID = "backend-test-scenario-partition-slots-v1";
|
|
17
|
+
const CANONICAL_HEADING_LINE = /^##\s+(Module Index|Coverage Scope|Coverage Matrix|Scenario Partitions)\s*$/;
|
|
18
|
+
const ALIAS_HEADING_LINE = /^##\s+(模块索引|覆盖范围|覆盖矩阵|场景分区)\s*$/;
|
|
19
|
+
const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{2,3})\b/;
|
|
20
|
+
const SECTION_HEADING = /^###\s+/;
|
|
21
|
+
const TEST_POINT_RE = /\bTP-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
|
|
22
|
+
const VARIANT_LABEL = /变体测试点|Variant Test Points/i;
|
|
23
|
+
const FORBIDDEN_SLOT_ALIASES = new Set(["SINGLE", "MULTIPLE"]);
|
|
24
|
+
export function canonicalizeBackendTestPlanProtocolHeadings(markdown) {
|
|
25
|
+
const lines = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
|
|
26
|
+
const rewrittenHeadings = [];
|
|
27
|
+
const next = lines.map((line) => {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
const alias = ALIAS_HEADING_LINE.exec(trimmed);
|
|
30
|
+
if (!alias)
|
|
31
|
+
return line;
|
|
32
|
+
const canonical = BACKEND_TEST_PLAN_HEADING_ALIASES[alias[1]];
|
|
33
|
+
rewrittenHeadings.push(canonical);
|
|
34
|
+
return `## ${canonical}`;
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
markdown: next.join("\n"),
|
|
38
|
+
changed: rewrittenHeadings.length > 0,
|
|
39
|
+
rewrittenHeadings,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function countCanonicalPlanHeading(markdown, heading) {
|
|
43
|
+
const canonical = canonicalizeBackendTestPlanProtocolHeadings(markdown).markdown;
|
|
44
|
+
return canonical
|
|
45
|
+
.split("\n")
|
|
46
|
+
.filter((line) => line.trim() === `## ${heading}`).length;
|
|
47
|
+
}
|
|
48
|
+
function orderedUnique(values) {
|
|
49
|
+
return [...new Set(values.filter(Boolean))];
|
|
50
|
+
}
|
|
51
|
+
function collectSectionTokens(body, headingNeedle) {
|
|
52
|
+
const lines = body.split("\n");
|
|
53
|
+
const start = lines.findIndex((line) => line.trim() === `### ${headingNeedle}`);
|
|
54
|
+
if (start < 0)
|
|
55
|
+
return [];
|
|
56
|
+
const tokens = [];
|
|
57
|
+
for (const line of lines.slice(start + 1)) {
|
|
58
|
+
if (SECTION_HEADING.test(line) || CASE_HEADING.test(line.trim()))
|
|
59
|
+
break;
|
|
60
|
+
tokens.push(...(line.match(TEST_POINT_RE) ?? []));
|
|
61
|
+
}
|
|
62
|
+
return tokens;
|
|
63
|
+
}
|
|
64
|
+
function collectLabeledTokens(body, label) {
|
|
65
|
+
return body
|
|
66
|
+
.split("\n")
|
|
67
|
+
.filter((line) => label.test(line))
|
|
68
|
+
.flatMap((line) => line.match(TEST_POINT_RE) ?? []);
|
|
69
|
+
}
|
|
70
|
+
export function collectMarkdownPartitionSlotCoverage(markdown, requiredVariantSlots) {
|
|
71
|
+
const declaredTestPoints = [];
|
|
72
|
+
const variantTestPoints = [];
|
|
73
|
+
const lines = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
|
|
74
|
+
const headingIndexes = lines.flatMap((line, index) => CASE_HEADING.test(line.trim()) ? [index] : []);
|
|
75
|
+
for (let i = 0; i < headingIndexes.length; i += 1) {
|
|
76
|
+
const start = headingIndexes[i];
|
|
77
|
+
const end = headingIndexes[i + 1] ?? lines.length;
|
|
78
|
+
const body = lines.slice(start, end).join("\n");
|
|
79
|
+
declaredTestPoints.push(...collectSectionTokens(body, "测试点"));
|
|
80
|
+
variantTestPoints.push(...collectLabeledTokens(body, VARIANT_LABEL));
|
|
81
|
+
}
|
|
82
|
+
const declaredSet = new Set(orderedUnique(declaredTestPoints));
|
|
83
|
+
const variantSet = new Set(orderedUnique(variantTestPoints));
|
|
84
|
+
const required = requiredVariantSlots.filter((slotId) => !FORBIDDEN_SLOT_ALIASES.has(slotId));
|
|
85
|
+
return {
|
|
86
|
+
declaredTestPoints: orderedUnique(declaredTestPoints),
|
|
87
|
+
variantTestPoints: orderedUnique(variantTestPoints),
|
|
88
|
+
missingFromDeclared: required.filter((slotId) => !declaredSet.has(slotId)),
|
|
89
|
+
missingFromVariant: required.filter((slotId) => !variantSet.has(slotId)),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function isForbiddenPartitionSlotAlias(token) {
|
|
93
|
+
return FORBIDDEN_SLOT_ALIASES.has(token.trim().toUpperCase());
|
|
94
|
+
}
|
|
95
|
+
export function lookupRequiredVariantSlotsForMarkdownPath(inventory, markdownPath) {
|
|
96
|
+
if (!inventory)
|
|
97
|
+
return [];
|
|
98
|
+
const stem = markdownPath
|
|
99
|
+
.replace(/\\/g, "/")
|
|
100
|
+
.replace(/^.*\//, "")
|
|
101
|
+
.replace(/\.md$/i, "");
|
|
102
|
+
return inventory.modules.find((item) => item.stem === stem)?.requiredVariantSlots ?? [];
|
|
103
|
+
}
|
|
104
|
+
const MODULE_INDEX_HEADER = [
|
|
105
|
+
"Module Stem",
|
|
106
|
+
"Business Resource",
|
|
107
|
+
"Owned Operations",
|
|
108
|
+
"Owned Rule Keys",
|
|
109
|
+
"Case IDs",
|
|
110
|
+
"Split Reason",
|
|
111
|
+
"Markdown Path",
|
|
112
|
+
"Pytest Path",
|
|
113
|
+
];
|
|
114
|
+
function markdownTableCells(line) {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
if (!trimmed.startsWith("|") || !trimmed.endsWith("|"))
|
|
117
|
+
return undefined;
|
|
118
|
+
return trimmed
|
|
119
|
+
.slice(1, -1)
|
|
120
|
+
.split("|")
|
|
121
|
+
.map((cell) => cell.trim().replace(/^`|`$/g, ""));
|
|
122
|
+
}
|
|
123
|
+
function isMarkdownSeparatorRow(cells) {
|
|
124
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
125
|
+
}
|
|
126
|
+
/** Validate the minimum complete run-owned Markdown protocol before N2 succeeds. */
|
|
127
|
+
export function assessBackendTestPlanProtocol(markdown) {
|
|
128
|
+
const canonical = canonicalizeBackendTestPlanProtocolHeadings(markdown).markdown;
|
|
129
|
+
const lines = canonical.split("\n");
|
|
130
|
+
const issues = [];
|
|
131
|
+
if (lines.some((line) => /^(?:```|~~~)/.test(line.trim()))) {
|
|
132
|
+
issues.push({
|
|
133
|
+
code: "fenced-plan-output",
|
|
134
|
+
detail: "return the final Markdown plan directly, without a code fence",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const headingIndexes = new Map();
|
|
138
|
+
for (const heading of [
|
|
139
|
+
"Coverage Scope",
|
|
140
|
+
"Coverage Matrix",
|
|
141
|
+
"Scenario Partitions",
|
|
142
|
+
"Module Index",
|
|
143
|
+
]) {
|
|
144
|
+
headingIndexes.set(heading, lines.flatMap((line, index) => line.trim() === `## ${heading}` ? [index] : []));
|
|
145
|
+
}
|
|
146
|
+
const requireUnique = (heading, missingCode, duplicateCode) => {
|
|
147
|
+
const count = headingIndexes.get(heading)?.length ?? 0;
|
|
148
|
+
if (count === 0) {
|
|
149
|
+
issues.push({
|
|
150
|
+
code: missingCode,
|
|
151
|
+
detail: `require exactly one exact ## ${heading} section`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else if (count > 1) {
|
|
155
|
+
issues.push({
|
|
156
|
+
code: duplicateCode,
|
|
157
|
+
detail: `found ${count}; require exactly one exact ## ${heading} section`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
// Module Index is first so retry diagnostics lead with the shard authority.
|
|
162
|
+
requireUnique("Module Index", "missing-module-index", "duplicate-module-index");
|
|
163
|
+
requireUnique("Coverage Scope", "missing-coverage-scope", "duplicate-coverage-scope");
|
|
164
|
+
requireUnique("Coverage Matrix", "missing-coverage-matrix", "duplicate-coverage-matrix");
|
|
165
|
+
const scenarioCount = headingIndexes.get("Scenario Partitions")?.length ?? 0;
|
|
166
|
+
if (scenarioCount > 1) {
|
|
167
|
+
issues.push({
|
|
168
|
+
code: "duplicate-scenario-partitions",
|
|
169
|
+
detail: `found ${scenarioCount}; require at most one exact ## Scenario Partitions section`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const moduleIndexes = headingIndexes.get("Module Index") ?? [];
|
|
173
|
+
if (moduleIndexes.length === 1) {
|
|
174
|
+
const start = moduleIndexes[0] + 1;
|
|
175
|
+
const nextHeading = lines
|
|
176
|
+
.slice(start)
|
|
177
|
+
.findIndex((line) => /^##\s+\S/.test(line.trim()));
|
|
178
|
+
const end = nextHeading < 0 ? lines.length : start + nextHeading;
|
|
179
|
+
const rows = lines
|
|
180
|
+
.slice(start, end)
|
|
181
|
+
.map(markdownTableCells)
|
|
182
|
+
.filter((cells) => cells !== undefined);
|
|
183
|
+
const headerIndex = rows.findIndex((cells) => cells.length === MODULE_INDEX_HEADER.length &&
|
|
184
|
+
MODULE_INDEX_HEADER.every((name, index) => cells[index] === name));
|
|
185
|
+
if (headerIndex < 0) {
|
|
186
|
+
issues.push({
|
|
187
|
+
code: "invalid-module-index-header",
|
|
188
|
+
detail: `require exact Module Index header: ${MODULE_INDEX_HEADER.join(" | ")}`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const dataRows = rows
|
|
193
|
+
.slice(headerIndex + 1)
|
|
194
|
+
.filter((cells) => !isMarkdownSeparatorRow(cells));
|
|
195
|
+
if (!dataRows.some((cells) => cells.length === MODULE_INDEX_HEADER.length &&
|
|
196
|
+
Boolean(cells[0]?.trim()) &&
|
|
197
|
+
Boolean(cells[6]?.trim()) &&
|
|
198
|
+
Boolean(cells[7]?.trim()))) {
|
|
199
|
+
issues.push({
|
|
200
|
+
code: "empty-module-index",
|
|
201
|
+
detail: "Module Index must contain at least one complete 8-cell module row",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { ok: issues.length === 0, issues };
|
|
207
|
+
}
|
|
208
|
+
export function isCanonicalPlanHeadingLine(line) {
|
|
209
|
+
return CANONICAL_HEADING_LINE.test(line.trim());
|
|
210
|
+
}
|
|
@@ -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";
|
|
@@ -1116,7 +1213,11 @@ function compareIntent(intent, observed, bound, example, field) {
|
|
|
1116
1213
|
? "UNDETERMINED"
|
|
1117
1214
|
: "MISMATCH";
|
|
1118
1215
|
}
|
|
1119
|
-
|
|
1216
|
+
const comparableObserved = observed.kind === "boolean"
|
|
1217
|
+
? (observed.literal === "True" ? "true" : observed.literal === "False" ? "false" : literal)
|
|
1218
|
+
: literal;
|
|
1219
|
+
const comparableExpected = expected === "True" ? "true" : expected === "False" ? "false" : expected;
|
|
1220
|
+
return comparableObserved === comparableExpected
|
|
1120
1221
|
? "MATCH"
|
|
1121
1222
|
: observed.kind === "unknown"
|
|
1122
1223
|
? "UNDETERMINED"
|
|
@@ -1163,6 +1264,10 @@ function suggestedFixFor(intent, field, bound, example) {
|
|
|
1163
1264
|
return field && bound !== undefined
|
|
1164
1265
|
? `set ${field}="x"*${bound + 1}`
|
|
1165
1266
|
: undefined;
|
|
1267
|
+
case "max-1":
|
|
1268
|
+
return field && bound !== undefined
|
|
1269
|
+
? `set ${field}="x"*${Math.max(0, bound - 1)}`
|
|
1270
|
+
: undefined;
|
|
1166
1271
|
case "min":
|
|
1167
1272
|
return field && bound !== undefined
|
|
1168
1273
|
? `set ${field}="x"*${bound}`
|
|
@@ -1207,6 +1312,12 @@ function boundedScenarioText(value, logicalLength) {
|
|
|
1207
1312
|
const suffix = `<truncated length=${logicalLength ?? value.length}>`;
|
|
1208
1313
|
return `${value.slice(0, Math.max(0, MAX_PERSISTED_SCENARIO_TEXT - suffix.length - 3))}...${suffix}`;
|
|
1209
1314
|
}
|
|
1315
|
+
/** Facts schema forbids negative lengths. Internal helper sentinels stay in-memory only. */
|
|
1316
|
+
function persistedObservedLength(length) {
|
|
1317
|
+
return typeof length === "number" && Number.isFinite(length) && length >= 0
|
|
1318
|
+
? Math.trunc(length)
|
|
1319
|
+
: undefined;
|
|
1320
|
+
}
|
|
1210
1321
|
function localPythonFunctionRegion(source, functionName) {
|
|
1211
1322
|
const escaped = functionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1212
1323
|
const startMatch = new RegExp(`^def\\s+${escaped}\\s*\\(`, "m").exec(source);
|
|
@@ -1365,6 +1476,32 @@ function hasSafeSameModulePayloadHelper(source, block, parameterNames) {
|
|
|
1365
1476
|
return false;
|
|
1366
1477
|
return Boolean(safeSameModuleDictHelper(source, call.name));
|
|
1367
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
|
+
}
|
|
1368
1505
|
function observeDeterministicFunctionFieldOverride(input) {
|
|
1369
1506
|
const region = scenarioFunctionRegion(input.source, input.tpId);
|
|
1370
1507
|
if (!region)
|
|
@@ -1449,11 +1586,12 @@ function scenarioParamDiagnosticValues(intent, observed) {
|
|
|
1449
1586
|
const expectedRaw = intent.startsWith("custom-literal:")
|
|
1450
1587
|
? intent.slice("custom-literal:".length)
|
|
1451
1588
|
: intent === "unknown" ? undefined : intent;
|
|
1589
|
+
const persistedLength = persistedObservedLength(observed.length);
|
|
1452
1590
|
return {
|
|
1453
1591
|
expectedRaw,
|
|
1454
|
-
observedRaw: boundedScenarioText(observed.text,
|
|
1592
|
+
observedRaw: boundedScenarioText(observed.text, persistedLength),
|
|
1455
1593
|
expectedNormalized: expectedRaw === undefined ? undefined : normalizeCustomLiteralExpected(expectedRaw).slice(0, 512),
|
|
1456
|
-
observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal),
|
|
1594
|
+
observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal), persistedLength),
|
|
1457
1595
|
};
|
|
1458
1596
|
}
|
|
1459
1597
|
export async function assessBackendScenarioParamConsistency(input) {
|
|
@@ -1496,7 +1634,10 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
1496
1634
|
constants,
|
|
1497
1635
|
})
|
|
1498
1636
|
: undefined;
|
|
1499
|
-
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);
|
|
1500
1641
|
// Request-level / health checks often have no pytest.param payload row,
|
|
1501
1642
|
// or only a TP-id label positional (no field value).
|
|
1502
1643
|
const requestLevelNoParam = Boolean(source) &&
|
|
@@ -1539,8 +1680,8 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
1539
1680
|
tpId,
|
|
1540
1681
|
field: inferred.field,
|
|
1541
1682
|
intent: inferred.intent,
|
|
1542
|
-
observed: boundedScenarioText(observed.text, observed.length),
|
|
1543
|
-
observedLength: observed.length,
|
|
1683
|
+
observed: boundedScenarioText(observed.text, persistedObservedLength(observed.length)),
|
|
1684
|
+
observedLength: persistedObservedLength(observed.length),
|
|
1544
1685
|
status,
|
|
1545
1686
|
repairability,
|
|
1546
1687
|
reasonCode: status === "MATCH" ? "SCENARIO_PARAM_MATCH" : status === "MISMATCH" ? "SCENARIO_PARAM_MISMATCH" : "SCENARIO_PARAM_UNDETERMINED",
|
|
@@ -1709,6 +1850,12 @@ export function deterministicRewriteScenarioParam(input) {
|
|
|
1709
1850
|
}
|
|
1710
1851
|
valueLiteral = `"x" * ${bound + 1}`;
|
|
1711
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;
|
|
1712
1859
|
case "min-1":
|
|
1713
1860
|
if (bound === undefined) {
|
|
1714
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;
|