@tea-agent/loop-agent 0.25.5 → 0.26.0
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 +2 -1
- package/CHANGELOG.md +1020 -986
- package/bin/loop-agent.js +21 -21
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/init.js +68 -129
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/dag-pi-executor.js +26 -20
- package/dist/executors/model-routing.js +34 -18
- package/dist/executors/shell-executor.js +1 -1
- package/dist/governance/manifest-types.js +33 -5
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/task-demand-routing.js +3 -1
- package/dist/worker/console/chat/model-resolver.js +15 -3
- package/dist/worker/observe/static/constants.js +3 -2
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/dag-model.js +1 -0
- package/dist/worker/observe/static/dom.js +220 -220
- package/dist/worker/observe/static/relations.js +133 -133
- package/dist/worker/observe/static/router.js +93 -93
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/styles.css +182 -42
- package/dist/worker/observe/static/views/batch.js +227 -227
- package/dist/worker/observe/static/views/dag-graph.js +172 -172
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -492
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +362 -45
- package/dist/workflows/dag/backend-test-case-manifest.js +20 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +17 -0
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/init-hybrid.js +18 -15
- package/dist/workflows/dag/lifecycle.js +40 -30
- package/dist/workflows/dag/node-execution.js +13 -0
- package/dist/workflows/dag/types.js +59 -19
- package/docs/init-surface.manifest.json +3 -1
- package/docs/skills/README.md +7 -7
- package/docs/templates/README.md +1 -0
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +473 -473
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/backend-test-dag.json +9 -9
- package/docs/templates/backend-test-result.schema.json +99 -99
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -42
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
- package/docs/templates/frontend-eval/metrics.md +138 -138
- package/docs/templates/frontend-eval/smoke-targets.md +53 -53
- package/docs/templates/frontend-task-constraints.md +35 -35
- package/docs/templates/frontend-task-requirement.md +70 -70
- package/docs/templates/harness.schema.json +29 -7
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/init-managed-agents.md +137 -0
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
- package/docs/templates/knowledge-sync-dag.json +178 -178
- package/docs/templates/knowledge-sync-draft.schema.json +71 -71
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +80 -80
- package/docs/templates/worker-dogfood-setup.md +68 -68
- package/harness.json +1 -2
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
- package/scripts/kb-graph-incremental-prepare.mjs +386 -386
- package/scripts/kb-graph-materialize.mjs +105 -105
- package/scripts/kb-graph-promote.mjs +164 -164
- package/scripts/kb-query.mjs +554 -554
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/SKILL.md +67 -67
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
- package/skills/analyze-product-dependencies/references/example.md +76 -76
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
- package/skills/analyze-product-requirements/SKILL.md +90 -90
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
- package/skills/analyze-product-requirements/references/example.md +86 -86
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
- package/skills/browser-tools/browser-content.js +103 -103
- package/skills/browser-tools/browser-cookies.js +35 -35
- package/skills/browser-tools/browser-eval.js +53 -53
- package/skills/browser-tools/browser-hn-scraper.js +108 -108
- package/skills/browser-tools/browser-nav.js +44 -44
- package/skills/browser-tools/browser-pick.js +162 -162
- package/skills/browser-tools/browser-screenshot.js +34 -34
- package/skills/browser-tools/browser-start.js +86 -86
- package/skills/browser-tools/package-lock.json +2556 -2556
- package/skills/browser-tools/package.json +19 -19
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +5 -4
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +57 -57
- package/skills/loop-agent/references/model-routing.md +2 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/playwright-cli/SKILL.md +420 -420
- package/skills/playwright-cli/references/element-attributes.md +23 -23
- package/skills/playwright-cli/references/playwright-tests.md +39 -39
- package/skills/playwright-cli/references/request-mocking.md +87 -87
- package/skills/playwright-cli/references/running-code.md +241 -241
- package/skills/playwright-cli/references/session-management.md +225 -225
- package/skills/playwright-cli/references/storage-state.md +275 -275
- package/skills/playwright-cli/references/test-generation.md +433 -433
- package/skills/playwright-cli/references/tracing.md +139 -139
- package/skills/playwright-cli/references/video-recording.md +143 -143
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/using-git-worktrees/SKILL.md +215 -215
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
|
@@ -4,10 +4,11 @@ import path from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import YAML from "yaml";
|
|
6
6
|
import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
|
|
7
|
-
const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{
|
|
8
|
-
const CASE_ID_IN_TEXT = /\bBE-[A-Z0-9_-]+-\d{
|
|
7
|
+
const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{3})\b.*$/gm;
|
|
8
|
+
const CASE_ID_IN_TEXT = /\bBE-[A-Z0-9_-]+-\d{3}\b/g;
|
|
9
|
+
const NON_CANONICAL_CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-(?:\d{2}|\d{2,3}[A-Z]+))\b.*$/gm;
|
|
9
10
|
const AC_ID_IN_TEXT = /\bAC-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
|
|
10
|
-
const RULE_KEY = /^(?:AC|REQ|BR|API)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/;
|
|
11
|
+
const RULE_KEY = /^(?:AC|REQ|BR|API|GET|POST|PUT|PATCH|DELETE)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/;
|
|
11
12
|
const TEST_POINT = /^TP-[A-Z0-9]+(?:-[A-Z0-9]+)*$/;
|
|
12
13
|
const COVERAGE_HEADERS = [
|
|
13
14
|
"Rule Key",
|
|
@@ -27,6 +28,40 @@ const CASE_SECTION_ALIASES = {
|
|
|
27
28
|
scenarioTypes: ["场景类型", "Scenario Types", "Scenario Type"],
|
|
28
29
|
automation: ["自动化映射", "自动化说明", "Automation Notes"],
|
|
29
30
|
};
|
|
31
|
+
const TEST_POINT_BINDING_MODES = ["variant", "assertion", "cross-cutting"];
|
|
32
|
+
const CHANGE_CLASSIFICATIONS = [
|
|
33
|
+
"new-operation",
|
|
34
|
+
"contract-change",
|
|
35
|
+
"behavior-change",
|
|
36
|
+
"bugfix",
|
|
37
|
+
"implementation-optimization",
|
|
38
|
+
];
|
|
39
|
+
const COVERAGE_POLICIES = [
|
|
40
|
+
"full-contract",
|
|
41
|
+
"affected-contract-full",
|
|
42
|
+
"affected-behavior-full",
|
|
43
|
+
"reproduction-plus-neighbors",
|
|
44
|
+
"change-focused-plus-regression-floor",
|
|
45
|
+
];
|
|
46
|
+
const COVERAGE_POLICY_BY_CLASSIFICATION = {
|
|
47
|
+
"new-operation": "full-contract",
|
|
48
|
+
"contract-change": "affected-contract-full",
|
|
49
|
+
"behavior-change": "affected-behavior-full",
|
|
50
|
+
bugfix: "reproduction-plus-neighbors",
|
|
51
|
+
"implementation-optimization": "change-focused-plus-regression-floor",
|
|
52
|
+
};
|
|
53
|
+
const REQUIRED_REGRESSION_FLOOR = {
|
|
54
|
+
"new-operation": [],
|
|
55
|
+
"contract-change": ["main-success-path", "unchanged-response-shape", "changed-contract-boundaries"],
|
|
56
|
+
"behavior-change": ["main-success-path", "unchanged-response-shape", "affected-state-transition"],
|
|
57
|
+
bugfix: ["main-success-path", "unchanged-response-shape", "defect-reproduction", "adjacent-boundary"],
|
|
58
|
+
"implementation-optimization": ["main-success-path", "unchanged-response-shape"],
|
|
59
|
+
};
|
|
60
|
+
const AUTOMATION_BINDING_LABELS = {
|
|
61
|
+
variant: ["变体测试点", "Variant Test Points"],
|
|
62
|
+
assertion: ["场景断言测试点", "Assertion Test Points"],
|
|
63
|
+
"cross-cutting": ["横切证据测试点", "Cross-Cutting Test Points"],
|
|
64
|
+
};
|
|
30
65
|
const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
31
66
|
const inputFileSchema = z.object({ path: z.string().min(1), sha256: sha256Schema }).strict();
|
|
32
67
|
const sourceBindingSchema = z.object({
|
|
@@ -43,8 +78,15 @@ const coverageCaseSchema = z.object({
|
|
|
43
78
|
acIds: z.array(z.string()),
|
|
44
79
|
ruleKeys: z.array(z.string()),
|
|
45
80
|
testPoints: z.array(z.string()),
|
|
81
|
+
testPointBindings: z.array(z.object({
|
|
82
|
+
testPoint: z.string(),
|
|
83
|
+
mode: z.enum(TEST_POINT_BINDING_MODES),
|
|
84
|
+
}).strict()),
|
|
85
|
+
unclassifiedTestPoints: z.array(z.string()),
|
|
86
|
+
duplicateBindingTestPoints: z.array(z.string()),
|
|
46
87
|
scenarioTypes: z.array(z.string()),
|
|
47
88
|
declaredScripts: z.array(z.string()),
|
|
89
|
+
declaredPrimarySymbols: z.array(z.string()),
|
|
48
90
|
}).strict();
|
|
49
91
|
const coverageRuleSchema = z.object({
|
|
50
92
|
ruleKey: z.string().min(1),
|
|
@@ -60,13 +102,23 @@ const coverageRuleSchema = z.object({
|
|
|
60
102
|
missingTestPoints: z.array(z.string()),
|
|
61
103
|
status: z.enum(["COVERED", "PARTIAL", "GAP", "CONFLICT"]),
|
|
62
104
|
}).strict();
|
|
105
|
+
const coverageScopeSchema = z.object({
|
|
106
|
+
changeClassification: z.enum(CHANGE_CLASSIFICATIONS),
|
|
107
|
+
coveragePolicy: z.enum(COVERAGE_POLICIES),
|
|
108
|
+
affectedOperations: z.array(z.string().min(1)),
|
|
109
|
+
affectedRuleKeys: z.array(z.string().min(1)),
|
|
110
|
+
regressionFloor: z.array(z.string().min(1)),
|
|
111
|
+
scopeEvidence: z.array(z.string().min(1)),
|
|
112
|
+
completenessClaim: z.enum(["affected-operations-full", "affected-scope"]),
|
|
113
|
+
}).strict();
|
|
63
114
|
export const backendTestCaseCoverageFactsSchema = z.object({
|
|
64
|
-
schemaId: z.literal("backend-test-case-coverage-facts-
|
|
65
|
-
schemaVersion: z.literal(
|
|
115
|
+
schemaId: z.literal("backend-test-case-coverage-facts-v3"),
|
|
116
|
+
schemaVersion: z.literal(3),
|
|
66
117
|
taskId: z.string().min(1),
|
|
67
118
|
status: z.enum(["PASS", "FAIL", "UNAVAILABLE"]),
|
|
68
119
|
sourceBinding: sourceBindingSchema,
|
|
69
120
|
inputFiles: z.array(inputFileSchema),
|
|
121
|
+
coverageScope: coverageScopeSchema,
|
|
70
122
|
summary: z.object({
|
|
71
123
|
explicitAcCount: z.number().int().min(0),
|
|
72
124
|
coveredAcCount: z.number().int().min(0),
|
|
@@ -86,6 +138,11 @@ export const backendTestCaseCoverageFactsSchema = z.object({
|
|
|
86
138
|
coveredBusinessStateCount: z.number().int().min(0),
|
|
87
139
|
gapCount: z.number().int().min(0),
|
|
88
140
|
conflictCount: z.number().int().min(0),
|
|
141
|
+
variantTestPointCount: z.number().int().min(0),
|
|
142
|
+
assertionTestPointCount: z.number().int().min(0),
|
|
143
|
+
crossCuttingTestPointCount: z.number().int().min(0),
|
|
144
|
+
unclassifiedTestPointCount: z.number().int().min(0),
|
|
145
|
+
duplicateBindingTestPointCount: z.number().int().min(0),
|
|
89
146
|
}).strict(),
|
|
90
147
|
cases: z.array(coverageCaseSchema),
|
|
91
148
|
rules: z.array(coverageRuleSchema),
|
|
@@ -97,7 +154,12 @@ const correspondenceEntrySchema = z.object({
|
|
|
97
154
|
markdownModule: z.string().min(1),
|
|
98
155
|
caseId: z.string().optional(),
|
|
99
156
|
testPoints: z.array(z.string()),
|
|
157
|
+
variantTestPoints: z.array(z.string()),
|
|
158
|
+
assertionTestPoints: z.array(z.string()),
|
|
159
|
+
crossCuttingTestPoints: z.array(z.string()),
|
|
160
|
+
mappedTestPoints: z.array(z.string()),
|
|
100
161
|
declaredScript: z.string().optional(),
|
|
162
|
+
declaredPrimarySymbol: z.string().optional(),
|
|
101
163
|
expectedScript: z.string().min(1),
|
|
102
164
|
actualScripts: z.array(z.string()),
|
|
103
165
|
pytestSymbols: z.array(z.string()),
|
|
@@ -110,14 +172,17 @@ const correspondenceEntrySchema = z.object({
|
|
|
110
172
|
"EXTRA_PYTEST",
|
|
111
173
|
"SCRIPT_MISMATCH",
|
|
112
174
|
"SYMBOL_MISSING_CASE_ID",
|
|
113
|
-
"
|
|
114
|
-
"
|
|
175
|
+
"VARIANT_PARAMETER_MISSING",
|
|
176
|
+
"ASSERTION_BINDING_MISSING",
|
|
177
|
+
"CROSS_CUTTING_EVIDENCE_MISSING",
|
|
178
|
+
"TEST_POINT_BINDING_DUPLICATE",
|
|
179
|
+
"TEST_POINT_BINDING_EXTRA",
|
|
115
180
|
]),
|
|
116
181
|
findings: z.array(z.string()),
|
|
117
182
|
}).strict();
|
|
118
183
|
export const backendTestMarkdownPytestCorrespondenceFactsSchema = z.object({
|
|
119
|
-
schemaId: z.literal("backend-test-markdown-pytest-correspondence-facts-
|
|
120
|
-
schemaVersion: z.literal(
|
|
184
|
+
schemaId: z.literal("backend-test-markdown-pytest-correspondence-facts-v2"),
|
|
185
|
+
schemaVersion: z.literal(2),
|
|
121
186
|
taskId: z.string().min(1),
|
|
122
187
|
status: z.enum(["PASS", "FAIL", "UNAVAILABLE"]),
|
|
123
188
|
inputFiles: z.array(inputFileSchema),
|
|
@@ -126,12 +191,18 @@ export const backendTestMarkdownPytestCorrespondenceFactsSchema = z.object({
|
|
|
126
191
|
exactModuleCount: z.number().int().min(0),
|
|
127
192
|
markdownCaseCount: z.number().int().min(0),
|
|
128
193
|
exactCorrespondenceCount: z.number().int().min(0),
|
|
194
|
+
primarySymbolCount: z.number().int().min(0),
|
|
129
195
|
missingPytestCount: z.number().int().min(0),
|
|
130
196
|
multiplePytestCount: z.number().int().min(0),
|
|
131
197
|
extraPytestCount: z.number().int().min(0),
|
|
132
198
|
scriptMismatchCount: z.number().int().min(0),
|
|
133
199
|
testPointCount: z.number().int().min(0),
|
|
134
200
|
mappedTestPointCount: z.number().int().min(0),
|
|
201
|
+
variantTestPointCount: z.number().int().min(0),
|
|
202
|
+
assertionTestPointCount: z.number().int().min(0),
|
|
203
|
+
crossCuttingTestPointCount: z.number().int().min(0),
|
|
204
|
+
unclassifiedTestPointCount: z.number().int().min(0),
|
|
205
|
+
duplicateBindingTestPointCount: z.number().int().min(0),
|
|
135
206
|
}).strict(),
|
|
136
207
|
entries: z.array(correspondenceEntrySchema),
|
|
137
208
|
findings: z.array(z.string()),
|
|
@@ -165,10 +236,19 @@ function openApiTestPoints(field, dimension, values = []) {
|
|
|
165
236
|
return [`${prefix}-VALID-CLASS`, `${prefix}-VALID-MIXED`, `${prefix}-UPPERCASE`, `${prefix}-WHITESPACE`, `${prefix}-PUNCTUATION`, `${prefix}-SLASH`, `${prefix}-EMOJI`, `${prefix}-CONTROL`];
|
|
166
237
|
return [`${prefix}-PRESENT`, `${prefix}-MISSING`, `${prefix}-NULL`, `${prefix}-WRONG-TYPE`];
|
|
167
238
|
}
|
|
239
|
+
function resolveBoundSourcePath(workspaceRoot, taskId, sourcePath) {
|
|
240
|
+
if (path.isAbsolute(sourcePath))
|
|
241
|
+
return sourcePath;
|
|
242
|
+
const normalized = sourcePath.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
243
|
+
const boundPrefix = `.harness/tasks/${taskId}/`;
|
|
244
|
+
return normalized.startsWith(boundPrefix)
|
|
245
|
+
? path.join(workspaceRoot, ...normalized.split("/"))
|
|
246
|
+
: path.join(workspaceRoot, ".harness", "tasks", taskId, ...normalized.split("/"));
|
|
247
|
+
}
|
|
168
248
|
export async function extractBackendTestOpenApiRules(input) {
|
|
169
249
|
const rules = [];
|
|
170
250
|
for (const referencePath of input.sourceBinding.referencePaths) {
|
|
171
|
-
const physicalPath =
|
|
251
|
+
const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, referencePath);
|
|
172
252
|
let document;
|
|
173
253
|
try {
|
|
174
254
|
const parsed = YAML.parse(await readFile(physicalPath, "utf8"));
|
|
@@ -287,11 +367,8 @@ function orderedUnique(values) {
|
|
|
287
367
|
return result;
|
|
288
368
|
}
|
|
289
369
|
function canonicalCaseId(value) {
|
|
290
|
-
const upper = value.toUpperCase();
|
|
291
|
-
|
|
292
|
-
if (!match)
|
|
293
|
-
return upper;
|
|
294
|
-
return `${match[1].replaceAll("_", "-").replace(/-+/g, "-")}-${match[2].padStart(3, "0")}`;
|
|
370
|
+
const upper = value.toUpperCase().replaceAll("_", "-").replace(/-+/g, "-");
|
|
371
|
+
return /^(BE-[A-Z0-9]+(?:-[A-Z0-9]+)*)-\d{3}$/.test(upper) ? upper : value.toUpperCase();
|
|
295
372
|
}
|
|
296
373
|
function caseIds(value) {
|
|
297
374
|
return orderedUnique((value.match(CASE_ID_IN_TEXT) ?? []).map(canonicalCaseId));
|
|
@@ -323,6 +400,31 @@ function declaredScripts(body) {
|
|
|
323
400
|
return orderedUnique([...automation.matchAll(/testcase\/[A-Za-z0-9_./-]*test_[A-Za-z0-9_.-]*\.py/gi)]
|
|
324
401
|
.map((match) => match[0].replaceAll("\\", "/")));
|
|
325
402
|
}
|
|
403
|
+
function automationLabelBody(body, labels) {
|
|
404
|
+
const automation = sectionBody(body, CASE_SECTION_ALIASES.automation);
|
|
405
|
+
const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
406
|
+
const match = new RegExp(`^\\s*[-*+]\\s*(?:${escaped})\\s*[::]\\s*(.*)$`, "mi").exec(automation);
|
|
407
|
+
return match?.[1]?.replaceAll("`", "").trim() ?? "";
|
|
408
|
+
}
|
|
409
|
+
function testPointBindingFacts(body, testPoints) {
|
|
410
|
+
const bindings = TEST_POINT_BINDING_MODES.flatMap((mode) => {
|
|
411
|
+
const value = automationLabelBody(body, AUTOMATION_BINDING_LABELS[mode]);
|
|
412
|
+
return orderedUnique(value.match(/\bTP-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g) ?? [])
|
|
413
|
+
.map((testPoint) => ({ testPoint, mode }));
|
|
414
|
+
});
|
|
415
|
+
const counts = new Map();
|
|
416
|
+
for (const binding of bindings)
|
|
417
|
+
counts.set(binding.testPoint, (counts.get(binding.testPoint) ?? 0) + 1);
|
|
418
|
+
return {
|
|
419
|
+
testPointBindings: bindings,
|
|
420
|
+
unclassifiedTestPoints: testPoints.filter((testPoint) => !counts.has(testPoint)),
|
|
421
|
+
duplicateBindingTestPoints: testPoints.filter((testPoint) => (counts.get(testPoint) ?? 0) > 1),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function declaredPrimarySymbols(body) {
|
|
425
|
+
const value = automationLabelBody(body, ["primary symbol", "Primary Symbol", "主测试符号"]);
|
|
426
|
+
return orderedUnique(value.match(/\btest_[A-Za-z0-9_]+\b/g) ?? []);
|
|
427
|
+
}
|
|
326
428
|
function cleanTitle(heading, id) {
|
|
327
429
|
return heading.replace(/^##\s+/, "").replace(/^BE-[A-Z0-9_-]+-\d{2,3}\s*(?:[||—–-]\s*)?/i, "").trim() || id;
|
|
328
430
|
}
|
|
@@ -352,15 +454,18 @@ async function parseMarkdownCases(workspaceRoot) {
|
|
|
352
454
|
const heading = headings[index];
|
|
353
455
|
const id = canonicalCaseId(heading[1]);
|
|
354
456
|
const body = markdown.slice(heading.index, headings[index + 1]?.index ?? markdown.length);
|
|
457
|
+
const testPoints = listTokens(body, CASE_SECTION_ALIASES.testPoints, TEST_POINT);
|
|
355
458
|
result.push({
|
|
356
459
|
caseId: id,
|
|
357
460
|
title: cleanTitle(heading[0], id),
|
|
358
461
|
markdownPath: path.relative(workspaceRoot, file).replaceAll(path.sep, "/"),
|
|
359
462
|
acIds: orderedUnique(body.match(AC_ID_IN_TEXT) ?? []),
|
|
360
463
|
ruleKeys: listTokens(body, CASE_SECTION_ALIASES.rules, RULE_KEY),
|
|
361
|
-
testPoints
|
|
464
|
+
testPoints,
|
|
465
|
+
...testPointBindingFacts(body, testPoints),
|
|
362
466
|
scenarioTypes: scenarioTypes(body),
|
|
363
467
|
declaredScripts: declaredScripts(body),
|
|
468
|
+
declaredPrimarySymbols: declaredPrimarySymbols(body),
|
|
364
469
|
body,
|
|
365
470
|
});
|
|
366
471
|
}
|
|
@@ -392,6 +497,89 @@ function splitCellTokens(value, pattern) {
|
|
|
392
497
|
const values = orderedUnique(value.split(/;|<br\s*\/?\s*>|,|、/i).map((item) => item.replaceAll("`", "").trim()).filter(Boolean));
|
|
393
498
|
return pattern ? values.filter((item) => pattern.test(item)) : values;
|
|
394
499
|
}
|
|
500
|
+
function parseCoverageScope(readme) {
|
|
501
|
+
const findings = [];
|
|
502
|
+
const fallback = {
|
|
503
|
+
changeClassification: "new-operation",
|
|
504
|
+
coveragePolicy: "full-contract",
|
|
505
|
+
affectedOperations: [],
|
|
506
|
+
affectedRuleKeys: [],
|
|
507
|
+
regressionFloor: [],
|
|
508
|
+
scopeEvidence: [],
|
|
509
|
+
completenessClaim: "affected-operations-full",
|
|
510
|
+
};
|
|
511
|
+
const marker = /^##\s+Coverage Scope\s*$/mi.exec(readme);
|
|
512
|
+
if (!marker)
|
|
513
|
+
return { scope: fallback, findings: ["README is missing required ## Coverage Scope section; defaulted to full-contract"] };
|
|
514
|
+
const rest = readme.slice(marker.index + marker[0].length);
|
|
515
|
+
const nextHeading = /^##\s+/m.exec(rest);
|
|
516
|
+
const section = rest.slice(0, nextHeading?.index ?? rest.length);
|
|
517
|
+
const lines = section.split(/\r?\n/).filter((line) => line.trim().startsWith("|"));
|
|
518
|
+
if (lines.length < 2 || JSON.stringify(parseTableRow(lines[0])) !== JSON.stringify(["Field", "Value"])) {
|
|
519
|
+
return { scope: fallback, findings: ["Coverage Scope table headers must be exactly: Field | Value"] };
|
|
520
|
+
}
|
|
521
|
+
const separatorCells = lines[1] ? parseTableRow(lines[1]) : [];
|
|
522
|
+
const hasSeparator = separatorCells.length === 2 && separatorCells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
523
|
+
if (!hasSeparator)
|
|
524
|
+
findings.push("Coverage Scope table is missing the required |---|---| separator row");
|
|
525
|
+
const values = new Map();
|
|
526
|
+
for (const line of lines.slice(hasSeparator ? 2 : 1)) {
|
|
527
|
+
const cells = parseTableRow(line);
|
|
528
|
+
if (cells.length !== 2) {
|
|
529
|
+
findings.push(`Coverage Scope row must have exactly 2 columns: ${line.slice(0, 160)}`);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const field = cells[0].trim();
|
|
533
|
+
if (values.has(field))
|
|
534
|
+
findings.push(`duplicate Coverage Scope field: ${field}`);
|
|
535
|
+
values.set(field, cells[1].trim());
|
|
536
|
+
}
|
|
537
|
+
const classificationRaw = (values.get("Change Classification") ?? "").replaceAll("`", "").trim();
|
|
538
|
+
const policyRaw = (values.get("Coverage Policy") ?? "").replaceAll("`", "").trim();
|
|
539
|
+
const changeClassification = CHANGE_CLASSIFICATIONS.includes(classificationRaw)
|
|
540
|
+
? classificationRaw
|
|
541
|
+
: fallback.changeClassification;
|
|
542
|
+
const coveragePolicy = COVERAGE_POLICIES.includes(policyRaw)
|
|
543
|
+
? policyRaw
|
|
544
|
+
: fallback.coveragePolicy;
|
|
545
|
+
if (!CHANGE_CLASSIFICATIONS.includes(classificationRaw))
|
|
546
|
+
findings.push(`invalid or missing Change Classification: ${classificationRaw || "<empty>"}`);
|
|
547
|
+
if (!COVERAGE_POLICIES.includes(policyRaw))
|
|
548
|
+
findings.push(`invalid or missing Coverage Policy: ${policyRaw || "<empty>"}`);
|
|
549
|
+
const expectedPolicy = COVERAGE_POLICY_BY_CLASSIFICATION[changeClassification];
|
|
550
|
+
if (coveragePolicy !== expectedPolicy)
|
|
551
|
+
findings.push(`Change Classification ${changeClassification} requires Coverage Policy ${expectedPolicy}, received ${coveragePolicy}`);
|
|
552
|
+
const affectedOperations = splitCellTokens(values.get("Affected Operations") ?? "").map((value) => value.replace(/\s+/g, " ").trim());
|
|
553
|
+
const affectedRuleKeys = splitCellTokens(values.get("Affected Rule Keys") ?? "", RULE_KEY);
|
|
554
|
+
const regressionFloor = splitCellTokens(values.get("Regression Floor") ?? "").map((value) => value.toLowerCase());
|
|
555
|
+
const scopeEvidence = splitCellTokens(values.get("Scope Evidence") ?? "");
|
|
556
|
+
if (affectedOperations.length === 0)
|
|
557
|
+
findings.push("Coverage Scope requires at least one Affected Operations entry");
|
|
558
|
+
if (affectedRuleKeys.length === 0)
|
|
559
|
+
findings.push("Coverage Scope requires at least one Affected Rule Keys entry");
|
|
560
|
+
if (scopeEvidence.length === 0)
|
|
561
|
+
findings.push("Coverage Scope requires non-empty Scope Evidence");
|
|
562
|
+
for (const required of REQUIRED_REGRESSION_FLOOR[changeClassification]) {
|
|
563
|
+
if (!regressionFloor.includes(required))
|
|
564
|
+
findings.push(`Coverage Scope regression floor is missing required entry: ${required}`);
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
scope: {
|
|
568
|
+
changeClassification,
|
|
569
|
+
coveragePolicy,
|
|
570
|
+
affectedOperations,
|
|
571
|
+
affectedRuleKeys,
|
|
572
|
+
regressionFloor,
|
|
573
|
+
scopeEvidence,
|
|
574
|
+
completenessClaim: coveragePolicy === "full-contract" ? "affected-operations-full" : "affected-scope",
|
|
575
|
+
},
|
|
576
|
+
findings,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function operationFromEndpointField(endpointField) {
|
|
580
|
+
const match = /^([A-Z]+)\s+(\/\S+)/.exec(endpointField.trim());
|
|
581
|
+
return match ? `${match[1]} ${match[2]}` : undefined;
|
|
582
|
+
}
|
|
395
583
|
function parseCoverageMatrix(readme) {
|
|
396
584
|
const findings = [];
|
|
397
585
|
const marker = /^##\s+(?:(?:\d+(?:\.\d+)*[.)]?\s+)?(?:Coverage Matrix|覆盖矩阵))\s*$/mi.exec(readme);
|
|
@@ -452,9 +640,17 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
452
640
|
const findings = [];
|
|
453
641
|
const readmePath = path.join(input.workspaceRoot, "testcase", "md", "README.md");
|
|
454
642
|
const readme = (await exists(readmePath)) ? await readFile(readmePath, "utf8") : "";
|
|
643
|
+
const coverageScope = parseCoverageScope(readme);
|
|
644
|
+
findings.push(...coverageScope.findings);
|
|
455
645
|
const matrix = parseCoverageMatrix(readme);
|
|
456
646
|
findings.push(...matrix.findings);
|
|
457
647
|
const cases = await parseMarkdownCases(input.workspaceRoot);
|
|
648
|
+
for (const file of await markdownModuleFiles(input.workspaceRoot)) {
|
|
649
|
+
const markdown = await readFile(file, "utf8");
|
|
650
|
+
for (const match of markdown.matchAll(NON_CANONICAL_CASE_HEADING)) {
|
|
651
|
+
findings.push(`non-canonical Case ID heading must use BE-<MODULE>-<NNN>: ${match[1]}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
458
654
|
const caseById = new Map(cases.map((item) => [item.caseId, item]));
|
|
459
655
|
const rules = [];
|
|
460
656
|
const evidenceGaps = [];
|
|
@@ -518,10 +714,19 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
518
714
|
});
|
|
519
715
|
}
|
|
520
716
|
const extractedOpenApiRules = await extractBackendTestOpenApiRules(input);
|
|
521
|
-
|
|
717
|
+
const knownOperations = orderedUnique(extractedOpenApiRules.map((rule) => operationFromEndpointField(rule.endpointField)).filter((value) => Boolean(value)));
|
|
718
|
+
for (const affectedOperation of coverageScope.scope.affectedOperations) {
|
|
719
|
+
if (knownOperations.length > 0 && !knownOperations.includes(affectedOperation))
|
|
720
|
+
findings.push(`Coverage Scope Affected Operation is absent from bound OpenAPI evidence: ${affectedOperation}`);
|
|
721
|
+
}
|
|
722
|
+
const requiredOpenApiRules = extractedOpenApiRules.filter((rule) => {
|
|
723
|
+
const operation = operationFromEndpointField(rule.endpointField);
|
|
724
|
+
return Boolean(operation && coverageScope.scope.affectedOperations.includes(operation));
|
|
725
|
+
});
|
|
726
|
+
for (const extracted of requiredOpenApiRules) {
|
|
522
727
|
if (seenRules.has(extracted.ruleKey))
|
|
523
728
|
continue;
|
|
524
|
-
findings.push(`Coverage Matrix is missing documented OpenAPI rule: ${extracted.ruleKey} (${extracted.source})`);
|
|
729
|
+
findings.push(`Coverage Matrix is missing documented in-scope OpenAPI rule: ${extracted.ruleKey} (${extracted.source})`);
|
|
525
730
|
seenRules.add(extracted.ruleKey);
|
|
526
731
|
rules.push({
|
|
527
732
|
...extracted,
|
|
@@ -532,6 +737,11 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
532
737
|
status: "PARTIAL",
|
|
533
738
|
});
|
|
534
739
|
}
|
|
740
|
+
for (const affectedRuleKey of coverageScope.scope.affectedRuleKeys) {
|
|
741
|
+
if (!seenRules.has(affectedRuleKey))
|
|
742
|
+
findings.push(`Coverage Scope Affected Rule Key is absent from Coverage Matrix: ${affectedRuleKey}`);
|
|
743
|
+
}
|
|
744
|
+
const nonCrossCuttingOwners = new Map();
|
|
535
745
|
for (const testCase of cases) {
|
|
536
746
|
if (testCase.ruleKeys.length === 0)
|
|
537
747
|
findings.push(`${testCase.caseId} missing section or entries: 覆盖规则`);
|
|
@@ -539,10 +749,29 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
539
749
|
findings.push(`${testCase.caseId} missing section or entries: 测试点`);
|
|
540
750
|
if (testCase.scenarioTypes.length === 0)
|
|
541
751
|
findings.push(`${testCase.caseId} missing section or entries: 场景类型`);
|
|
752
|
+
if (testCase.declaredPrimarySymbols.length !== 1)
|
|
753
|
+
findings.push(`${testCase.caseId} must declare exactly one primary symbol`);
|
|
754
|
+
if (testCase.unclassifiedTestPoints.length > 0)
|
|
755
|
+
findings.push(`${testCase.caseId} has unclassified Test Points: ${testCase.unclassifiedTestPoints.join(", ")}`);
|
|
756
|
+
if (testCase.duplicateBindingTestPoints.length > 0)
|
|
757
|
+
findings.push(`${testCase.caseId} has duplicate Test Point bindings: ${testCase.duplicateBindingTestPoints.join(", ")}`);
|
|
758
|
+
const extraBindings = testCase.testPointBindings.filter((binding) => !testCase.testPoints.includes(binding.testPoint));
|
|
759
|
+
if (extraBindings.length > 0)
|
|
760
|
+
findings.push(`${testCase.caseId} binds undeclared Test Points: ${orderedUnique(extraBindings.map((binding) => binding.testPoint)).join(", ")}`);
|
|
761
|
+
for (const binding of testCase.testPointBindings.filter((item) => item.mode !== "cross-cutting")) {
|
|
762
|
+
const owners = nonCrossCuttingOwners.get(binding.testPoint) ?? [];
|
|
763
|
+
owners.push(testCase.caseId);
|
|
764
|
+
nonCrossCuttingOwners.set(binding.testPoint, owners);
|
|
765
|
+
}
|
|
542
766
|
for (const ruleKey of testCase.ruleKeys)
|
|
543
767
|
if (!seenRules.has(ruleKey))
|
|
544
768
|
findings.push(`${testCase.caseId} references unknown Rule Key: ${ruleKey}`);
|
|
545
769
|
}
|
|
770
|
+
for (const [testPoint, owners] of nonCrossCuttingOwners) {
|
|
771
|
+
const uniqueOwners = orderedUnique(owners);
|
|
772
|
+
if (uniqueOwners.length > 1)
|
|
773
|
+
findings.push(`${testPoint} is bound as a non-cross-cutting Test Point by multiple Cases: ${uniqueOwners.join(", ")}`);
|
|
774
|
+
}
|
|
546
775
|
const explicitAc = input.sourceBinding.requirementIds.filter((id) => id.startsWith("AC-"));
|
|
547
776
|
const coveredAc = new Set(cases.flatMap((item) => item.acIds));
|
|
548
777
|
const [enumValueCount, coveredEnumValueCount] = dimensionCounts(rules, /enum/);
|
|
@@ -553,8 +782,8 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
553
782
|
const invalidEquivalenceClassCount = rules.reduce((sum, rule) => sum + rule.requiredTestPoints.filter((point) => invalidPointPattern.test(point)).length, 0);
|
|
554
783
|
const coveredInvalidEquivalenceClassCount = rules.reduce((sum, rule) => sum + rule.coveredTestPoints.filter((point) => invalidPointPattern.test(point)).length, 0);
|
|
555
784
|
const facts = backendTestCaseCoverageFactsSchema.parse({
|
|
556
|
-
schemaId: "backend-test-case-coverage-facts-
|
|
557
|
-
schemaVersion:
|
|
785
|
+
schemaId: "backend-test-case-coverage-facts-v3",
|
|
786
|
+
schemaVersion: 3,
|
|
558
787
|
taskId: input.sourceBinding.taskId,
|
|
559
788
|
status: findings.length === 0 ? "PASS" : "FAIL",
|
|
560
789
|
sourceBinding: input.sourceBinding,
|
|
@@ -562,10 +791,11 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
562
791
|
...((await exists(readmePath)) ? [readmePath] : []),
|
|
563
792
|
...(await markdownModuleFiles(input.workspaceRoot)),
|
|
564
793
|
...(await Promise.all(input.sourceBinding.referencePaths.map(async (referencePath) => {
|
|
565
|
-
const physicalPath =
|
|
794
|
+
const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, referencePath);
|
|
566
795
|
return (await exists(physicalPath)) ? physicalPath : "";
|
|
567
796
|
}))).filter(Boolean),
|
|
568
797
|
]),
|
|
798
|
+
coverageScope: coverageScope.scope,
|
|
569
799
|
summary: {
|
|
570
800
|
explicitAcCount: explicitAc.length,
|
|
571
801
|
coveredAcCount: explicitAc.filter((id) => coveredAc.has(id)).length,
|
|
@@ -585,6 +815,11 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
585
815
|
coveredBusinessStateCount,
|
|
586
816
|
gapCount: evidenceGaps.length,
|
|
587
817
|
conflictCount: conflicts.length,
|
|
818
|
+
variantTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "variant").length, 0),
|
|
819
|
+
assertionTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "assertion").length, 0),
|
|
820
|
+
crossCuttingTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "cross-cutting").length, 0),
|
|
821
|
+
unclassifiedTestPointCount: cases.reduce((sum, item) => sum + item.unclassifiedTestPoints.length, 0),
|
|
822
|
+
duplicateBindingTestPointCount: cases.reduce((sum, item) => sum + item.duplicateBindingTestPoints.length, 0),
|
|
588
823
|
},
|
|
589
824
|
cases: cases.map(({ body: _body, ...item }) => item),
|
|
590
825
|
rules,
|
|
@@ -600,6 +835,17 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
600
835
|
"",
|
|
601
836
|
facts.status,
|
|
602
837
|
"",
|
|
838
|
+
"## Coverage Scope",
|
|
839
|
+
"",
|
|
840
|
+
`- Change Classification: ${facts.coverageScope.changeClassification}`,
|
|
841
|
+
`- Coverage Policy: ${facts.coverageScope.coveragePolicy}`,
|
|
842
|
+
`- Completeness Claim: ${facts.coverageScope.completenessClaim}`,
|
|
843
|
+
`- Affected Operations: ${facts.coverageScope.affectedOperations.join(", ") || "Unavailable"}`,
|
|
844
|
+
`- Affected Rule Keys: ${facts.coverageScope.affectedRuleKeys.join(", ") || "Unavailable"}`,
|
|
845
|
+
`- Regression Floor: ${facts.coverageScope.regressionFloor.join(", ") || "Unavailable"}`,
|
|
846
|
+
`- Scope Evidence: ${facts.coverageScope.scopeEvidence.join(", ") || "Unavailable"}`,
|
|
847
|
+
...(facts.coverageScope.completenessClaim === "affected-scope" ? ["- Coverage is scoped to affected operations; it is not whole-API completeness."] : ["- Coverage fully evaluates the declared affected operations; it is not whole-API completeness unless every operation is explicitly listed."]),
|
|
848
|
+
"",
|
|
603
849
|
"## Coverage Summary",
|
|
604
850
|
"",
|
|
605
851
|
"| Dimension | Total | Covered |",
|
|
@@ -611,6 +857,11 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
611
857
|
`| Boundary | ${summary.boundaryPointCount} | ${summary.coveredBoundaryPointCount} |`,
|
|
612
858
|
`| Format | ${summary.formatClassCount} | ${summary.coveredFormatClassCount} |`,
|
|
613
859
|
`| Business State | ${summary.businessStateCount} | ${summary.coveredBusinessStateCount} |`,
|
|
860
|
+
`| Variant bindings | ${summary.variantTestPointCount} | ${summary.variantTestPointCount} |`,
|
|
861
|
+
`| Assertion bindings | ${summary.assertionTestPointCount} | ${summary.assertionTestPointCount} |`,
|
|
862
|
+
`| Cross-cutting bindings | ${summary.crossCuttingTestPointCount} | ${summary.crossCuttingTestPointCount} |`,
|
|
863
|
+
`| Unclassified bindings | ${summary.unclassifiedTestPointCount} | 0 |`,
|
|
864
|
+
`| Duplicate bindings | ${summary.duplicateBindingTestPointCount} | 0 |`,
|
|
614
865
|
"",
|
|
615
866
|
"## Product Requirement Coverage",
|
|
616
867
|
"",
|
|
@@ -658,6 +909,11 @@ function symbolCaseId(symbol) {
|
|
|
658
909
|
const match = symbol.match(/^test_(BE(?:_[A-Z0-9]+)+?_\d{2,3})(?:_|$)/i);
|
|
659
910
|
return match ? canonicalCaseId(match[1].replaceAll("_", "-")) : undefined;
|
|
660
911
|
}
|
|
912
|
+
function metadataTestPoints(region, label) {
|
|
913
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
914
|
+
const match = new RegExp(`^\\s*${escaped}\\s*:\\s*(.*)$`, "mi").exec(region);
|
|
915
|
+
return orderedUnique(match?.[1]?.match(/\bTP-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g) ?? []);
|
|
916
|
+
}
|
|
661
917
|
function pytestFunctionRegionStart(source, functionIndex) {
|
|
662
918
|
let regionStart = source.lastIndexOf("\n", functionIndex - 1) + 1;
|
|
663
919
|
let cursor = regionStart;
|
|
@@ -672,24 +928,40 @@ function pytestFunctionRegionStart(source, functionIndex) {
|
|
|
672
928
|
}
|
|
673
929
|
return regionStart;
|
|
674
930
|
}
|
|
931
|
+
function pytestParameterCollections(source) {
|
|
932
|
+
const collections = new Map();
|
|
933
|
+
for (const match of source.matchAll(/^([A-Z][A-Z0-9_]*)\s*=\s*\[([\s\S]*?)^\]/gm)) {
|
|
934
|
+
collections.set(match[1], orderedUnique([...match[2].matchAll(/\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/g)].map((item) => item[1])));
|
|
935
|
+
}
|
|
936
|
+
return collections;
|
|
937
|
+
}
|
|
675
938
|
function pytestSymbols(script, source) {
|
|
676
939
|
const matches = [...source.matchAll(/^([ \t]*)(?:async\s+)?def\s+(test_[A-Za-z0-9_]+)\s*\([^)]*\)\s*(?:->\s*[^:\r\n]+)?\s*:/gm)];
|
|
677
940
|
const regionStarts = matches.map((match) => pytestFunctionRegionStart(source, match.index));
|
|
941
|
+
const parameterCollections = pytestParameterCollections(source);
|
|
678
942
|
return matches.map((match, index) => {
|
|
679
943
|
const regionStart = regionStarts[index];
|
|
680
944
|
const regionEnd = regionStarts[index + 1] ?? source.length;
|
|
681
|
-
const region = source.slice(
|
|
945
|
+
const region = source.slice(match.index, regionEnd);
|
|
946
|
+
const decorators = source.slice(regionStart, match.index);
|
|
682
947
|
const ids = caseIds(region);
|
|
683
948
|
const fromSymbol = symbolCaseId(match[2]);
|
|
684
949
|
if (fromSymbol)
|
|
685
950
|
ids.unshift(fromSymbol);
|
|
686
|
-
const parameterIds = orderedUnique([
|
|
951
|
+
const parameterIds = orderedUnique([
|
|
952
|
+
...[...decorators.matchAll(/\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/g)].map((item) => item[1]),
|
|
953
|
+
...[...parameterCollections.entries()].flatMap(([name, values]) => new RegExp(`\\b${name}\\b`).test(decorators) ? values : []),
|
|
954
|
+
]);
|
|
955
|
+
const assertionTestPoints = metadataTestPoints(region, "Assertion-Test-Points");
|
|
956
|
+
const crossCuttingTestPoints = metadataTestPoints(region, "Cross-Cutting-Test-Points");
|
|
687
957
|
return {
|
|
688
958
|
script,
|
|
689
959
|
symbol: match[2],
|
|
690
960
|
caseIds: orderedUnique(ids),
|
|
691
|
-
testPoints: orderedUnique(
|
|
961
|
+
testPoints: orderedUnique([...parameterIds, ...assertionTestPoints, ...crossCuttingTestPoints]),
|
|
692
962
|
parameterIds,
|
|
963
|
+
assertionTestPoints,
|
|
964
|
+
crossCuttingTestPoints,
|
|
693
965
|
};
|
|
694
966
|
});
|
|
695
967
|
}
|
|
@@ -715,41 +987,69 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
|
|
|
715
987
|
const actualScripts = orderedUnique(refs.map((item) => item.script));
|
|
716
988
|
const symbols = orderedUnique(refs.map((item) => item.symbol));
|
|
717
989
|
const parameterIds = orderedUnique(refs.flatMap((item) => item.parameterIds));
|
|
718
|
-
const
|
|
719
|
-
|
|
990
|
+
const assertionBindings = orderedUnique(refs.flatMap((item) => item.assertionTestPoints));
|
|
991
|
+
const crossCuttingBindings = orderedUnique(refs.flatMap((item) => item.crossCuttingTestPoints));
|
|
992
|
+
const variantTestPoints = testCase.testPointBindings.filter((item) => item.mode === "variant").map((item) => item.testPoint);
|
|
993
|
+
const assertionTestPoints = testCase.testPointBindings.filter((item) => item.mode === "assertion").map((item) => item.testPoint);
|
|
994
|
+
const crossCuttingTestPoints = testCase.testPointBindings.filter((item) => item.mode === "cross-cutting").map((item) => item.testPoint);
|
|
995
|
+
const mappedTestPoints = orderedUnique([
|
|
996
|
+
...variantTestPoints.filter((point) => parameterIds.includes(point)),
|
|
997
|
+
...assertionTestPoints.filter((point) => assertionBindings.includes(point)),
|
|
998
|
+
...crossCuttingTestPoints.filter((point) => crossCuttingBindings.includes(point)),
|
|
999
|
+
]);
|
|
1000
|
+
const declaredPrimarySymbol = testCase.declaredPrimarySymbols[0];
|
|
1001
|
+
const cardinality = refs.length === 0 ? "1:0" : refs.length === 1 ? "1:1" : "1:N";
|
|
720
1002
|
let status = refs.length === 0 ? "MISSING_PYTEST" : refs.length > 1 ? "MULTIPLE_PYTEST" : "EXACT_1_TO_1";
|
|
721
1003
|
const entryFindings = [];
|
|
722
1004
|
if (refs.length === 0)
|
|
723
1005
|
entryFindings.push(`${testCase.caseId} has no pytest symbol`);
|
|
724
1006
|
if (refs.length > 1)
|
|
725
1007
|
entryFindings.push(`${testCase.caseId} maps to multiple pytest symbols: ${symbols.join(", ")}`);
|
|
726
|
-
if (!declared || declared !== expected || actualScripts.some((script) => script !== expected)) {
|
|
1008
|
+
if (!declared || declared !== expected || actualScripts.some((script) => script !== expected) || !declaredPrimarySymbol || symbols.some((symbol) => symbol !== declaredPrimarySymbol)) {
|
|
727
1009
|
if (refs.length > 0 && status === "EXACT_1_TO_1")
|
|
728
1010
|
status = "SCRIPT_MISMATCH";
|
|
729
|
-
entryFindings.push(`${testCase.caseId} script mapping mismatch:
|
|
1011
|
+
entryFindings.push(`${testCase.caseId} script/primary-symbol mapping mismatch: declaredScript=${declared ?? "none"}, expectedScript=${expected}, actual=${actualScripts.join(", ") || "none"}, declaredPrimarySymbol=${declaredPrimarySymbol ?? "none"}, actualSymbols=${symbols.join(", ") || "none"}`);
|
|
730
1012
|
}
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const extraPoints = parameterIds.filter((point) => !testCase.testPoints.includes(point));
|
|
737
|
-
if (missingPoints.length > 0) {
|
|
1013
|
+
const missingVariant = variantTestPoints.filter((point) => !parameterIds.includes(point));
|
|
1014
|
+
const missingAssertion = assertionTestPoints.filter((point) => !assertionBindings.includes(point));
|
|
1015
|
+
const missingCrossCutting = crossCuttingTestPoints.filter((point) => !crossCuttingBindings.includes(point));
|
|
1016
|
+
const extraPoints = orderedUnique([...parameterIds, ...assertionBindings, ...crossCuttingBindings]).filter((point) => !testCase.testPoints.includes(point));
|
|
1017
|
+
if (testCase.unclassifiedTestPoints.length > 0 || testCase.duplicateBindingTestPoints.length > 0) {
|
|
738
1018
|
if (refs.length > 0 && status === "EXACT_1_TO_1")
|
|
739
|
-
status = "
|
|
740
|
-
entryFindings.push(`${testCase.caseId}
|
|
1019
|
+
status = "TEST_POINT_BINDING_DUPLICATE";
|
|
1020
|
+
entryFindings.push(`${testCase.caseId} invalid Markdown Test Point bindings: unclassified=${testCase.unclassifiedTestPoints.join(", ") || "none"}; duplicate=${testCase.duplicateBindingTestPoints.join(", ") || "none"}`);
|
|
1021
|
+
}
|
|
1022
|
+
if (missingVariant.length > 0) {
|
|
1023
|
+
if (refs.length > 0 && status === "EXACT_1_TO_1")
|
|
1024
|
+
status = "VARIANT_PARAMETER_MISSING";
|
|
1025
|
+
entryFindings.push(`${testCase.caseId} missing variant pytest parameter IDs: ${missingVariant.join(", ")}`);
|
|
1026
|
+
}
|
|
1027
|
+
if (missingAssertion.length > 0) {
|
|
1028
|
+
if (refs.length > 0 && status === "EXACT_1_TO_1")
|
|
1029
|
+
status = "ASSERTION_BINDING_MISSING";
|
|
1030
|
+
entryFindings.push(`${testCase.caseId} missing assertion docstring bindings: ${missingAssertion.join(", ")}`);
|
|
1031
|
+
}
|
|
1032
|
+
if (missingCrossCutting.length > 0) {
|
|
1033
|
+
if (refs.length > 0 && status === "EXACT_1_TO_1")
|
|
1034
|
+
status = "CROSS_CUTTING_EVIDENCE_MISSING";
|
|
1035
|
+
entryFindings.push(`${testCase.caseId} missing cross-cutting evidence bindings: ${missingCrossCutting.join(", ")}`);
|
|
741
1036
|
}
|
|
742
1037
|
if (extraPoints.length > 0) {
|
|
743
1038
|
if (refs.length > 0 && status === "EXACT_1_TO_1")
|
|
744
|
-
status = "
|
|
745
|
-
entryFindings.push(`${testCase.caseId} has extra pytest
|
|
1039
|
+
status = "TEST_POINT_BINDING_EXTRA";
|
|
1040
|
+
entryFindings.push(`${testCase.caseId} has extra pytest Test Point bindings: ${extraPoints.join(", ")}`);
|
|
746
1041
|
}
|
|
747
1042
|
findings.push(...entryFindings);
|
|
748
1043
|
entries.push({
|
|
749
1044
|
markdownModule: testCase.markdownPath,
|
|
750
1045
|
caseId: testCase.caseId,
|
|
751
1046
|
testPoints: testCase.testPoints,
|
|
1047
|
+
variantTestPoints,
|
|
1048
|
+
assertionTestPoints,
|
|
1049
|
+
crossCuttingTestPoints,
|
|
1050
|
+
mappedTestPoints,
|
|
752
1051
|
...(declared ? { declaredScript: declared } : {}),
|
|
1052
|
+
...(declaredPrimarySymbol ? { declaredPrimarySymbol } : {}),
|
|
753
1053
|
expectedScript: expected,
|
|
754
1054
|
actualScripts,
|
|
755
1055
|
pytestSymbols: symbols,
|
|
@@ -772,6 +1072,10 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
|
|
|
772
1072
|
markdownModule: "—",
|
|
773
1073
|
...(unknownId ? { caseId: unknownId } : {}),
|
|
774
1074
|
testPoints: symbol.testPoints,
|
|
1075
|
+
variantTestPoints: [],
|
|
1076
|
+
assertionTestPoints: [],
|
|
1077
|
+
crossCuttingTestPoints: [],
|
|
1078
|
+
mappedTestPoints: [],
|
|
775
1079
|
expectedScript: symbol.script,
|
|
776
1080
|
actualScripts: [symbol.script],
|
|
777
1081
|
pytestSymbols: [symbol.symbol],
|
|
@@ -788,8 +1092,8 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
|
|
|
788
1092
|
}).length;
|
|
789
1093
|
const caseEntries = entries.filter((entry) => entry.cardinality !== "0:1");
|
|
790
1094
|
const facts = backendTestMarkdownPytestCorrespondenceFactsSchema.parse({
|
|
791
|
-
schemaId: "backend-test-markdown-pytest-correspondence-facts-
|
|
792
|
-
schemaVersion:
|
|
1095
|
+
schemaId: "backend-test-markdown-pytest-correspondence-facts-v2",
|
|
1096
|
+
schemaVersion: 2,
|
|
793
1097
|
taskId: input.taskId,
|
|
794
1098
|
status: findings.length === 0 ? "PASS" : "FAIL",
|
|
795
1099
|
inputFiles: await inputFileFacts(input.workspaceRoot, [
|
|
@@ -802,12 +1106,18 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
|
|
|
802
1106
|
exactModuleCount,
|
|
803
1107
|
markdownCaseCount: cases.length,
|
|
804
1108
|
exactCorrespondenceCount: caseEntries.filter((entry) => entry.status === "EXACT_1_TO_1").length,
|
|
1109
|
+
primarySymbolCount: orderedUnique(caseEntries.flatMap((entry) => entry.pytestSymbols)).length,
|
|
805
1110
|
missingPytestCount: caseEntries.filter((entry) => entry.status === "MISSING_PYTEST").length,
|
|
806
1111
|
multiplePytestCount: caseEntries.filter((entry) => entry.status === "MULTIPLE_PYTEST").length,
|
|
807
1112
|
extraPytestCount: entries.filter((entry) => entry.cardinality === "0:1").length,
|
|
808
1113
|
scriptMismatchCount: caseEntries.filter((entry) => entry.status === "SCRIPT_MISMATCH").length,
|
|
809
1114
|
testPointCount: cases.reduce((sum, item) => sum + item.testPoints.length, 0),
|
|
810
|
-
mappedTestPointCount: caseEntries.reduce((sum, entry) => sum + entry.
|
|
1115
|
+
mappedTestPointCount: caseEntries.reduce((sum, entry) => sum + entry.mappedTestPoints.length, 0),
|
|
1116
|
+
variantTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "variant").length, 0),
|
|
1117
|
+
assertionTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "assertion").length, 0),
|
|
1118
|
+
crossCuttingTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "cross-cutting").length, 0),
|
|
1119
|
+
unclassifiedTestPointCount: cases.reduce((sum, item) => sum + item.unclassifiedTestPoints.length, 0),
|
|
1120
|
+
duplicateBindingTestPointCount: cases.reduce((sum, item) => sum + item.duplicateBindingTestPoints.length, 0),
|
|
811
1121
|
},
|
|
812
1122
|
entries,
|
|
813
1123
|
findings: orderedUnique(findings),
|
|
@@ -825,16 +1135,22 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
|
|
|
825
1135
|
`- Exact module mappings: ${facts.summary.exactModuleCount}`,
|
|
826
1136
|
`- Markdown Cases: ${facts.summary.markdownCaseCount}`,
|
|
827
1137
|
`- Exact 1:1: ${facts.summary.exactCorrespondenceCount}`,
|
|
1138
|
+
`- Primary pytest symbols: ${facts.summary.primarySymbolCount}`,
|
|
828
1139
|
`- Missing pytest: ${facts.summary.missingPytestCount}`,
|
|
829
1140
|
`- Multiple pytest: ${facts.summary.multiplePytestCount}`,
|
|
830
1141
|
`- Extra pytest: ${facts.summary.extraPytestCount}`,
|
|
831
1142
|
`- Test Points: ${facts.summary.mappedTestPointCount}/${facts.summary.testPointCount}`,
|
|
1143
|
+
`- Variant Test Points: ${facts.summary.variantTestPointCount}`,
|
|
1144
|
+
`- Assertion Test Points: ${facts.summary.assertionTestPointCount}`,
|
|
1145
|
+
`- Cross-Cutting Test Points: ${facts.summary.crossCuttingTestPointCount}`,
|
|
1146
|
+
`- Unclassified Test Points: ${facts.summary.unclassifiedTestPointCount}`,
|
|
1147
|
+
`- Duplicate Test Point Bindings: ${facts.summary.duplicateBindingTestPointCount}`,
|
|
832
1148
|
"",
|
|
833
1149
|
"## Correspondence Matrix",
|
|
834
1150
|
"",
|
|
835
|
-
"| Markdown Module | Case ID |
|
|
836
|
-
"
|
|
837
|
-
...facts.entries.map((entry) => `| ${entry.markdownModule} | ${entry.caseId ?? "—"} | ${entry.
|
|
1151
|
+
"| Markdown Module | Case ID | Variant | Assertion | Cross-Cutting | Mapped | Declared Script | Primary Symbol | Actual Script | Pytest Symbol | Parameter IDs | Cardinality | Status |",
|
|
1152
|
+
"|---|---|---|---|---|---|---|---|---|---|---|---|---|",
|
|
1153
|
+
...facts.entries.map((entry) => `| ${entry.markdownModule} | ${entry.caseId ?? "—"} | ${entry.variantTestPoints.join(", ") || "—"} | ${entry.assertionTestPoints.join(", ") || "—"} | ${entry.crossCuttingTestPoints.join(", ") || "—"} | ${entry.mappedTestPoints.join(", ") || "—"} | ${entry.declaredScript ?? "—"} | ${entry.declaredPrimarySymbol ?? "—"} | ${entry.actualScripts.join(", ") || "—"} | ${entry.pytestSymbols.join(", ") || "—"} | ${entry.parameterIds.join(", ") || "—"} | ${entry.cardinality} | ${entry.status} |`),
|
|
838
1154
|
"",
|
|
839
1155
|
"## Findings",
|
|
840
1156
|
"",
|
|
@@ -928,6 +1244,7 @@ export async function materializeBackendTestCaseManifestFromFacts(input) {
|
|
|
928
1244
|
...base,
|
|
929
1245
|
materializationStatus,
|
|
930
1246
|
sourceFactsIssues,
|
|
1247
|
+
coverageScope: coverageResult.value?.coverageScope,
|
|
931
1248
|
ruleCoverageSummary: coverageResult.value?.summary,
|
|
932
1249
|
correspondenceSummary: correspondenceResult.value?.summary,
|
|
933
1250
|
artifactRefs: {
|