@tea-agent/loop-agent 0.13.0-beta.0 → 0.13.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 +157 -155
- package/CHANGELOG.md +301 -322
- package/README.md +335 -345
- package/bin/agent-worker.js +22 -22
- package/bin/loop-agent.js +21 -21
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/init.js +597 -528
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/shell-executor.js +200 -21
- package/dist/infrastructure/evaluation/candidate-store.js +5 -1
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/runtime.js +27 -27
- package/dist/worker/observe/static/api.js +46 -46
- package/dist/worker/observe/static/app.js +150 -150
- package/dist/worker/observe/static/constants.js +148 -148
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-helpers.js +172 -172
- 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 +72 -72
- package/dist/worker/observe/static/dom.js +212 -53
- package/dist/worker/observe/static/format-pool.js +67 -67
- package/dist/worker/observe/static/format.js +292 -292
- package/dist/worker/observe/static/index.html +308 -308
- package/dist/worker/observe/static/kpi.js +94 -94
- 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/shell-chrome.js +68 -68
- package/dist/worker/observe/static/state.js +267 -253
- package/dist/worker/observe/static/styles.css +1902 -1902
- 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/dag-inspector.js +627 -607
- package/dist/worker/observe/static/views/dag.js +371 -362
- package/dist/worker/observe/static/views/dashboard.js +509 -252
- 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/pool.js +350 -350
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/session-timeline.js +219 -205
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/task.js +314 -314
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
- package/dist/workflows/dag/backend-test-result-contract.js +568 -0
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/decision-envelope.js +57 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
- package/dist/workflows/dag/frontend-project-capability.js +309 -0
- package/dist/workflows/dag/frontend-repair.js +341 -0
- package/dist/workflows/dag/frontend-risk.js +161 -0
- package/dist/workflows/dag/frontend-verification-trace.js +190 -0
- package/dist/workflows/dag/init-hybrid.js +1020 -125
- package/dist/workflows/dag/repair-artifact.js +43 -3
- package/dist/workflows/dag/skill-instructions.js +4 -2
- package/dist/workflows/dag/types.js +29 -8
- package/docs/README.md +105 -104
- package/docs/agent-dag-recovery-playbook.md +195 -195
- package/docs/agent-dag-runner.md +67 -67
- package/docs/architecture/README.md +26 -26
- package/docs/architecture/dag-execution.md +140 -140
- package/docs/architecture/evolution.md +54 -54
- package/docs/architecture/facts-and-state.md +71 -71
- package/docs/architecture/runtime-boundaries.md +191 -191
- package/docs/architecture/system-overview.md +93 -93
- package/docs/architecture/worker-and-feature.md +85 -85
- package/docs/cursor-prompt-sidecar.md +36 -36
- package/docs/decisions/README.md +18 -18
- package/docs/design/README.md +167 -167
- package/docs/development-principles.md +73 -73
- package/docs/exec-plans/README.md +6 -6
- package/docs/exec-plans/active/README.md +1 -4
- package/docs/exec-plans/completed/README.md +106 -84
- package/docs/feature-workflow.md +414 -389
- package/docs/harness-methodology-debugging.md +153 -153
- package/docs/harness-methodology-tdd.md +130 -130
- package/docs/harness-methodology-verification.md +27 -27
- package/docs/init-surface.manifest.json +307 -289
- package/docs/loop-agent-harness.md +142 -142
- package/docs/production-readiness.md +96 -96
- package/docs/progress/README.md +76 -60
- package/docs/reports/README.md +150 -108
- package/docs/skills/README.md +7 -7
- package/docs/skills/vetted-skill-registry.md +29 -29
- 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-dogfood-report.md +117 -117
- 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/agent-dag.base.json +190 -190
- package/docs/templates/agent-dag.final-verification.json +185 -185
- package/docs/templates/agent-dag.schema.json +411 -411
- package/docs/templates/agent-dag.supervised-implementation.json +620 -501
- package/docs/templates/backend-test-analysis.schema.json +44 -44
- package/docs/templates/backend-test-case-manifest.schema.json +190 -0
- package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +204 -202
- package/docs/templates/backend-test-dag.json +559 -311
- package/docs/templates/backend-test-dag.retrospect.prompt.md +139 -125
- package/docs/templates/backend-test-dag.review-cases.prompt.md +83 -81
- package/docs/templates/backend-test-execution.schema.json +133 -0
- package/docs/templates/backend-test-result.schema.json +99 -0
- package/docs/templates/branch-merge-report.md +93 -0
- package/docs/templates/exec-plan.md +64 -64
- 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 -0
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
- package/docs/templates/frontend-eval/metrics.md +138 -0
- package/docs/templates/frontend-eval/smoke-targets.md +53 -0
- package/docs/templates/frontend-implementation-contract.schema.json +27 -0
- package/docs/templates/frontend-task-constraints.md +35 -35
- package/docs/templates/frontend-task-requirement.md +70 -70
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -5
- package/docs/templates/frontend-test-dag.json +23 -23
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -3
- package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -3
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -3
- package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -3
- package/docs/templates/harness.schema.json +221 -221
- package/docs/templates/hybrid-dag.json +188 -188
- package/docs/templates/init-evolution-review.md +35 -35
- 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/AGENTS.md +8 -8
- package/docs/templates/product-line/README.md +9 -9
- package/docs/templates/product-line/acceptance.yaml +14 -14
- 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/task-graph.yaml +15 -15
- package/docs/templates/product-line/task.yaml +64 -64
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/progress-log.md +17 -17
- 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/docs/verification-matrix.md +70 -70
- package/examples/decision-gate-agent-dag.json +177 -177
- package/examples/example-dag.json +46 -46
- package/examples/hybrid-loop-agent-dag.json +189 -189
- package/harness.json +66 -66
- package/package.json +52 -88
- package/scripts/check-product-line-docs.sh +29 -29
- package/scripts/check-task-pool-root.sh +32 -32
- package/scripts/kb-bootstrap-init-skeleton.sh +240 -240
- package/scripts/kb-graph-incremental-prepare.mjs +386 -386
- package/scripts/kb-graph-incremental-prepare.sh +5 -5
- package/scripts/kb-graph-materialize.mjs +105 -105
- package/scripts/kb-graph-materialize.sh +4 -4
- package/scripts/kb-graph-promote.mjs +164 -164
- package/scripts/kb-graph-promote.sh +4 -4
- package/scripts/kb-query.mjs +554 -554
- package/scripts/kb-query.sh +5 -5
- package/skills/agent-worker/SKILL.md +39 -39
- package/skills/agent-worker/references/agent-worker-operator.md +60 -60
- 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/SKILL.md +196 -0
- package/skills/browser-tools/browser-content.js +103 -0
- package/skills/browser-tools/browser-cookies.js +35 -0
- package/skills/browser-tools/browser-eval.js +53 -0
- package/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/skills/browser-tools/browser-nav.js +44 -0
- package/skills/browser-tools/browser-pick.js +162 -0
- package/skills/browser-tools/browser-screenshot.js +34 -0
- package/skills/browser-tools/browser-start.js +86 -0
- package/skills/browser-tools/package-lock.json +2556 -0
- package/skills/browser-tools/package.json +19 -0
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/frontend-design-review/SKILL.md +66 -66
- package/skills/frontend-design-review/references/review-checklist.md +58 -58
- package/skills/frontend-implementation/SKILL.md +49 -47
- package/skills/frontend-implementation/references/code-standards.md +32 -32
- package/skills/frontend-implementation/references/design-spec.md +46 -46
- package/skills/frontend-implementation/references/node-contracts.md +27 -76
- package/skills/frontend-review/SKILL.md +59 -59
- package/skills/frontend-review/references/review-findings.md +47 -47
- package/skills/frontend-verification/SKILL.md +53 -53
- package/skills/frontend-verification/references/verification-checklist.md +68 -68
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/grill-with-docs/SKILL.md +88 -88
- package/skills/grill-with-docs/adr-format.md +47 -47
- package/skills/grill-with-docs/context-format.md +60 -60
- package/skills/init-capability-evolution/SKILL.md +70 -70
- package/skills/loop-agent/SKILL.md +151 -151
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +527 -505
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/harness-policy.md +263 -263
- package/skills/loop-agent/references/hybrid-dag.md +243 -238
- 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 +36 -36
- package/skills/loop-agent/references/multi-worktree.md +54 -54
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
- package/skills/loop-agent/references/task-workflow.md +89 -89
- package/skills/loop-agent/references/verification-and-failure-handling.md +141 -139
- 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/playwright-cli-case-generator/SKILL.md +74 -74
- 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/SKILL.md +296 -296
- 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/test-driven-development/SKILL.md +20 -20
- 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
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
6
|
+
export const BACKEND_TEST_CASE_MANIFEST_SCHEMA_ID = "backend-test-case-manifest-v1";
|
|
7
|
+
export const BACKEND_TEST_CASE_ID_PATTERN = /^BE-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}$/;
|
|
8
|
+
export const BACKEND_TEST_AC_ID_PATTERN = /^AC-[A-Z0-9]+(?:-[A-Z0-9]+)*$/;
|
|
9
|
+
const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
|
|
10
|
+
const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
|
|
11
|
+
export const backendTestCaseAutomationStatusSchema = z.enum([
|
|
12
|
+
"planned",
|
|
13
|
+
"generated",
|
|
14
|
+
"skipped",
|
|
15
|
+
"unsupported",
|
|
16
|
+
]);
|
|
17
|
+
export const backendTestCaseCategorySchema = z.enum([
|
|
18
|
+
"positive",
|
|
19
|
+
"negative",
|
|
20
|
+
"boundary",
|
|
21
|
+
"state-transition",
|
|
22
|
+
"auth",
|
|
23
|
+
"timeout",
|
|
24
|
+
"concurrency",
|
|
25
|
+
"other",
|
|
26
|
+
]);
|
|
27
|
+
const relativePosixPathSchema = z
|
|
28
|
+
.string()
|
|
29
|
+
.min(1)
|
|
30
|
+
.refine((value) => !path.posix.isAbsolute(value) &&
|
|
31
|
+
!value.includes("\\") &&
|
|
32
|
+
!value.split("/").some((segment) => segment === ".." || segment === ""), "file must be a relative posix path without .. segments");
|
|
33
|
+
const gapSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
acId: z.string().regex(BACKEND_TEST_AC_ID_PATTERN).optional(),
|
|
36
|
+
caseId: z.string().regex(BACKEND_TEST_CASE_ID_PATTERN).optional(),
|
|
37
|
+
description: z.string().min(1),
|
|
38
|
+
evidenceRef: z.string().min(1).optional(),
|
|
39
|
+
})
|
|
40
|
+
.strict()
|
|
41
|
+
.refine((value) => Boolean(value.acId || value.caseId), {
|
|
42
|
+
message: "evidence gap requires acId and/or caseId",
|
|
43
|
+
});
|
|
44
|
+
const caseSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
caseId: z.string().regex(BACKEND_TEST_CASE_ID_PATTERN),
|
|
47
|
+
acIds: z.array(z.string().regex(BACKEND_TEST_AC_ID_PATTERN)).min(1),
|
|
48
|
+
title: z.string().min(1),
|
|
49
|
+
category: backendTestCaseCategorySchema,
|
|
50
|
+
automationStatus: backendTestCaseAutomationStatusSchema,
|
|
51
|
+
endpointRef: z.string().min(1).optional(),
|
|
52
|
+
ruleRefs: z.array(z.string().min(1)).optional(),
|
|
53
|
+
file: relativePosixPathSchema.optional(),
|
|
54
|
+
symbol: z.string().min(1).optional(),
|
|
55
|
+
gapReason: z.string().min(1).optional(),
|
|
56
|
+
evidenceRef: z.string().min(1).optional(),
|
|
57
|
+
})
|
|
58
|
+
.strict()
|
|
59
|
+
.superRefine((value, ctx) => {
|
|
60
|
+
if (value.automationStatus === "generated") {
|
|
61
|
+
if (!value.file) {
|
|
62
|
+
ctx.addIssue({
|
|
63
|
+
code: z.ZodIssueCode.custom,
|
|
64
|
+
message: "generated case requires file",
|
|
65
|
+
path: ["file"],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (!value.symbol) {
|
|
69
|
+
ctx.addIssue({
|
|
70
|
+
code: z.ZodIssueCode.custom,
|
|
71
|
+
message: "generated case requires symbol",
|
|
72
|
+
path: ["symbol"],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (value.automationStatus === "skipped" ||
|
|
77
|
+
value.automationStatus === "unsupported") {
|
|
78
|
+
if (!value.gapReason) {
|
|
79
|
+
ctx.addIssue({
|
|
80
|
+
code: z.ZodIssueCode.custom,
|
|
81
|
+
message: `${value.automationStatus} case requires gapReason`,
|
|
82
|
+
path: ["gapReason"],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
export const backendTestCaseManifestSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
schemaVersion: z.literal(1),
|
|
90
|
+
sourceBinding: z
|
|
91
|
+
.object({
|
|
92
|
+
taskId: z.string().min(1),
|
|
93
|
+
requirementPath: z.string().min(1),
|
|
94
|
+
requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
95
|
+
referencePaths: z.array(z.string().min(1)),
|
|
96
|
+
requirementIds: z.array(z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/)),
|
|
97
|
+
})
|
|
98
|
+
.strict(),
|
|
99
|
+
cases: z.array(caseSchema),
|
|
100
|
+
evidenceGaps: z.array(gapSchema).default([]),
|
|
101
|
+
coverageSummary: z
|
|
102
|
+
.object({
|
|
103
|
+
explicitAcCount: z.number().int().min(0),
|
|
104
|
+
coveredAcCount: z.number().int().min(0),
|
|
105
|
+
gappedAcCount: z.number().int().min(0),
|
|
106
|
+
caseCount: z.number().int().min(0),
|
|
107
|
+
generatedCount: z.number().int().min(0),
|
|
108
|
+
plannedCount: z.number().int().min(0),
|
|
109
|
+
skippedCount: z.number().int().min(0),
|
|
110
|
+
unsupportedCount: z.number().int().min(0),
|
|
111
|
+
/** Deterministic fraction in [0,1]; model must not invent this. */
|
|
112
|
+
acCoverageRatio: z.number().min(0).max(1),
|
|
113
|
+
})
|
|
114
|
+
.strict()
|
|
115
|
+
.optional(),
|
|
116
|
+
})
|
|
117
|
+
.strict();
|
|
118
|
+
function secretIssues(value, at = "$", issues = []) {
|
|
119
|
+
if (typeof value === "string" && SECRET_VALUE.test(value)) {
|
|
120
|
+
issues.push(`${at}: secret-like value is forbidden`);
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
value.forEach((item, index) => secretIssues(item, `${at}[${index}]`, issues));
|
|
124
|
+
}
|
|
125
|
+
else if (value && typeof value === "object") {
|
|
126
|
+
for (const [key, child] of Object.entries(value)) {
|
|
127
|
+
if (SECRET_KEY.test(key)) {
|
|
128
|
+
issues.push(`${at}.${key}: secret-shaped field name is forbidden`);
|
|
129
|
+
}
|
|
130
|
+
secretIssues(child, `${at}.${key}`, issues);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return issues;
|
|
134
|
+
}
|
|
135
|
+
export function extractStrictJsonObject(text) {
|
|
136
|
+
const trimmed = text.trim();
|
|
137
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
138
|
+
return JSON.parse(trimmed);
|
|
139
|
+
}
|
|
140
|
+
const blocks = [...trimmed.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)];
|
|
141
|
+
if (blocks.length !== 1 || trimmed.replace(blocks[0][0], "").trim()) {
|
|
142
|
+
throw new Error("manifest output must be one pure JSON object or one fenced json block with no trailing text");
|
|
143
|
+
}
|
|
144
|
+
return JSON.parse(blocks[0][1]);
|
|
145
|
+
}
|
|
146
|
+
function assertSourceBinding(manifest, binding) {
|
|
147
|
+
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
148
|
+
const references = binding.sources
|
|
149
|
+
.filter((source) => source.kind === "reference")
|
|
150
|
+
.map((source) => source.path)
|
|
151
|
+
.sort();
|
|
152
|
+
const actualReferences = [...manifest.sourceBinding.referencePaths].sort();
|
|
153
|
+
if (!requirement ||
|
|
154
|
+
manifest.sourceBinding.taskId !== binding.taskId ||
|
|
155
|
+
manifest.sourceBinding.requirementPath !== requirement.path ||
|
|
156
|
+
manifest.sourceBinding.requirementSha256 !== requirement.sha256) {
|
|
157
|
+
throw new Error("case manifest source binding does not match DAG requirement source");
|
|
158
|
+
}
|
|
159
|
+
if (JSON.stringify(actualReferences) !== JSON.stringify(references)) {
|
|
160
|
+
throw new Error("case manifest referencePaths do not match DAG source binding");
|
|
161
|
+
}
|
|
162
|
+
if (JSON.stringify(manifest.sourceBinding.requirementIds) !==
|
|
163
|
+
JSON.stringify(binding.requirementIds)) {
|
|
164
|
+
throw new Error("case manifest requirementIds do not match DAG source binding");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function computeCaseManifestCoverageSummary(manifest) {
|
|
168
|
+
const explicitAc = manifest.sourceBinding.requirementIds.filter((id) => id.startsWith("AC-"));
|
|
169
|
+
const covered = new Set();
|
|
170
|
+
for (const item of manifest.cases) {
|
|
171
|
+
for (const acId of item.acIds)
|
|
172
|
+
covered.add(acId);
|
|
173
|
+
}
|
|
174
|
+
const gapped = new Set(manifest.evidenceGaps
|
|
175
|
+
.map((gap) => gap.acId)
|
|
176
|
+
.filter((id) => Boolean(id)));
|
|
177
|
+
const coveredExplicit = explicitAc.filter((id) => covered.has(id));
|
|
178
|
+
const gappedExplicit = explicitAc.filter((id) => !covered.has(id) && gapped.has(id));
|
|
179
|
+
const statusCounts = {
|
|
180
|
+
generatedCount: 0,
|
|
181
|
+
plannedCount: 0,
|
|
182
|
+
skippedCount: 0,
|
|
183
|
+
unsupportedCount: 0,
|
|
184
|
+
};
|
|
185
|
+
for (const item of manifest.cases) {
|
|
186
|
+
if (item.automationStatus === "generated")
|
|
187
|
+
statusCounts.generatedCount += 1;
|
|
188
|
+
else if (item.automationStatus === "planned")
|
|
189
|
+
statusCounts.plannedCount += 1;
|
|
190
|
+
else if (item.automationStatus === "skipped")
|
|
191
|
+
statusCounts.skippedCount += 1;
|
|
192
|
+
else
|
|
193
|
+
statusCounts.unsupportedCount += 1;
|
|
194
|
+
}
|
|
195
|
+
const explicitAcCount = explicitAc.length;
|
|
196
|
+
const coveredAcCount = coveredExplicit.length;
|
|
197
|
+
const acCoverageRatio = explicitAcCount === 0 ? 1 : coveredAcCount / explicitAcCount;
|
|
198
|
+
return {
|
|
199
|
+
explicitAcCount,
|
|
200
|
+
coveredAcCount,
|
|
201
|
+
gappedAcCount: gappedExplicit.length,
|
|
202
|
+
caseCount: manifest.cases.length,
|
|
203
|
+
...statusCounts,
|
|
204
|
+
acCoverageRatio,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export function assertBackendTestCaseManifestInvariants(manifest, binding) {
|
|
208
|
+
if (binding)
|
|
209
|
+
assertSourceBinding(manifest, binding);
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
for (const item of manifest.cases) {
|
|
212
|
+
if (seen.has(item.caseId)) {
|
|
213
|
+
throw new Error(`duplicate caseId: ${item.caseId}`);
|
|
214
|
+
}
|
|
215
|
+
seen.add(item.caseId);
|
|
216
|
+
}
|
|
217
|
+
const explicitAc = new Set(manifest.sourceBinding.requirementIds.filter((id) => id.startsWith("AC-")));
|
|
218
|
+
const knownAc = new Set(explicitAc);
|
|
219
|
+
// Allow ACs listed only via cases when sourceBinding.requirementIds is broader,
|
|
220
|
+
// but unknown relative to binding.requirementIds AC-* still fail when binding present.
|
|
221
|
+
const referencedAc = new Set();
|
|
222
|
+
for (const item of manifest.cases) {
|
|
223
|
+
for (const acId of item.acIds)
|
|
224
|
+
referencedAc.add(acId);
|
|
225
|
+
}
|
|
226
|
+
for (const gap of manifest.evidenceGaps) {
|
|
227
|
+
if (gap.acId)
|
|
228
|
+
referencedAc.add(gap.acId);
|
|
229
|
+
}
|
|
230
|
+
if (binding) {
|
|
231
|
+
const bindingAc = binding.requirementIds.filter((id) => id.startsWith("AC-"));
|
|
232
|
+
for (const acId of referencedAc) {
|
|
233
|
+
if (!bindingAc.includes(acId) && !explicitAc.has(acId)) {
|
|
234
|
+
throw new Error(`unknown AC reference: ${acId}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Unknown = referenced AC not in sourceBinding.requirementIds at all
|
|
238
|
+
for (const acId of referencedAc) {
|
|
239
|
+
if (!manifest.sourceBinding.requirementIds.includes(acId)) {
|
|
240
|
+
// requirementIds may include non-AC; unknown AC means not present
|
|
241
|
+
if (!binding.requirementIds.includes(acId)) {
|
|
242
|
+
throw new Error(`unknown AC reference: ${acId}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Stricter: AC refs must appear in sourceBinding.requirementIds when they are AC-*
|
|
247
|
+
for (const acId of referencedAc) {
|
|
248
|
+
if (!manifest.sourceBinding.requirementIds.includes(acId)) {
|
|
249
|
+
throw new Error(`unknown AC reference: ${acId}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
for (const acId of referencedAc) {
|
|
255
|
+
if (!manifest.sourceBinding.requirementIds.includes(acId)) {
|
|
256
|
+
throw new Error(`unknown AC reference: ${acId}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const coveredByCase = new Set();
|
|
261
|
+
for (const item of manifest.cases) {
|
|
262
|
+
for (const acId of item.acIds)
|
|
263
|
+
coveredByCase.add(acId);
|
|
264
|
+
}
|
|
265
|
+
const gappedAc = new Set(manifest.evidenceGaps
|
|
266
|
+
.map((gap) => gap.acId)
|
|
267
|
+
.filter((id) => Boolean(id)));
|
|
268
|
+
for (const acId of explicitAc) {
|
|
269
|
+
if (!coveredByCase.has(acId) && !gappedAc.has(acId)) {
|
|
270
|
+
throw new Error(`explicit acceptance criterion ${acId} has no case and no evidence gap`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (manifest.coverageSummary) {
|
|
274
|
+
const expected = computeCaseManifestCoverageSummary(manifest);
|
|
275
|
+
const actual = manifest.coverageSummary;
|
|
276
|
+
for (const key of Object.keys(expected)) {
|
|
277
|
+
if (actual[key] !== expected[key]) {
|
|
278
|
+
throw new Error(`coverageSummary.${key} mismatch: expected ${String(expected[key])}, got ${String(actual[key])}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
export async function materializeBackendTestCaseManifest(input) {
|
|
284
|
+
if (!input.sourceBinding) {
|
|
285
|
+
throw new Error("backend-test case manifest gate requires DAG sourceBinding");
|
|
286
|
+
}
|
|
287
|
+
if (!/^[a-z0-9][a-z0-9._-]*\.json$/.test(input.artifactName) ||
|
|
288
|
+
!/^[a-z0-9][a-z0-9._-]*$/.test(input.outputDir)) {
|
|
289
|
+
throw new Error("unsafe structured artifact path");
|
|
290
|
+
}
|
|
291
|
+
const nodePath = path.join(input.runDir, `${input.fromNodeId}.json`);
|
|
292
|
+
const record = JSON.parse(await readFile(nodePath, "utf8"));
|
|
293
|
+
const raw = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
294
|
+
let parsed;
|
|
295
|
+
try {
|
|
296
|
+
parsed = extractStrictJsonObject(raw);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
throw new Error(`invalid-output: ${error instanceof Error ? error.message : String(error)}`);
|
|
300
|
+
}
|
|
301
|
+
const secrets = secretIssues(parsed);
|
|
302
|
+
if (secrets.length) {
|
|
303
|
+
throw new Error(`invalid-output: ${secrets.join("; ")}`);
|
|
304
|
+
}
|
|
305
|
+
const result = backendTestCaseManifestSchema.safeParse(parsed);
|
|
306
|
+
if (!result.success) {
|
|
307
|
+
throw new Error(`invalid-output: ${result.error.issues
|
|
308
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
309
|
+
.join("; ")}`);
|
|
310
|
+
}
|
|
311
|
+
const withSummary = {
|
|
312
|
+
...result.data,
|
|
313
|
+
coverageSummary: result.data.coverageSummary ??
|
|
314
|
+
computeCaseManifestCoverageSummary(result.data),
|
|
315
|
+
};
|
|
316
|
+
assertBackendTestCaseManifestInvariants(withSummary, input.sourceBinding);
|
|
317
|
+
const relativePath = path.posix.join(input.outputDir, input.artifactName);
|
|
318
|
+
const artifactPath = await writeDagRunJsonArtifact(input.runDir, relativePath, withSummary);
|
|
319
|
+
const normalized = `${JSON.stringify(withSummary, null, 2)}\n`;
|
|
320
|
+
return {
|
|
321
|
+
path: artifactPath,
|
|
322
|
+
sha256: createHash("sha256").update(normalized).digest("hex"),
|
|
323
|
+
schemaId: BACKEND_TEST_CASE_MANIFEST_SCHEMA_ID,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/** Convert BE-ORDER-001 → test_BE_ORDER_001 for convention matching. */
|
|
327
|
+
export function caseIdToPytestSymbolPrefix(caseId) {
|
|
328
|
+
return `test_${caseId.replace(/-/g, "_")}`;
|
|
329
|
+
}
|
|
330
|
+
async function walkPyTestFiles(rootDir, relativeDir = "") {
|
|
331
|
+
const abs = relativeDir ? path.join(rootDir, relativeDir) : rootDir;
|
|
332
|
+
let entries;
|
|
333
|
+
try {
|
|
334
|
+
entries = await readdir(abs, { withFileTypes: true });
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
const files = [];
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
const rel = relativeDir
|
|
342
|
+
? path.posix.join(relativeDir.replace(/\\/g, "/"), entry.name)
|
|
343
|
+
: entry.name;
|
|
344
|
+
if (entry.isDirectory()) {
|
|
345
|
+
files.push(...(await walkPyTestFiles(rootDir, rel)));
|
|
346
|
+
}
|
|
347
|
+
else if (entry.isFile() && /^test_.*\.py$/.test(entry.name)) {
|
|
348
|
+
files.push(rel.replace(/\\/g, "/"));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return files;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Locate pytest functions `def test_...` and optional docstring case IDs.
|
|
355
|
+
*/
|
|
356
|
+
export function scanPytestSymbolsInSource(source, file) {
|
|
357
|
+
const hits = [];
|
|
358
|
+
const fnRe = /^[ \t]*(?:async\s+)?def\s+(test_[A-Za-z0-9_]+)\s*\(/gm;
|
|
359
|
+
let match;
|
|
360
|
+
while ((match = fnRe.exec(source))) {
|
|
361
|
+
const symbol = match[1];
|
|
362
|
+
const after = source.slice(match.index + match[0].length, match.index + match[0].length + 400);
|
|
363
|
+
const doc = after.match(/:\s*(?:\r?\n)?[ \t]*(?:"""|''')([\s\S]*?)(?:"""|''')/);
|
|
364
|
+
const firstLine = doc?.[1]?.trim().split(/\r?\n/)[0] ?? "";
|
|
365
|
+
const idMatch = firstLine.match(/\b(BE-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3})\b/);
|
|
366
|
+
const fromName = symbol.match(/^test_(BE_[A-Z0-9]+(?:_[A-Z0-9]+)*_\d{3})(?:_|$)/i);
|
|
367
|
+
let caseId = idMatch?.[1];
|
|
368
|
+
if (!caseId && fromName) {
|
|
369
|
+
const raw = fromName[1];
|
|
370
|
+
// BE_ORDER_001 → BE-ORDER-001 (module may contain underscores → treat last _NNN as seq)
|
|
371
|
+
const m = raw.match(/^(BE(?:_[A-Z0-9]+)+)_(\d{3})$/i);
|
|
372
|
+
if (m) {
|
|
373
|
+
caseId = `${m[1].replace(/_/g, "-").toUpperCase()}-${m[2]}`;
|
|
374
|
+
// Fix BE-ORDER-001 already has dashes from replace; above yields BE-ORDER-001 if raw BE_ORDER_001
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
hits.push({ file, symbol, caseId });
|
|
378
|
+
}
|
|
379
|
+
return hits;
|
|
380
|
+
}
|
|
381
|
+
export async function discoverPytestSymbols(workspaceRoot, testRoot = "testcase") {
|
|
382
|
+
const root = path.join(workspaceRoot, testRoot);
|
|
383
|
+
const files = await walkPyTestFiles(root);
|
|
384
|
+
const hits = [];
|
|
385
|
+
for (const file of files) {
|
|
386
|
+
const abs = path.join(root, file);
|
|
387
|
+
const source = await readFile(abs, "utf8");
|
|
388
|
+
const rel = path.posix.join(testRoot, file);
|
|
389
|
+
hits.push(...scanPytestSymbolsInSource(source, rel));
|
|
390
|
+
}
|
|
391
|
+
return hits;
|
|
392
|
+
}
|
|
393
|
+
function symbolMatchesCase(hit, caseId, declaredSymbol) {
|
|
394
|
+
if (declaredSymbol && hit.symbol === declaredSymbol)
|
|
395
|
+
return true;
|
|
396
|
+
if (hit.caseId === caseId)
|
|
397
|
+
return true;
|
|
398
|
+
const prefix = caseIdToPytestSymbolPrefix(caseId);
|
|
399
|
+
return hit.symbol === prefix || hit.symbol.startsWith(`${prefix}_`);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Traceability gate (W3.4):
|
|
403
|
+
* - skipped/unsupported require gapReason (schema-enforced; re-checked)
|
|
404
|
+
* - generated: declared file+symbol must exist on disk
|
|
405
|
+
* - planned: convention scan — if any pytest symbol maps to caseId it is OK;
|
|
406
|
+
* if none, fail-closed only when *some* generated/automation evidence is expected?
|
|
407
|
+
* MVP: planned cases must either have a convention symbol on disk OR remain planned
|
|
408
|
+
* without generated claim. Fail only when automationStatus=generated is false mapping,
|
|
409
|
+
* OR when a planned case has no symbol AND we require automation — per 需求 MVP:
|
|
410
|
+
* "traceability gate 按约定命名扫描 ... 并与 manifest caseId 对齐"
|
|
411
|
+
* Fail when: generated missing file/symbol; OR planned case with no matching symbol
|
|
412
|
+
* when at least one test_*.py exists and case is not skipped/unsupported.
|
|
413
|
+
* Safer AC-003: generated without real file/symbol fails; planned without symbol is OK
|
|
414
|
+
* (still planned). Convention: if symbol exists for caseId, optional enrichment only.
|
|
415
|
+
*/
|
|
416
|
+
export async function assertBackendTestTraceability(input) {
|
|
417
|
+
const testRoot = input.testRoot ?? "testcase";
|
|
418
|
+
const hits = await discoverPytestSymbols(input.workspaceRoot, testRoot);
|
|
419
|
+
const matched = [];
|
|
420
|
+
for (const item of input.manifest.cases) {
|
|
421
|
+
if (item.automationStatus === "skipped" ||
|
|
422
|
+
item.automationStatus === "unsupported") {
|
|
423
|
+
if (!item.gapReason?.trim()) {
|
|
424
|
+
throw new Error(`traceability: ${item.caseId} is ${item.automationStatus} without gapReason`);
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (item.automationStatus === "generated") {
|
|
429
|
+
if (!item.file || !item.symbol) {
|
|
430
|
+
throw new Error(`traceability: generated case ${item.caseId} missing file/symbol`);
|
|
431
|
+
}
|
|
432
|
+
const abs = path.join(input.workspaceRoot, item.file);
|
|
433
|
+
let st;
|
|
434
|
+
try {
|
|
435
|
+
st = await stat(abs);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
throw new Error(`traceability: generated case ${item.caseId} file not found: ${item.file}`);
|
|
439
|
+
}
|
|
440
|
+
if (!st.isFile()) {
|
|
441
|
+
throw new Error(`traceability: generated case ${item.caseId} path is not a file: ${item.file}`);
|
|
442
|
+
}
|
|
443
|
+
const source = await readFile(abs, "utf8");
|
|
444
|
+
const localHits = scanPytestSymbolsInSource(source, item.file);
|
|
445
|
+
const found = localHits.find((hit) => hit.symbol === item.symbol);
|
|
446
|
+
if (!found) {
|
|
447
|
+
throw new Error(`traceability: generated case ${item.caseId} symbol not found: ${item.symbol} in ${item.file}`);
|
|
448
|
+
}
|
|
449
|
+
// Exact symbol must also bind to the caseId (convention name or docstring/ID).
|
|
450
|
+
// Reject forged mappings like caseId=BE-ORDER-001 + symbol=test_unrelated.
|
|
451
|
+
const bound = found.caseId === item.caseId ||
|
|
452
|
+
(() => {
|
|
453
|
+
const prefix = caseIdToPytestSymbolPrefix(item.caseId);
|
|
454
|
+
return (found.symbol === prefix || found.symbol.startsWith(`${prefix}_`));
|
|
455
|
+
})();
|
|
456
|
+
if (!bound) {
|
|
457
|
+
throw new Error(`traceability: generated case ${item.caseId} symbol ${item.symbol} is not bound to caseId (name convention or docstring ID)`);
|
|
458
|
+
}
|
|
459
|
+
matched.push({
|
|
460
|
+
caseId: item.caseId,
|
|
461
|
+
file: found.file,
|
|
462
|
+
symbol: found.symbol,
|
|
463
|
+
});
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
// planned: if a convention symbol exists, record match; missing is allowed for planned
|
|
467
|
+
const hit = hits.find((candidate) => symbolMatchesCase(candidate, item.caseId, item.symbol));
|
|
468
|
+
if (hit) {
|
|
469
|
+
matched.push({
|
|
470
|
+
caseId: item.caseId,
|
|
471
|
+
file: hit.file,
|
|
472
|
+
symbol: hit.symbol,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return { ok: true, matched };
|
|
477
|
+
}
|
|
478
|
+
export async function runBackendTestTraceabilityGate(input) {
|
|
479
|
+
const relative = input.manifestRelativePath ?? "contracts/backend-test-case-manifest.json";
|
|
480
|
+
const manifestPath = path.join(input.runDir, relative);
|
|
481
|
+
const raw = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
482
|
+
const parsed = backendTestCaseManifestSchema.safeParse(raw);
|
|
483
|
+
if (!parsed.success) {
|
|
484
|
+
throw new Error(`traceability: invalid manifest: ${parsed.error.issues
|
|
485
|
+
.map((issue) => issue.message)
|
|
486
|
+
.join("; ")}`);
|
|
487
|
+
}
|
|
488
|
+
assertBackendTestCaseManifestInvariants(parsed.data);
|
|
489
|
+
const result = await assertBackendTestTraceability({
|
|
490
|
+
workspaceRoot: input.workspaceRoot,
|
|
491
|
+
manifest: parsed.data,
|
|
492
|
+
testRoot: input.testRoot,
|
|
493
|
+
});
|
|
494
|
+
return {
|
|
495
|
+
ok: true,
|
|
496
|
+
matchedCount: result.matched.length,
|
|
497
|
+
manifestPath,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
/** Shell-friendly: build a one-liner that invokes node -e would be heavy; export for shell-executor special command. */
|
|
501
|
+
export function formatTraceabilityGateStdout(result) {
|
|
502
|
+
return `Traceability gate: pass\nManifest: ${result.manifestPath}\nMatched symbols: ${result.matchedCount}`;
|
|
503
|
+
}
|